@hed-hog/contact 0.0.305 → 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.
- package/dist/person/dto/account.dto.d.ts +1 -0
- package/dist/person/dto/account.dto.d.ts.map +1 -1
- package/dist/person/dto/account.dto.js +5 -0
- package/dist/person/dto/account.dto.js.map +1 -1
- package/dist/person/person.controller.d.ts +3 -2
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.service.d.ts +1 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +6 -1
- package/dist/person/person.service.js.map +1 -1
- package/hedhog/frontend/app/_lib/crm-mocks.ts.ejs +1 -0
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +267 -2
- package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +2 -0
- package/hedhog/frontend/app/accounts/page.tsx.ejs +20 -1
- package/hedhog/frontend/app/person/_components/person-field-with-create.tsx.ejs +37 -0
- package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/person/_components/person-types.ts.ejs +1 -0
- package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +17 -7
- package/hedhog/frontend/app/pipeline/page.tsx.ejs +21 -2
- package/package.json +4 -4
- package/src/person/dto/account.dto.ts +16 -12
- package/src/person/person.service.ts +5 -0
|
@@ -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-
|
|
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">
|
|
@@ -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
|
-
<
|
|
313
|
-
{lead.
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
<
|
|
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-
|
|
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.
|
|
3
|
+
"version": "0.0.309",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"@nestjs/core": "^11",
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
|
-
"@hed-hog/
|
|
13
|
-
"@hed-hog/core": "0.0.305",
|
|
14
|
-
"@hed-hog/address": "0.0.305",
|
|
12
|
+
"@hed-hog/core": "0.0.309",
|
|
15
13
|
"@hed-hog/api-mail": "0.0.9",
|
|
14
|
+
"@hed-hog/address": "0.0.309",
|
|
16
15
|
"@hed-hog/api": "0.0.6",
|
|
16
|
+
"@hed-hog/api-prisma": "0.0.6",
|
|
17
17
|
"@hed-hog/api-pagination": "0.0.7",
|
|
18
18
|
"@hed-hog/api-locale": "0.0.14"
|
|
19
19
|
},
|