@hed-hog/contact 0.0.306 → 0.0.310

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.
@@ -63,13 +63,30 @@ type ImportResult = {
63
63
 
64
64
  type ColumnMapping = Record<string, string>;
65
65
 
66
- type CompanyOption = {
67
- id: number;
68
- name: string;
69
- trade_name?: string | null;
70
- };
71
-
72
- type WizardStep = 'upload' | 'preview' | 'mapping' | 'confirm' | 'result';
66
+ type CompanyOption = {
67
+ id: number;
68
+ name: string;
69
+ trade_name?: string | null;
70
+ };
71
+
72
+ type WizardStep = 'upload' | 'preview' | 'mapping' | 'confirm' | 'result';
73
+
74
+ function getImportErrorMessage(error: unknown, fallback: string) {
75
+ if (typeof error === 'object' && error !== null) {
76
+ const response = 'response' in error ? error.response : undefined;
77
+ if (typeof response === 'object' && response !== null && 'data' in response) {
78
+ const data = response.data;
79
+ if (typeof data === 'object' && data !== null && 'message' in data) {
80
+ const message = data.message;
81
+ if (typeof message === 'string') return message;
82
+ }
83
+ }
84
+ if ('message' in error && typeof error.message === 'string') {
85
+ return error.message;
86
+ }
87
+ }
88
+ return fallback;
89
+ }
73
90
 
74
91
  // ─── CRM Field Definitions ───────────────────────────────────────────────────
75
92
 
@@ -147,7 +164,7 @@ function StepIndicator({ current }: { current: WizardStep }) {
147
164
  <Icon className="size-3.5" />
148
165
  )}
149
166
  <span className="text-[10px] font-medium leading-tight hidden sm:block">
150
- {t(step.labelKey as any)}
167
+ {t(step.labelKey as never)}
151
168
  </span>
152
169
  </div>
153
170
  );
