@hed-hog/contact 0.0.306 → 0.0.309

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.
@@ -8,7 +8,7 @@ import {
8
8
  type Person,
9
9
  } from '@/app/(app)/(libraries)/contact/person/_components/person-types';
10
10
  import { CopyButton } from '@/components/copy-button';
11
- import { Avatar, AvatarFallback } from '@/components/ui/avatar';
11
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
12
12
  import { Badge } from '@/components/ui/badge';
13
13
  import { Button } from '@/components/ui/button';
14
14
  import {
@@ -27,6 +27,7 @@ import {
27
27
  } from '@/components/ui/form';
28
28
  import { FormActions } from '@/components/ui/form-actions';
29
29
  import { Input } from '@/components/ui/input';
30
+ import { Progress } from '@/components/ui/progress';
30
31
  import {
31
32
  Select,
32
33
  SelectContent,
@@ -71,6 +72,7 @@ import {
71
72
  Plus,
72
73
  Star,
73
74
  Trash2,
75
+ Upload,
74
76
  Users,
75
77
  XCircle,
76
78
  } from 'lucide-react';
@@ -85,6 +87,7 @@ import {
85
87
  useState,
86
88
  } from 'react';
87
89
  import { useForm, useWatch } from 'react-hook-form';
90
+ import { toast } from 'sonner';
88
91
  import { z } from 'zod';
89
92
  import type {
90
93
  Account,
@@ -222,6 +225,13 @@ function getPersonInitials(name: string) {
222
225
  .join('');
223
226
  }
224
227
 
228
+ function getPersonAvatarUrl(avatarId?: number | null) {
229
+ if (typeof avatarId === 'number' && avatarId > 0) {
230
+ return `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`;
231
+ }
232
+ return undefined;
233
+ }
234
+
225
235
  function SectionHeader({
226
236
  title,
227
237
  description,
@@ -246,6 +256,9 @@ function SectionHeader({
246
256
 
247
257
  const ACCOUNT_FORM_DRAFT_STORAGE_KEY = 'contact-account-form-draft';
248
258
 
259
+ type UploadedFilePayload = { id?: number; filename?: string };
260
+ type OpenFilePayload = { url?: string };
261
+
249
262
  type AccountDraftPayload = {
250
263
  mode: 'create' | 'edit';
251
264
  savedAccountId: number | null;
@@ -255,6 +268,8 @@ type AccountDraftPayload = {
255
268
  documents: EditableAccountDocument[];
256
269
  localCollaborators: Person[];
257
270
  removedCollaboratorIds: number[];
271
+ avatarId: number | null;
272
+ avatarPreviewUrl: string;
258
273
  };
259
274
 
260
275
  export function AccountFormSheet({
@@ -296,6 +311,13 @@ export function AccountFormSheet({
296
311
  const contactValueRefs = useRef<Record<string, HTMLInputElement | null>>({});
297
312
  const addressLine1Refs = useRef<Record<string, HTMLInputElement | null>>({});
298
313
  const documentValueRefs = useRef<Record<string, HTMLInputElement | null>>({});
314
+ const fileInputRef = useRef<HTMLInputElement>(null);
315
+ const persistedAvatarIdRef = useRef<number | null>(null);
316
+
317
+ const [avatarId, setAvatarId] = useState<number | null>(null);
318
+ const [avatarPreviewUrl, setAvatarPreviewUrl] = useState('');
319
+ const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
320
+ const [avatarUploadProgress, setAvatarUploadProgress] = useState(0);
299
321
 
300
322
  const emptyCollaboratorPage = useMemo<PaginatedResult<Person>>(
301
323
  () => ({
@@ -516,7 +538,8 @@ export function AccountFormSheet({
516
538
  addresses.length > 0 ||
517
539
  documents.length > 0 ||
518
540
  localCollaborators.length > 0 ||
519
- removedCollaboratorIds.length > 0
541
+ removedCollaboratorIds.length > 0 ||
542
+ avatarId != null
520
543
  ),
521
544
  [
522
545
  watchedFormValues,
@@ -525,6 +548,7 @@ export function AccountFormSheet({
525
548
  documents,
526
549
  localCollaborators,
527
550
  removedCollaboratorIds,
551
+ avatarId,
528
552
  ]
529
553
  );
530
554
 
@@ -548,9 +572,13 @@ export function AccountFormSheet({
548
572
  documents,
549
573
  localCollaborators,
550
574
  removedCollaboratorIds,
575
+ avatarId,
576
+ avatarPreviewUrl,
551
577
  }),
552
578
  [
553
579
  addresses,
580
+ avatarId,
581
+ avatarPreviewUrl,
554
582
  contacts,
555
583
  currentAccountId,
556
584
  documents,
@@ -700,6 +728,127 @@ export function AccountFormSheet({
700
728
  [getDocumentTypeCode]
701
729
  );
702
730
 
731
+ const resolveApiUrl = (url: string) =>
732
+ /^https?:\/\//i.test(url)
733
+ ? url
734
+ : `${String(process.env.NEXT_PUBLIC_API_BASE_URL ?? '')}${url}`;
735
+
736
+ const getAccountAvatarUrl = useCallback((fileId: number) => {
737
+ return `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${fileId}`;
738
+ }, []);
739
+
740
+ const deleteFileById = useCallback(
741
+ async (fileId: number) => {
742
+ try {
743
+ await request({
744
+ method: 'DELETE',
745
+ url: '/file',
746
+ data: { ids: [fileId] },
747
+ });
748
+ } catch {
749
+ // silent cleanup failure
750
+ }
751
+ },
752
+ [request]
753
+ );
754
+
755
+ const cleanupUnsavedAvatar = useCallback(async () => {
756
+ if (avatarId !== null && avatarId !== persistedAvatarIdRef.current) {
757
+ await deleteFileById(avatarId);
758
+ }
759
+ }, [avatarId, deleteFileById]);
760
+
761
+ const handleAvatarUpload = useCallback(
762
+ async (file: File) => {
763
+ const MAX_SIZE = 5 * 1024 * 1024;
764
+ const ALLOWED = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
765
+
766
+ if (!ALLOWED.includes(file.type)) {
767
+ toast.error(personT('avatarInvalidType'));
768
+ return;
769
+ }
770
+
771
+ if (file.size > MAX_SIZE) {
772
+ toast.error(personT('avatarTooLarge'));
773
+ return;
774
+ }
775
+
776
+ const previousAvatarId = avatarId;
777
+
778
+ setIsUploadingAvatar(true);
779
+ setAvatarUploadProgress(0);
780
+
781
+ try {
782
+ const formData = new FormData();
783
+ formData.append('file', file);
784
+
785
+ const { data: uploaded } = await request<UploadedFilePayload>({
786
+ method: 'POST',
787
+ url: '/file',
788
+ data: formData,
789
+ headers: { 'Content-Type': 'multipart/form-data' },
790
+ onUploadProgress: (event: { loaded: number; total?: number }) => {
791
+ const total = event.total ?? 0;
792
+ if (total > 0) {
793
+ setAvatarUploadProgress(Math.round((event.loaded / total) * 100));
794
+ }
795
+ },
796
+ });
797
+
798
+ const newFileId = Number(uploaded?.id);
799
+ if (!newFileId) {
800
+ toast.error(personT('avatarUploadError'));
801
+ return;
802
+ }
803
+
804
+ const { data: opened } = await request<OpenFilePayload>({
805
+ method: 'PUT',
806
+ url: `/file/open/${newFileId}`,
807
+ });
808
+
809
+ const tempUrl = String(opened?.url || '').trim();
810
+
811
+ if (
812
+ previousAvatarId !== null &&
813
+ previousAvatarId !== persistedAvatarIdRef.current
814
+ ) {
815
+ await deleteFileById(previousAvatarId);
816
+ }
817
+
818
+ setAvatarId(newFileId);
819
+ setAvatarPreviewUrl(
820
+ tempUrl.length > 0
821
+ ? resolveApiUrl(tempUrl)
822
+ : getAccountAvatarUrl(newFileId)
823
+ );
824
+ setAvatarUploadProgress(100);
825
+ toast.success(personT('avatarUploadSuccess'));
826
+ } catch {
827
+ toast.error(personT('avatarUploadError'));
828
+ setAvatarUploadProgress(0);
829
+ } finally {
830
+ setIsUploadingAvatar(false);
831
+ if (fileInputRef.current) {
832
+ fileInputRef.current.value = '';
833
+ }
834
+ }
835
+ },
836
+ [avatarId, deleteFileById, getAccountAvatarUrl, personT, request]
837
+ );
838
+
839
+ const handleSelectAvatar = useCallback(() => {
840
+ fileInputRef.current?.click();
841
+ }, []);
842
+
843
+ const handleRemoveAvatar = useCallback(async () => {
844
+ if (avatarId !== null && avatarId !== persistedAvatarIdRef.current) {
845
+ await deleteFileById(avatarId);
846
+ }
847
+ setAvatarId(null);
848
+ setAvatarPreviewUrl('');
849
+ toast.success(personT('avatarRemoveSuccess'));
850
+ }, [avatarId, deleteFileById, personT]);
851
+
703
852
  const getPersonPrimaryContact = (
704
853
  person: Person,
705
854
  codes: string[]
@@ -852,12 +1001,21 @@ export function AccountFormSheet({
852
1001
  setCollaboratorPickerKey(0);
853
1002
  setCollaboratorSheetOpen(false);
854
1003
  setCollaboratorToEdit(null);
1004
+ const initAvatarId =
1005
+ storedDraft?.payload.avatarId ?? account?.avatar_id ?? null;
1006
+ setAvatarId(initAvatarId);
1007
+ persistedAvatarIdRef.current = account?.avatar_id ?? null;
1008
+ setAvatarPreviewUrl(
1009
+ storedDraft?.payload.avatarPreviewUrl ??
1010
+ (initAvatarId ? getAccountAvatarUrl(initAvatarId) : '')
1011
+ );
855
1012
  });
856
1013
 
857
1014
  return () => window.cancelAnimationFrame(frame);
858
1015
  }, [
859
1016
  account,
860
1017
  form,
1018
+ getAccountAvatarUrl,
861
1019
  loadDraft,
862
1020
  maskContactValueByType,
863
1021
  maskDocumentValueByType,
@@ -1114,6 +1272,7 @@ export function AccountFormSheet({
1114
1272
  contacts: normalizedContacts,
1115
1273
  addresses: normalizedAddresses,
1116
1274
  documents: normalizedDocuments,
1275
+ avatar_id: avatarId,
1117
1276
  },
1118
1277
  currentAccountId
1119
1278
  );
@@ -1122,10 +1281,12 @@ export function AccountFormSheet({
1122
1281
  if (!currentAccountId) {
1123
1282
  if (nextAccountId) {
1124
1283
  setSavedAccountId(nextAccountId);
1284
+ persistedAvatarIdRef.current = avatarId;
1125
1285
  }
1126
1286
  return;
1127
1287
  }
1128
1288
 
1289
+ persistedAvatarIdRef.current = avatarId;
1129
1290
  clearDraft();
1130
1291
  onOpenChange(false);
1131
1292
  };
@@ -1140,6 +1301,10 @@ export function AccountFormSheet({
1140
1301
  setLocalCollaborators([]);
1141
1302
  setRemovedCollaboratorIds([]);
1142
1303
  setCollaboratorPickerKey(0);
1304
+ void cleanupUnsavedAvatar();
1305
+ setAvatarId(null);
1306
+ persistedAvatarIdRef.current = null;
1307
+ setAvatarPreviewUrl('');
1143
1308
  }
1144
1309
 
1145
1310
  onOpenChange(nextOpen);
@@ -1233,6 +1398,79 @@ export function AccountFormSheet({
1233
1398
  )}
1234
1399
  />
1235
1400
  </div>
1401
+
1402
+ <div className="flex items-start gap-3">
1403
+ <Avatar className="h-16 w-16 rounded-md border">
1404
+ <AvatarImage
1405
+ src={avatarPreviewUrl || undefined}
1406
+ alt={watchedName || t('form.companyName')}
1407
+ className="rounded-md object-cover"
1408
+ />
1409
+ <AvatarFallback className="text-sm font-semibold uppercase">
1410
+ {companyPreviewName.slice(0, 2).toUpperCase()}
1411
+ </AvatarFallback>
1412
+ </Avatar>
1413
+ <div className="min-w-0 flex-1 space-y-2">
1414
+ <input
1415
+ ref={fileInputRef}
1416
+ type="file"
1417
+ accept="image/*"
1418
+ className="hidden"
1419
+ onChange={(event) => {
1420
+ const file = event.target.files?.[0];
1421
+ if (!file) return;
1422
+ void handleAvatarUpload(file);
1423
+ }}
1424
+ />
1425
+ <div className="flex flex-wrap gap-2">
1426
+ <Button
1427
+ type="button"
1428
+ variant="outline"
1429
+ size="sm"
1430
+ onClick={handleSelectAvatar}
1431
+ disabled={isUploadingAvatar}
1432
+ >
1433
+ {isUploadingAvatar ? (
1434
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1435
+ ) : (
1436
+ <Upload className="mr-2 h-4 w-4" />
1437
+ )}
1438
+ {avatarId
1439
+ ? personT('avatarReplace')
1440
+ : personT('avatarUpload')}
1441
+ </Button>
1442
+ {avatarId || isUploadingAvatar ? (
1443
+ <Button
1444
+ type="button"
1445
+ variant="ghost"
1446
+ size="sm"
1447
+ className="text-red-600 hover:text-red-700"
1448
+ onClick={() => void handleRemoveAvatar()}
1449
+ disabled={!avatarId || isUploadingAvatar}
1450
+ >
1451
+ <Trash2 className="mr-2 h-4 w-4" />
1452
+ {personT('avatarRemove')}
1453
+ </Button>
1454
+ ) : null}
1455
+ </div>
1456
+ <p className="text-xs text-muted-foreground">
1457
+ {personT('avatarGuidelines')}
1458
+ </p>
1459
+ {isUploadingAvatar ? (
1460
+ <div className="space-y-1">
1461
+ <Progress
1462
+ value={avatarUploadProgress}
1463
+ className="h-2"
1464
+ />
1465
+ <p className="text-xs text-muted-foreground">
1466
+ {personT('avatarUploadingProgress', {
1467
+ progress: avatarUploadProgress,
1468
+ })}
1469
+ </p>
1470
+ </div>
1471
+ ) : null}
1472
+ </div>
1473
+ </div>
1236
1474
  </section>
1237
1475
 
1238
1476
  <section className="space-y-3">
@@ -2083,6 +2321,33 @@ export function AccountFormSheet({
2083
2321
  loadOptions={loadCollaboratorOptions}
2084
2322
  getOptionValue={(person) => person.id}
2085
2323
  getOptionLabel={(person) => person.name}
2324
+ renderOption={({ option }) => (
2325
+ <div className="flex min-w-0 items-center gap-2">
2326
+ <Avatar className="h-7 w-7 shrink-0">
2327
+ <AvatarImage
2328
+ src={getPersonAvatarUrl(option.avatar_id)}
2329
+ alt={option.name}
2330
+ />
2331
+ <AvatarFallback className="text-xs font-semibold uppercase">
2332
+ {getPersonInitials(option.name) || '?'}
2333
+ </AvatarFallback>
2334
+ </Avatar>
2335
+ <div className="min-w-0">
2336
+ <div className="truncate text-sm">
2337
+ {option.name}
2338
+ </div>
2339
+ {option.job_title ? (
2340
+ <div className="truncate text-xs text-muted-foreground">
2341
+ {option.job_title} · #{option.id}
2342
+ </div>
2343
+ ) : (
2344
+ <div className="text-xs text-muted-foreground">
2345
+ #{option.id}
2346
+ </div>
2347
+ )}
2348
+ </div>
2349
+ </div>
2350
+ )}
2086
2351
  getOptionDescription={(person) =>
2087
2352
  [
2088
2353
  person.job_title,
@@ -80,6 +80,7 @@ export type Account = {
80
80
  state?: string | null;
81
81
  created_at: string;
82
82
  last_interaction_at?: string | null;
83
+ avatar_id?: number | null;
83
84
  contact?: AccountContact[];
84
85
  address?: AccountAddress[];
85
86
  document?: AccountDocument[];
@@ -113,4 +114,5 @@ export type AccountFormValues = {
113
114
  contacts?: AccountContact[];
114
115
  addresses?: AccountAddress[];
115
116
  documents?: AccountDocument[];
117
+ avatar_id?: number | null;
116
118
  };
@@ -17,7 +17,7 @@ import {
17
17
  AlertDialogHeader,
18
18
  AlertDialogTitle,
19
19
  } from '@/components/ui/alert-dialog';
20
- import { Avatar, AvatarFallback } from '@/components/ui/avatar';
20
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
21
21
  import { Badge } from '@/components/ui/badge';
22
22
  import { Button } from '@/components/ui/button';
23
23
  import { Card, CardContent } from '@/components/ui/card';
@@ -434,6 +434,10 @@ export default function AccountsPage() {
434
434
  contact: data.contacts ?? previous.contact ?? [],
435
435
  address: data.addresses ?? previous.address ?? [],
436
436
  document: data.documents ?? previous.document ?? [],
437
+ avatar_id:
438
+ data.avatar_id !== undefined
439
+ ? data.avatar_id
440
+ : (previous.avatar_id ?? null),
437
441
  }
438
442
  : previous
439
443
  );
@@ -475,6 +479,7 @@ export default function AccountsPage() {
475
479
  contact: data.contacts ?? [],
476
480
  address: data.addresses ?? [],
477
481
  document: data.documents ?? [],
482
+ avatar_id: data.avatar_id ?? null,
478
483
  });
479
484
  }
480
485
 
@@ -555,6 +560,13 @@ export default function AccountsPage() {
555
560
  return (
556
561
  <div className="flex items-center gap-3">
557
562
  <Avatar className="h-9 w-9 rounded-lg">
563
+ {account.avatar_id ? (
564
+ <AvatarImage
565
+ src={`${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${account.avatar_id}`}
566
+ alt={account.name}
567
+ className="rounded-lg object-cover"
568
+ />
569
+ ) : null}
558
570
  <AvatarFallback className="rounded-lg bg-slate-100 text-xs font-semibold uppercase text-slate-700">
559
571
  {getAccountInitials(account.name)}
560
572
  </AvatarFallback>
@@ -947,6 +959,13 @@ export default function AccountsPage() {
947
959
  <div className="flex items-start justify-between gap-3">
948
960
  <div className="flex min-w-0 items-start gap-2.5">
949
961
  <Avatar className="h-10 w-10 shrink-0 rounded-lg border border-slate-500/20">
962
+ {account.avatar_id ? (
963
+ <AvatarImage
964
+ src={`${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${account.avatar_id}`}
965
+ alt={account.name}
966
+ className="rounded-lg object-cover"
967
+ />
968
+ ) : null}
950
969
  <AvatarFallback className="rounded-lg bg-slate-500/8 text-sm font-semibold uppercase text-slate-700 dark:text-slate-200">
951
970
  {getAccountInitials(account.name)}
952
971
  </AvatarFallback>
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
3
4
  import { Button } from '@/components/ui/button';
4
5
  import { EntityPicker } from '@/components/ui/entity-picker';
5
6
  import {
@@ -36,8 +37,25 @@ import { z } from 'zod';
36
37
  type PersonOption = {
37
38
  id: number | string;
38
39
  name: string;
40
+ avatar_id?: number | null;
39
41
  };
40
42
 
43
+ function getPersonInitialsLocal(name: string) {
44
+ return name
45
+ .split(' ')
46
+ .filter(Boolean)
47
+ .slice(0, 2)
48
+ .map((part) => part[0]?.toUpperCase() || '')
49
+ .join('');
50
+ }
51
+
52
+ function getPersonAvatarUrlLocal(avatarId?: number | null) {
53
+ if (typeof avatarId === 'number' && avatarId > 0) {
54
+ return `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`;
55
+ }
56
+ return undefined;
57
+ }
58
+
41
59
  type CreatePersonValues = {
42
60
  name: string;
43
61
  type: 'individual' | 'company';
@@ -666,6 +684,25 @@ export function PersonFieldWithCreate<TFieldValues extends FieldValues>({
666
684
  }}
667
685
  getOptionValue={(person) => person.id}
668
686
  getOptionLabel={(person) => person.name}
687
+ renderOption={({ option }) => (
688
+ <div className="flex min-w-0 items-center gap-2">
689
+ <Avatar className="h-7 w-7 shrink-0">
690
+ <AvatarImage
691
+ src={getPersonAvatarUrlLocal(option.avatar_id)}
692
+ alt={option.name}
693
+ />
694
+ <AvatarFallback className="text-xs font-semibold uppercase">
695
+ {getPersonInitialsLocal(option.name) || '?'}
696
+ </AvatarFallback>
697
+ </Avatar>
698
+ <div className="min-w-0">
699
+ <div className="truncate text-sm">{option.name}</div>
700
+ <div className="text-xs text-muted-foreground">
701
+ #{option.id}
702
+ </div>
703
+ </div>
704
+ </div>
705
+ )}
669
706
  onChange={(value, option) => {
670
707
  void value;
671
708
  setSelectedPersonLabel(option?.name ?? '');
@@ -1985,7 +1985,7 @@ export function PersonFormSheet({
1985
1985
  <Sheet open={open} onOpenChange={handleSheetOpenChange}>
1986
1986
  <SheetContent
1987
1987
  side="right"
1988
- className="flex w-full flex-col sm:max-w-lg overflow-y-auto"
1988
+ className="flex w-full flex-col sm:max-w-xl md:max-w-2xl overflow-y-auto"
1989
1989
  >
1990
1990
  <SheetHeader className="shrink-0 border-b p-4">
1991
1991
  <div className="flex items-center gap-3">
@@ -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.309",
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",
14
- "@hed-hog/api-prisma": "0.0.6",
15
- "@hed-hog/api": "0.0.6",
16
- "@hed-hog/api-locale": "0.0.14",
12
+ "@hed-hog/core": "0.0.309",
17
13
  "@hed-hog/api-mail": "0.0.9",
18
- "@hed-hog/api-pagination": "0.0.7"
14
+ "@hed-hog/address": "0.0.309",
15
+ "@hed-hog/api": "0.0.6",
16
+ "@hed-hog/api-prisma": "0.0.6",
17
+ "@hed-hog/api-pagination": "0.0.7",
18
+ "@hed-hog/api-locale": "0.0.14"
19
19
  },
20
20
  "exports": {
21
21
  ".": {