@@ -419,7 +436,7 @@ function MappingStep({
419
436
  {duplicateFields
420
437
  .map((field) => {
421
438
  const fieldDef = CRM_FIELDS.find((f) => f.value === field);
422
- const label = fieldDef ? t(fieldDef.labelKey as any) : field;
439
+ const label = fieldDef ? t(fieldDef.labelKey as never) : field;
423
440
  return t('importMappingDuplicateWarning', { field: label });
424
441
  })
425
442
  .join(' ')}
@@ -481,7 +498,7 @@ function MappingStep({
481
498
  value={field.value}
482
499
  className="text-xs"
483
500
  >
484
- {t(field.labelKey as any)}
501
+ {t(field.labelKey as never)}
485
502
  </SelectItem>
486
503
  ))}
487
504
  </SelectContent>
@@ -564,7 +581,7 @@ function ConfirmStep({
564
581
  variant="outline"
565
582
  className="text-[10px] border-primary/30 bg-primary/5 text-primary px-1.5 py-0"
566
583
  >
567
- {fieldDef ? t(fieldDef.labelKey as any) : field}
584
+ {fieldDef ? t(fieldDef.labelKey as never) : field}
568
585
  </Badge>
569
586
  </div>
570
587
  );
@@ -588,9 +605,11 @@ function ConfirmStep({
588
605
  onChange={(val) => {
589
606
  onCompanyChange(val ? Number(val) : null);
590
607
  }}
591
- getOptionValue={(opt) => (opt as any).id}
592
- getOptionLabel={(opt) => (opt as any).name ?? ''}
593
- getOptionDescription={(opt) => (opt as any).trade_name ?? undefined}
608
+ getOptionValue={(opt) => (opt as CompanyOption).id}
609
+ getOptionLabel={(opt) => (opt as CompanyOption).name ?? ''}
610
+ getOptionDescription={(opt) =>
611
+ (opt as CompanyOption).trade_name ?? undefined
612
+ }
594
613
  loadOptions={async ({ page, pageSize, search }) => {
595
614
  const params = new URLSearchParams({
596
615
  page: String(page),
@@ -797,19 +816,29 @@ function ResultStep({
797
816
 
798
817
  // ─── Main Sheet ──────────────────────────────────────────────────────────────
799
818
 
800
- export type PersonImportSheetProps = {
801
- open: boolean;
802
- onOpenChange: (open: boolean) => void;
803
- onSuccess: () => void;
804
- initialCompanyId?: number | null;
805
- };
806
-
807
- export function PersonImportSheet({
808
- open,
809
- onOpenChange,
810
- onSuccess,
811
- initialCompanyId = null,
812
- }: PersonImportSheetProps) {
819
+ export type PersonImportSheetProps = {
820
+ open: boolean;
821
+ onOpenChange: (open: boolean) => void;
822
+ onSuccess: (result?: ImportResult) => void;
823
+ initialCompanyId?: number | null;
824
+ previewUrl?: string;
825
+ importUrl?: string;
826
+ title?: string;
827
+ description?: string;
828
+ nameRequired?: boolean;
829
+ };
830
+
831
+ export function PersonImportSheet({
832
+ open,
833
+ onOpenChange,
834
+ onSuccess,
835
+ initialCompanyId = null,
836
+ previewUrl = '/person/import/preview',
837
+ importUrl = '/person/import',
838
+ title,
839
+ description,
840
+ nameRequired = true,
841
+ }: PersonImportSheetProps) {
813
842
  const t = useTranslations('contact.ContactPage');
814
843
  const { request } = useApp();
815
844
 
@@ -857,8 +886,8 @@ export function PersonImportSheet({
857
886
  }
858
887
  onOpenChange(nextOpen);
859
888
  },
860
- [onOpenChange]
861
- );
889
+ [initialCompanyId, onOpenChange]
890
+ );
862
891
 
863
892
  // ── Auto-initialise mapping from columns ──
864
893
  const initMapping = useCallback((columns: string[]) => {
@@ -870,15 +899,16 @@ export function PersonImportSheet({
870
899
  }, []);
871
900
 
872
901
  // ── Navigation ──
873
- const canGoNext = (): boolean => {
874
- if (step === 'upload') return !!file;
875
- if (step === 'preview') return !!preview && !previewError;
876
- if (step === 'mapping') {
877
- const hasName = Object.values(mapping).includes('name');
878
- return hasName;
879
- }
880
- if (step === 'confirm') return true;
881
- return false;
902
+ const canGoNext = (): boolean => {
903
+ if (step === 'upload') return !!file;
904
+ if (step === 'preview') return !!preview && !previewError;
905
+ if (step === 'mapping') {
906
+ const hasName = Object.values(mapping).includes('name');
907
+ const hasEmail = Object.values(mapping).includes('email');
908
+ return nameRequired ? hasName : hasEmail;
909
+ }
910
+ if (step === 'confirm') return true;
911
+ return false;
882
912
  };
883
913
 
884
914
  const handleNext = async () => {
@@ -896,14 +926,19 @@ export function PersonImportSheet({
896
926
  await fetchPreview();
897
927
  } else if (step === 'preview') {
898
928
  setStep('mapping');
899
- } else if (step === 'mapping') {
900
- const hasName = Object.values(mapping).includes('name');
901
- if (!hasName) {
902
- setMappingError(t('importMappingNameRequired'));
903
- return;
904
- }
905
- setMappingError(null);
906
- setStep('confirm');
929
+ } else if (step === 'mapping') {
930
+ const hasName = Object.values(mapping).includes('name');
931
+ const hasEmail = Object.values(mapping).includes('email');
932
+ if (nameRequired && !hasName) {
933
+ setMappingError(t('importMappingNameRequired'));
934
+ return;
935
+ }
936
+ if (!nameRequired && !hasEmail) {
937
+ setMappingError('Mapeie uma coluna para Email.');
938
+ return;
939
+ }
940
+ setMappingError(null);
941
+ setStep('confirm');
907
942
  } else if (step === 'confirm') {
908
943
  await runImport();
909
944
  }
@@ -926,19 +961,17 @@ export function PersonImportSheet({
926
961
  const formData = new FormData();
927
962
  formData.append('file', file);
928
963
 
929
- const res = await request<ImportPreview>({
930
- url: '/person/import/preview',
931
- method: 'POST',
932
- data: formData,
964
+ const res = await request<ImportPreview>({
965
+ url: previewUrl,
966
+ method: 'POST',
967
+ data: formData,
933
968
  headers: { 'Content-Type': 'multipart/form-data' },
934
969
  });
935
970
 
936
971
  setPreview(res.data);
937
972
  initMapping(res.data.columns);
938
- } catch (err: any) {
939
- const msg =
940
- err?.response?.data?.message ?? err?.message ?? t('importErrorGeneric');
941
- setPreviewError(msg);
973
+ } catch (err: unknown) {
974
+ setPreviewError(getImportErrorMessage(err, t('importErrorGeneric')));
942
975
  } finally {
943
976
  setPreviewLoading(false);
944
977
  }
@@ -957,19 +990,17 @@ export function PersonImportSheet({
957
990
  formData.append('mapping', JSON.stringify(mapping));
958
991
  if (companyId) formData.append('company_id', String(companyId));
959
992
 
960
- const res = await request<ImportResult>({
961
- url: '/person/import',
962
- method: 'POST',
963
- data: formData,
993
+ const res = await request<ImportResult>({
994
+ url: importUrl,
995
+ method: 'POST',
996
+ data: formData,
964
997
  headers: { 'Content-Type': 'multipart/form-data' },
965
998
  });
966
-
967
- setResult(res.data);
968
- onSuccess();
969
- } catch (err: any) {
970
- const msg =
971
- err?.response?.data?.message ?? err?.message ?? t('importErrorGeneric');
972
- setImportError(msg);
999
+
1000
+ setResult(res.data);
1001
+ onSuccess(res.data);
1002
+ } catch (err: unknown) {
1003
+ setImportError(getImportErrorMessage(err, t('importErrorGeneric')));
973
1004
  } finally {
974
1005
  setImportLoading(false);
975
1006
  }
@@ -988,12 +1019,12 @@ export function PersonImportSheet({
988
1019
  <Upload className="h-4 w-4 text-primary" />
989
1020
  </div>
990
1021
  <div>
991
- <SheetTitle className="text-base">
992
- {t('importSheetTitle')}
993
- </SheetTitle>
994
- <SheetDescription className="text-xs">
995
- {t('importSheetDescription')}
996
- </SheetDescription>
1022
+ <SheetTitle className="text-base">
1023
+ {title ?? t('importSheetTitle')}
1024
+ </SheetTitle>
1025
+ <SheetDescription className="text-xs">
1026
+ {description ?? t('importSheetDescription')}
1027
+ </SheetDescription>
997
1028
  </div>
998
1029
  </div>
999
1030
  </SheetHeader>
@@ -91,6 +91,7 @@ export type PersonInteractionType =
91
91
  export type UserOption = {
92
92
  id: number;
93
93
  name: string;
94
+ photo_id?: number | null;
94
95
  };
95
96
 
96
97
  export type PersonStats = {
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
3
4
  import { Badge } from '@/components/ui/badge';
4
5
  import { Button } from '@/components/ui/button';
5
6
  import { Separator } from '@/components/ui/separator';
@@ -309,13 +310,22 @@ export function LeadDetailSheet({
309
310
  <SheetContent className="flex h-full w-full max-w-[95vw] flex-col overflow-hidden p-0 sm:max-w-4xl xl:max-w-5xl">
310
311
  <SheetHeader className="shrink-0 border-b px-5 py-4 text-left">
311
312
  <div className="flex items-start gap-3 pr-6">
312
- <div className="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-muted text-muted-foreground">
313
- {lead.type === 'company' ? (
314
- <Building2 className="size-5" />
315
- ) : (
316
- <User className="size-5" />
317
- )}
318
- </div>
313
+ <Avatar className="size-11 shrink-0 rounded-2xl">
314
+ {lead.avatar_id ? (
315
+ <AvatarImage
316
+ src={`${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${lead.avatar_id}`}
317
+ alt={lead.name}
318
+ className="object-cover"
319
+ />
320
+ ) : null}
321
+ <AvatarFallback className="rounded-2xl bg-muted text-muted-foreground">
322
+ {lead.type === 'company' ? (
323
+ <Building2 className="size-5" />
324
+ ) : (
325
+ <User className="size-5" />
326
+ )}
327
+ </AvatarFallback>
328
+ </Avatar>
319
329
 
320
330
  <div className="min-w-0 flex-1 space-y-2">
321
331
  <div className="space-y-1">
@@ -6,6 +6,7 @@ import {
6
6
  SearchBar,
7
7
  type SearchBarControl,
8
8
  } from '@/components/entity-list';
9
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
9
10
  import { Badge } from '@/components/ui/badge';
10
11
  import {
11
12
  Card,
@@ -27,6 +28,7 @@ import {
27
28
  TooltipTrigger,
28
29
  } from '@/components/ui/tooltip';
29
30
  import { useDebounce } from '@/hooks/use-debounce';
31
+ import { getPhotoUrl } from '@/lib/get-photo-url';
30
32
  import { cn } from '@/lib/utils';
31
33
  import {
32
34
  closestCenter,
@@ -231,6 +233,7 @@ function mapPersonToCrmLead(person: Person): CrmLead {
231
233
  ? person.trade_name || null
232
234
  : person.employer_company?.name || null,
233
235
  tags: Array.isArray(person.tags) ? person.tags.filter(Boolean) : [],
236
+ avatar_id: person.avatar_id ?? null,
234
237
  };
235
238
  }
236
239
 
@@ -808,7 +811,23 @@ export default function CrmPipelinePage() {
808
811
  <Tooltip>
809
812
  <TooltipTrigger asChild>
810
813
  <div className="flex min-w-0 cursor-default items-center gap-1 rounded-md bg-muted/60 px-1.5 py-0.5">
811
- <CircleUser className="size-3 shrink-0 text-muted-foreground" />
814
+ <Avatar className="size-3 shrink-0">
815
+ {lead.owner_user
816
+ ?.photo_id && (
817
+ <AvatarImage
818
+ src={getPhotoUrl(
819
+ lead.owner_user
820
+ .photo_id
821
+ )}
822
+ alt={
823
+ lead.owner_user.name
824
+ }
825
+ />
826
+ )}
827
+ <AvatarFallback className="rounded-full bg-transparent p-0">
828
+ <CircleUser className="size-3 text-muted-foreground" />
829
+ </AvatarFallback>
830
+ </Avatar>
812
831
  <span className="truncate text-[11px] text-muted-foreground">
813
832
  {lead.owner_user?.name ||
814
833
  dashboardT(
@@ -1031,7 +1050,7 @@ export default function CrmPipelinePage() {
1031
1050
 
1032
1051
  <DragOverlay>
1033
1052
  {activeDragLead ? (
1034
- <div className="w-[210px] rounded-xl border border-primary/30 bg-card p-3 shadow-lg">
1053
+ <div className="w-52.5 rounded-xl border border-primary/30 bg-card p-3 shadow-lg">
1035
1054
  <p className="line-clamp-2 text-xs font-semibold leading-4">
1036
1055
  {activeDragLead.name}
1037
1056
  </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/contact",
3
- "version": "0.0.306",
3
+ "version": "0.0.310",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -9,13 +9,13 @@
9
9
  "@nestjs/core": "^11",
10
10
  "@nestjs/jwt": "^11",
11
11
  "@nestjs/mapped-types": "*",
12
- "@hed-hog/core": "0.0.306",
13
- "@hed-hog/address": "0.0.306",
12
+ "@hed-hog/core": "0.0.310",
14
13
  "@hed-hog/api-prisma": "0.0.6",
15
- "@hed-hog/api": "0.0.6",
16
- "@hed-hog/api-locale": "0.0.14",
17
14
  "@hed-hog/api-mail": "0.0.9",
18
- "@hed-hog/api-pagination": "0.0.7"
15
+ "@hed-hog/api-pagination": "0.0.7",
16
+ "@hed-hog/api-locale": "0.0.14",
17
+ "@hed-hog/address": "0.0.310",
18
+ "@hed-hog/api": "0.0.6"
19
19
  },
20
20
  "exports": {
21
21
  ".": {
@@ -1,20 +1,20 @@
1
1
  import { Type } from 'class-transformer';
2
2
  import {
3
- IsArray,
4
- IsEmail,
5
- IsIn,
6
- IsInt,
7
- IsNumber,
8
- IsOptional,
9
- IsString,
10
- MaxLength,
11
- ValidateNested,
3
+ IsArray,
4
+ IsEmail,
5
+ IsIn,
6
+ IsInt,
7
+ IsNumber,
8
+ IsOptional,
9
+ IsString,
10
+ MaxLength,
11
+ ValidateNested,
12
12
  } from 'class-validator';
13
13
  import { PersonStatus } from './create.dto';
14
14
  import {
15
- UpdateAllAddressDTO,
16
- UpdateAllContactDTO,
17
- UpdateAllDocumentDTO,
15
+ UpdateAllAddressDTO,
16
+ UpdateAllContactDTO,
17
+ UpdateAllDocumentDTO,
18
18
  } from './update.dto';
19
19
 
20
20
  export const ACCOUNT_LIFECYCLE_STAGES = [
@@ -126,6 +126,10 @@ export class CreateAccountDTO {
126
126
  @ValidateNested({ each: true })
127
127
  @Type(() => UpdateAllDocumentDTO)
128
128
  documents?: UpdateAllDocumentDTO[];
129
+
130
+ @IsOptional()
131
+ @IsInt()
132
+ avatar_id?: number | null;
129
133
  }
130
134
 
131
135
  export class UpdateAccountDTO extends CreateAccountDTO {}
@@ -126,6 +126,7 @@ type AccountListItem = {
126
126
  state: string | null;
127
127
  created_at: string;
128
128
  last_interaction_at: string | null;
129
+ avatar_id: number | null;
129
130
  };
130
131
 
131
132
  type CrmActivityListParams = Omit<PaginationDTO, 'sortField' | 'sortOrder'> &
@@ -1348,6 +1349,7 @@ export class PersonService {
1348
1349
  name: normalizedName,
1349
1350
  type: 'company',
1350
1351
  status: data.status,
1352
+ avatar_id: data.avatar_id ?? null,
1351
1353
  },
1352
1354
  });
1353
1355
 
@@ -1391,6 +1393,7 @@ export class PersonService {
1391
1393
  name: nextName,
1392
1394
  type: 'company',
1393
1395
  status: data.status ?? person.status,
1396
+ avatar_id: data.avatar_id === undefined ? person.avatar_id : data.avatar_id,
1394
1397
  },
1395
1398
  });
1396
1399
 
@@ -4365,6 +4368,7 @@ export class PersonService {
4365
4368
  name: true,
4366
4369
  status: true,
4367
4370
  type: true,
4371
+ avatar_id: true,
4368
4372
  },
4369
4373
  });
4370
4374
 
@@ -4426,6 +4430,7 @@ export class PersonService {
4426
4430
  created_at: person.created_at?.toISOString?.() ?? String(person.created_at),
4427
4431
  last_interaction_at:
4428
4432
  this.normalizeDateTimeOrNull(person.last_interaction_at) ?? null,
4433
+ avatar_id: person.avatar_id ?? null,
4429
4434
  };
4430
4435
  }
4431
4436