@hed-hog/contact 0.0.186 → 0.0.190

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.
@@ -0,0 +1,1621 @@
1
+ 'use client';
2
+ import {
3
+ PageHeader,
4
+ PaginationFooter,
5
+ SearchBar,
6
+ StatsCards,
7
+ } from '@/components/entity-list';
8
+ import { Badge } from '@/components/ui/badge';
9
+ import { Button } from '@/components/ui/button';
10
+ import { Card, CardContent } from '@/components/ui/card';
11
+ import {
12
+ Dialog,
13
+ DialogContent,
14
+ DialogDescription,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ } from '@/components/ui/dialog';
18
+ import {
19
+ DropdownMenu,
20
+ DropdownMenuContent,
21
+ DropdownMenuItem,
22
+ DropdownMenuLabel,
23
+ DropdownMenuSeparator,
24
+ DropdownMenuTrigger,
25
+ } from '@/components/ui/dropdown-menu';
26
+ import {
27
+ Form,
28
+ FormControl,
29
+ FormField,
30
+ FormItem,
31
+ FormLabel,
32
+ FormMessage,
33
+ } from '@/components/ui/form';
34
+ import { Input } from '@/components/ui/input';
35
+ import {
36
+ Select,
37
+ SelectContent,
38
+ SelectItem,
39
+ SelectTrigger,
40
+ SelectValue,
41
+ } from '@/components/ui/select';
42
+ import {
43
+ Sheet,
44
+ SheetContent,
45
+ SheetDescription,
46
+ SheetHeader,
47
+ SheetTitle,
48
+ } from '@/components/ui/sheet';
49
+ import {
50
+ Table,
51
+ TableBody,
52
+ TableCell,
53
+ TableHead,
54
+ TableHeader,
55
+ TableRow,
56
+ } from '@/components/ui/table';
57
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
58
+ import { COUNTRIES } from '@/constants/countries';
59
+ import { formatDate } from '@/lib/format-date';
60
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
61
+ import { zodResolver } from '@hookform/resolvers/zod';
62
+ import {
63
+ Building2,
64
+ Copy,
65
+ Edit2,
66
+ FileText,
67
+ Loader2,
68
+ Mail,
69
+ MapPin,
70
+ MoreHorizontal,
71
+ Phone,
72
+ Trash2,
73
+ User,
74
+ UserPlus,
75
+ Users,
76
+ } from 'lucide-react';
77
+ import { useTranslations } from 'next-intl';
78
+ import { useState } from 'react';
79
+ import { useForm } from 'react-hook-form';
80
+ import { toast } from 'sonner';
81
+ import { z } from 'zod';
82
+
83
+ type PaginatedResult<T> = {
84
+ data: T[];
85
+ total: number;
86
+ page: number;
87
+ pageSize: number;
88
+ };
89
+
90
+ type Person = {
91
+ id: number;
92
+ name: string;
93
+ type: 'individual' | 'company';
94
+ status: 'active' | 'inactive';
95
+ created_at: string;
96
+ contact?: Array<{
97
+ id: number;
98
+ value: string;
99
+ is_primary: boolean;
100
+ contact_type_id: number;
101
+ contact_type?: {
102
+ id: number;
103
+ code: string;
104
+ };
105
+ }>;
106
+ address?: Array<{
107
+ id: number;
108
+ line1: string;
109
+ city: string;
110
+ state: string;
111
+ is_primary: boolean;
112
+ address_type_id: number;
113
+ postal_code: string;
114
+ country_code: string;
115
+ address_type?: {
116
+ id: number;
117
+ code: string;
118
+ };
119
+ }>;
120
+ document?: Array<{
121
+ id: number;
122
+ value: string;
123
+ document_type_id: number;
124
+ document_type?: {
125
+ id: number;
126
+ code: string;
127
+ };
128
+ }>;
129
+ };
130
+
131
+ export default function ContactPage() {
132
+ const t = useTranslations('contact.ContactPage');
133
+
134
+ const personSchema = z.object({
135
+ name: z.string().min(1, t('nameRequired')),
136
+ type: z.enum(['individual', 'company']),
137
+ status: z.enum(['active', 'inactive']).optional(),
138
+ });
139
+
140
+ const { request, currentLocaleCode, getSettingValue } = useApp();
141
+
142
+ const [searchQuery, setSearchQuery] = useState('');
143
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
144
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
145
+ const [editingPerson, setEditingPerson] = useState<Person | null>(null);
146
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
147
+ const [personToDelete, setPersonToDelete] = useState<Person | null>(null);
148
+
149
+ const [page, setPage] = useState(1);
150
+ const [pageSize, setPageSize] = useState(12);
151
+ const [typeFilter, setTypeFilter] = useState('all');
152
+ const [statusFilter, setStatusFilter] = useState('all');
153
+
154
+ const { data: contactTypes = [] } = useQuery({
155
+ queryKey: ['contact-types-all', currentLocaleCode],
156
+ queryFn: async () => {
157
+ const response = await request<{
158
+ data: Array<{
159
+ id: number;
160
+ code: string;
161
+ contact_type_id: number;
162
+ name: string;
163
+ }>;
164
+ }>({
165
+ url: '/person-contact-type?pageSize=100',
166
+ method: 'GET',
167
+ });
168
+ return response.data.data || [];
169
+ },
170
+ });
171
+
172
+ const { data: addressTypes = [] } = useQuery({
173
+ queryKey: ['address-types-all', currentLocaleCode],
174
+ queryFn: async () => {
175
+ const response = await request<{
176
+ data: Array<{
177
+ id: number;
178
+ address_type_id: number;
179
+ code: string;
180
+ name: string;
181
+ }>;
182
+ }>({
183
+ url: '/person-address-type?pageSize=100',
184
+ method: 'GET',
185
+ });
186
+ return response.data.data || [];
187
+ },
188
+ });
189
+
190
+ const { data: documentTypes = [] } = useQuery({
191
+ queryKey: ['document-types-all', currentLocaleCode],
192
+ queryFn: async () => {
193
+ const response = await request<{
194
+ data: Array<{
195
+ id: number;
196
+ document_type_id: number;
197
+ code: string;
198
+ name: string;
199
+ }>;
200
+ }>({
201
+ url: '/person-document-type?pageSize=100',
202
+ method: 'GET',
203
+ });
204
+ return response.data.data || [];
205
+ },
206
+ });
207
+
208
+ const {
209
+ data: stats = {
210
+ total: 0,
211
+ individual: 0,
212
+ company: 0,
213
+ active: 0,
214
+ inactive: 0,
215
+ },
216
+ refetch: refetchStats,
217
+ } = useQuery({
218
+ queryKey: ['person-stats', currentLocaleCode],
219
+ queryFn: async () => {
220
+ const response = await request<{
221
+ total: number;
222
+ individual: number;
223
+ company: number;
224
+ active: number;
225
+ inactive: number;
226
+ }>({
227
+ url: '/person/stats',
228
+ method: 'GET',
229
+ });
230
+ return response.data;
231
+ },
232
+ });
233
+
234
+ const {
235
+ data: paginate = { data: [], total: 0, page: 1, pageSize: 12 },
236
+ isLoading,
237
+ refetch,
238
+ } = useQuery<PaginatedResult<Person>>({
239
+ queryKey: [
240
+ 'persons',
241
+ page,
242
+ pageSize,
243
+ searchQuery,
244
+ typeFilter,
245
+ statusFilter,
246
+ currentLocaleCode,
247
+ ],
248
+ queryFn: async () => {
249
+ const params = new URLSearchParams();
250
+ params.set('page', String(page));
251
+ params.set('pageSize', String(pageSize));
252
+ if (searchQuery) params.set('search', searchQuery);
253
+ if (typeFilter && typeFilter !== 'all') params.set('type', typeFilter);
254
+ if (statusFilter && statusFilter !== 'all')
255
+ params.set('status', statusFilter);
256
+
257
+ const response = await request<PaginatedResult<Person>>({
258
+ url: `/person?${params.toString()}`,
259
+ method: 'GET',
260
+ });
261
+
262
+ return response.data;
263
+ },
264
+ });
265
+
266
+ const form = useForm<z.infer<typeof personSchema>>({
267
+ resolver: zodResolver(personSchema),
268
+ defaultValues: {
269
+ name: '',
270
+ type: 'individual',
271
+ status: 'active',
272
+ },
273
+ });
274
+
275
+ async function fetchViaCep(cep: string) {
276
+ try {
277
+ const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
278
+ if (!response.ok) return null;
279
+ const data = await response.json();
280
+ if (data.erro) return null;
281
+ return data;
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+
287
+ const editForm = useForm<z.infer<typeof personSchema>>({
288
+ resolver: zodResolver(personSchema),
289
+ });
290
+ const [contacts, setContacts] = useState<any[]>([]);
291
+ const [addresses, setAddresses] = useState<any[]>([]);
292
+ const [documents, setDocuments] = useState<any[]>([]);
293
+ const [loadingCEP, setLoadingCEP] = useState<{ [key: number]: boolean }>({});
294
+
295
+ const openEditSheet = (person: Person) => {
296
+ setEditingPerson(person);
297
+ editForm.reset({
298
+ name: person.name,
299
+ type: person.type,
300
+ status: person.status,
301
+ });
302
+ setContacts(person.contact ? [...person.contact] : []);
303
+ setAddresses(person.address ? [...person.address] : []);
304
+ setDocuments(person.document ? [...person.document] : []);
305
+ setIsSheetOpen(true);
306
+ };
307
+
308
+ const handleContactChange = (idx: number, field: string, value: any) => {
309
+ setContacts((prev) =>
310
+ prev.map((c, i) => (i === idx ? { ...c, [field]: value } : c))
311
+ );
312
+ };
313
+ const handleAddContact = () => {
314
+ setContacts((prev) => [
315
+ ...prev,
316
+ {
317
+ id: undefined,
318
+ value: '',
319
+ is_primary: false,
320
+ contact_type_id: contactTypes[0]?.contact_type_id || 1,
321
+ },
322
+ ]);
323
+ };
324
+ const handleRemoveContact = (idx: number) => {
325
+ setContacts((prev) => prev.filter((_, i) => i !== idx));
326
+ };
327
+
328
+ const handleAddressChange = (idx: number, field: string, value: any) => {
329
+ setAddresses((prev) =>
330
+ prev.map((a, i) => (i === idx ? { ...a, [field]: value } : a))
331
+ );
332
+ };
333
+ const handleAddAddress = () => {
334
+ setAddresses((prev) => [
335
+ ...prev,
336
+ {
337
+ id: undefined,
338
+ line1: '',
339
+ line2: '',
340
+ city: '',
341
+ state: '',
342
+ is_primary: false,
343
+ address_type_id: addressTypes[0]?.address_type_id || 1,
344
+ country_code: 'BRA',
345
+ postal_code: '',
346
+ },
347
+ ]);
348
+ };
349
+ const handleRemoveAddress = (idx: number) => {
350
+ setAddresses((prev) => prev.filter((_, i) => i !== idx));
351
+ };
352
+
353
+ const handleDocumentChange = (idx: number, field: string, value: any) => {
354
+ setDocuments((prev) =>
355
+ prev.map((d, i) => (i === idx ? { ...d, [field]: value } : d))
356
+ );
357
+ };
358
+ const handleAddDocument = () => {
359
+ setDocuments((prev) => [
360
+ ...prev,
361
+ {
362
+ id: undefined,
363
+ value: '',
364
+ document_type_id: documentTypes[0]?.document_type_id || 1,
365
+ },
366
+ ]);
367
+ };
368
+ const handleRemoveDocument = (idx: number) => {
369
+ setDocuments((prev) => prev.filter((_, i) => i !== idx));
370
+ };
371
+
372
+ const handleCEP = async (e: any, idx: number) => {
373
+ let v = e.target.value.replace(/\D/g, '');
374
+ if (v.length > 5) v = v.slice(0, 5) + '-' + v.slice(5, 8);
375
+ handleAddressChange(idx, 'postal_code', v);
376
+ const rawCep = v.replace(/\D/g, '');
377
+ if (rawCep.length === 8) {
378
+ setLoadingCEP((prev) => ({ ...prev, [idx]: true }));
379
+ const data = await fetchViaCep(rawCep);
380
+ if (data) {
381
+ handleAddressChange(idx, 'line1', data.logradouro || '');
382
+ handleAddressChange(idx, 'city', data.localidade || '');
383
+ handleAddressChange(idx, 'state', data.uf || '');
384
+ }
385
+ setLoadingCEP((prev) => ({ ...prev, [idx]: false }));
386
+ }
387
+ };
388
+
389
+ const onEdit = async (values: z.infer<typeof personSchema>) => {
390
+ if (!editingPerson) return;
391
+ try {
392
+ const addressesWithLine2 = addresses.map((a) => ({
393
+ ...a,
394
+ line2: a.line2 || '',
395
+ }));
396
+ await request({
397
+ url: `/person/${editingPerson.id}`,
398
+ method: 'PATCH',
399
+ data: {
400
+ name: values.name,
401
+ type: values.type,
402
+ status: values.status,
403
+ contacts: contacts,
404
+ addresses: addressesWithLine2,
405
+ documents: documents,
406
+ },
407
+ });
408
+ toast.success(t('updateSuccess'));
409
+ setIsSheetOpen(false);
410
+ setEditingPerson(null);
411
+ editForm.reset();
412
+ refetch();
413
+ } catch (error: any) {
414
+ toast.error(error?.message || t('updateError'));
415
+ }
416
+ };
417
+
418
+ const onSubmit = async (values: z.infer<typeof personSchema>) => {
419
+ try {
420
+ await request({
421
+ url: '/person',
422
+ method: 'POST',
423
+ data: values,
424
+ });
425
+ toast.success(t('createSuccess'));
426
+ setIsDialogOpen(false);
427
+ form.reset();
428
+ refetch();
429
+ refetchStats();
430
+ } catch (error: any) {
431
+ toast.error(error?.message || t('createError'));
432
+ }
433
+ };
434
+
435
+ const handleDelete = async () => {
436
+ if (!personToDelete) return;
437
+ try {
438
+ await request({
439
+ url: '/person',
440
+ method: 'DELETE',
441
+ data: { ids: [personToDelete.id] },
442
+ });
443
+ toast.success(t('deleteSuccess'));
444
+ setDeleteDialogOpen(false);
445
+ setPersonToDelete(null);
446
+ refetch();
447
+ refetchStats();
448
+ } catch (error: any) {
449
+ toast.error(error?.message || t('deleteError'));
450
+ }
451
+ };
452
+
453
+ const statsCards = [
454
+ {
455
+ title: t('statsTotal'),
456
+ value: stats.total,
457
+ icon: <Users className="h-4 w-4" />,
458
+ },
459
+ {
460
+ title: t('statsIndividual'),
461
+ value: stats.individual,
462
+ icon: <User className="h-4 w-4" />,
463
+ },
464
+ {
465
+ title: t('statsCompany'),
466
+ value: stats.company,
467
+ icon: <Building2 className="h-4 w-4" />,
468
+ },
469
+ {
470
+ title: t('statsActive'),
471
+ value: stats.active,
472
+ icon: <UserPlus className="h-4 w-4" />,
473
+ },
474
+ ];
475
+
476
+ const getPrimaryContact = (person: Person, typeCode: string) => {
477
+ if (!person.contact?.length) return '-';
478
+ const code = typeCode.toUpperCase();
479
+ const type = contactTypes.find((ct: any) => ct.code === code);
480
+ if (!type) return '-';
481
+ const contact = person.contact.find(
482
+ (c) => c.contact_type_id === type.contact_type_id && c.is_primary
483
+ );
484
+ if (!contact?.value) return '-';
485
+
486
+ if (code === 'PHONE') {
487
+ const digits = contact.value.replace(/\D/g, '');
488
+ if (digits.length === 11)
489
+ return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7)}`;
490
+ if (digits.length === 10)
491
+ return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6)}`;
492
+ }
493
+ return contact.value;
494
+ };
495
+
496
+ const getPrimaryAddress = (person: Person) => {
497
+ const address = person.address?.find((a) => a.is_primary);
498
+ return address ? `${address.city}, ${address.state}` : '-';
499
+ };
500
+
501
+ const getContactTypeName = (contactTypeId: number) => {
502
+ const contactType = contactTypes.find(
503
+ (ct: any) => ct.contact_type_id === contactTypeId
504
+ );
505
+ return contactType?.name || t('unknown');
506
+ };
507
+
508
+ const getAddressTypeName = (addressTypeId: number) => {
509
+ const addressType = addressTypes.find(
510
+ (at: any) => at.address_type_id === addressTypeId
511
+ );
512
+ return addressType?.name || t('unknown');
513
+ };
514
+
515
+ const getDocumentTypeName = (documentTypeId: number) => {
516
+ const documentType = documentTypes.find(
517
+ (dt: any) => dt.document_type_id === documentTypeId
518
+ );
519
+ return documentType?.name || t('unknown');
520
+ };
521
+
522
+ return (
523
+ <div className="flex flex-col h-screen px-4">
524
+ <PageHeader
525
+ breadcrumbs={[
526
+ { label: 'Home', href: '/' },
527
+ { label: t('description') },
528
+ ]}
529
+ actions={[
530
+ {
531
+ label: t('addNew'),
532
+ onClick: () => setIsDialogOpen(true),
533
+ variant: 'default',
534
+ },
535
+ ]}
536
+ title={t('title')}
537
+ description={t('description')}
538
+ />
539
+
540
+ <StatsCards stats={statsCards} />
541
+
542
+ <div className="mb-4 flex flex-col gap-4 md:flex-row mt-4">
543
+ <SearchBar
544
+ searchQuery={searchQuery}
545
+ onSearchChange={setSearchQuery}
546
+ onSearch={() => refetch()}
547
+ placeholder={t('searchPlaceholder')}
548
+ />
549
+ <Select value={typeFilter} onValueChange={setTypeFilter}>
550
+ <SelectTrigger className="w-full md:w-[200px]">
551
+ <SelectValue placeholder={t('filterByType')} />
552
+ </SelectTrigger>
553
+ <SelectContent>
554
+ <SelectItem value="all">{t('allTypes')}</SelectItem>
555
+ <SelectItem value="individual">{t('individual')}</SelectItem>
556
+ <SelectItem value="company">{t('company')}</SelectItem>
557
+ </SelectContent>
558
+ </Select>
559
+ <Select value={statusFilter} onValueChange={setStatusFilter}>
560
+ <SelectTrigger className="w-full md:w-[200px]">
561
+ <SelectValue placeholder={t('filterByStatus')} />
562
+ </SelectTrigger>
563
+ <SelectContent>
564
+ <SelectItem value="all">{t('allStatuses')}</SelectItem>
565
+ <SelectItem value="active">{t('active')}</SelectItem>
566
+ <SelectItem value="inactive">{t('inactive')}</SelectItem>
567
+ </SelectContent>
568
+ </Select>
569
+ </div>
570
+
571
+ <div className="rounded-md border mb-4">
572
+ <Table>
573
+ <TableHeader>
574
+ <TableRow>
575
+ <TableHead>{t('columnId')}</TableHead>
576
+ <TableHead>{t('columnName')}</TableHead>
577
+ <TableHead>{t('columnType')}</TableHead>
578
+ <TableHead>{t('columnStatus')}</TableHead>
579
+ <TableHead>{t('columnEmail')}</TableHead>
580
+ <TableHead>{t('columnPhone')}</TableHead>
581
+ <TableHead>{t('columnPlace')}</TableHead>
582
+ <TableHead className="text-right">{t('columnActions')}</TableHead>
583
+ </TableRow>
584
+ </TableHeader>
585
+ <TableBody>
586
+ {isLoading ? (
587
+ <TableRow>
588
+ <TableCell colSpan={7} className="text-center">
589
+ {t('loading')}
590
+ </TableCell>
591
+ </TableRow>
592
+ ) : paginate?.data?.length === 0 ? (
593
+ <TableRow>
594
+ <TableCell colSpan={7} className="text-center">
595
+ {t('emptyState')}
596
+ </TableCell>
597
+ </TableRow>
598
+ ) : (
599
+ paginate?.data?.map((person: Person) => (
600
+ <TableRow
601
+ key={person.id}
602
+ onDoubleClick={() => openEditSheet(person)}
603
+ className="cursor-pointer"
604
+ >
605
+ <TableCell className="font-medium">#{person.id}</TableCell>
606
+ <TableCell className="font-medium">
607
+ <div className="flex items-center gap-2">
608
+ {person.name || '-'}
609
+ {person.name && (
610
+ <Button
611
+ variant="ghost"
612
+ size="icon"
613
+ className="h-6 w-6"
614
+ onClick={() => {
615
+ navigator.clipboard.writeText(person.name);
616
+ toast.success(t('copiedToClipboard') || 'Copiado!');
617
+ }}
618
+ >
619
+ <Copy className="h-3 w-3" />
620
+ </Button>
621
+ )}
622
+ </div>
623
+ </TableCell>
624
+
625
+ <TableCell>
626
+ <Badge
627
+ variant={
628
+ person.type === 'individual' ? 'default' : 'secondary'
629
+ }
630
+ >
631
+ {person.type === 'individual' ? (
632
+ <>
633
+ <User className="mr-1 h-3 w-3" />
634
+ {t('individual')}
635
+ </>
636
+ ) : (
637
+ <>
638
+ <Building2 className="mr-1 h-3 w-3" />
639
+ {t('company')}
640
+ </>
641
+ )}
642
+ </Badge>
643
+ </TableCell>
644
+ <TableCell>
645
+ <Badge
646
+ variant={
647
+ person.status === 'active' ? 'default' : 'outline'
648
+ }
649
+ >
650
+ {person.status === 'active' ? t('active') : t('inactive')}
651
+ </Badge>
652
+ </TableCell>
653
+ <TableCell>
654
+ <div className="flex items-center gap-2">
655
+ <Mail className="h-4 w-4 text-muted-foreground" />
656
+ {getPrimaryContact(person, 'email')}
657
+ {getPrimaryContact(person, 'email') &&
658
+ getPrimaryContact(person, 'email') !== '-' && (
659
+ <Button
660
+ variant="ghost"
661
+ size="icon"
662
+ className="h-6 w-6"
663
+ onClick={() => {
664
+ navigator.clipboard.writeText(
665
+ getPrimaryContact(person, 'email')
666
+ );
667
+ toast.success(
668
+ t('copiedToClipboard') || 'Copiado!'
669
+ );
670
+ }}
671
+ >
672
+ <Copy className="h-3 w-3" />
673
+ </Button>
674
+ )}
675
+ </div>
676
+ </TableCell>
677
+ <TableCell>
678
+ <div className="flex items-center gap-2">
679
+ <Phone className="h-4 w-4 text-muted-foreground" />
680
+ {getPrimaryContact(person, 'phone')}
681
+ {getPrimaryContact(person, 'phone') &&
682
+ getPrimaryContact(person, 'phone') !== '-' && (
683
+ <Button
684
+ variant="ghost"
685
+ size="icon"
686
+ className="h-6 w-6"
687
+ onClick={() => {
688
+ navigator.clipboard.writeText(
689
+ getPrimaryContact(person, 'phone')
690
+ );
691
+ toast.success(
692
+ t('copiedToClipboard') || 'Copiado!'
693
+ );
694
+ }}
695
+ >
696
+ <Copy className="h-3 w-3" />
697
+ </Button>
698
+ )}
699
+ </div>
700
+ </TableCell>
701
+ <TableCell>
702
+ <div className="flex items-center gap-2">
703
+ <MapPin className="h-4 w-4 text-muted-foreground" />
704
+ {getPrimaryAddress(person)}
705
+ {getPrimaryAddress(person) &&
706
+ getPrimaryAddress(person) !== '-' && (
707
+ <Button
708
+ variant="ghost"
709
+ size="icon"
710
+ className="h-6 w-6"
711
+ onClick={() => {
712
+ navigator.clipboard.writeText(
713
+ getPrimaryAddress(person)
714
+ );
715
+ toast.success(
716
+ t('copiedToClipboard') || 'Copiado!'
717
+ );
718
+ }}
719
+ >
720
+ <Copy className="h-3 w-3" />
721
+ </Button>
722
+ )}
723
+ </div>
724
+ </TableCell>
725
+ <TableCell className="text-right">
726
+ <DropdownMenu>
727
+ <DropdownMenuTrigger asChild>
728
+ <Button variant="ghost" size="icon">
729
+ <MoreHorizontal className="h-4 w-4" />
730
+ </Button>
731
+ </DropdownMenuTrigger>
732
+ <DropdownMenuContent align="end">
733
+ <DropdownMenuLabel>{t('actions')}</DropdownMenuLabel>
734
+ <DropdownMenuSeparator />
735
+ <DropdownMenuItem onClick={() => openEditSheet(person)}>
736
+ <Edit2 className="mr-2 h-4 w-4" />
737
+ {t('buttonEditUser')}
738
+ </DropdownMenuItem>
739
+ <DropdownMenuItem
740
+ onClick={(e) => {
741
+ e.preventDefault();
742
+ e.stopPropagation();
743
+ setPersonToDelete(person);
744
+ setDeleteDialogOpen(true);
745
+ }}
746
+ className="text-destructive"
747
+ >
748
+ <Trash2 className="mr-2 h-4 w-4" />
749
+ {t('delete')}
750
+ </DropdownMenuItem>
751
+ {/* Dialog de confirmação de exclusão */}
752
+ <Dialog open={deleteDialogOpen}>
753
+ <DialogContent className="max-w-md">
754
+ <DialogHeader>
755
+ <DialogTitle>
756
+ {t('deleteConfirmTitle')}
757
+ </DialogTitle>
758
+ <DialogDescription>
759
+ {t('deleteConfirmDescription')}
760
+ </DialogDescription>
761
+ </DialogHeader>
762
+ <div className="flex justify-end gap-2 pt-4">
763
+ <Button
764
+ variant="outline"
765
+ onClick={() => {
766
+ setDeleteDialogOpen(false);
767
+ setPersonToDelete(null);
768
+ }}
769
+ >
770
+ {t('cancel')}
771
+ </Button>
772
+ <Button
773
+ variant="destructive"
774
+ onClick={handleDelete}
775
+ >
776
+ {t('delete')}
777
+ </Button>
778
+ </div>
779
+ </DialogContent>
780
+ </Dialog>
781
+ </DropdownMenuContent>
782
+ </DropdownMenu>
783
+ </TableCell>
784
+ </TableRow>
785
+ ))
786
+ )}
787
+ </TableBody>
788
+ </Table>
789
+ </div>
790
+
791
+ <PaginationFooter
792
+ currentPage={page}
793
+ pageSize={pageSize}
794
+ totalItems={paginate?.total || 0}
795
+ onPageChange={setPage}
796
+ onPageSizeChange={setPageSize}
797
+ pageSizeOptions={[10, 20, 30, 40, 50]}
798
+ />
799
+
800
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
801
+ <DialogContent className="max-w-md">
802
+ <DialogHeader>
803
+ <DialogTitle>{t('dialogCreateTitle')}</DialogTitle>
804
+ <DialogDescription>
805
+ {t('dialogCreateDescription')}
806
+ </DialogDescription>
807
+ </DialogHeader>
808
+ <Form {...form}>
809
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
810
+ <FormField
811
+ control={form.control}
812
+ name="type"
813
+ render={({ field }) => (
814
+ <FormItem>
815
+ <FormLabel>{t('typeOfPerson')}</FormLabel>
816
+ <Select
817
+ onValueChange={field.onChange}
818
+ defaultValue={field.value}
819
+ >
820
+ <FormControl>
821
+ <SelectTrigger className="w-full">
822
+ <SelectValue placeholder={t('selectType')} />
823
+ </SelectTrigger>
824
+ </FormControl>
825
+ <SelectContent>
826
+ <SelectItem value="individual">
827
+ <div className="flex items-center gap-2">
828
+ <User className="h-4 w-4" />
829
+ {t('individual')}
830
+ </div>
831
+ </SelectItem>
832
+ <SelectItem value="company">
833
+ <div className="flex items-center gap-2">
834
+ <Building2 className="h-4 w-4" />
835
+ {t('company')}
836
+ </div>
837
+ </SelectItem>
838
+ </SelectContent>
839
+ </Select>
840
+ <FormMessage />
841
+ </FormItem>
842
+ )}
843
+ />
844
+
845
+ <FormField
846
+ control={form.control}
847
+ name="name"
848
+ render={({ field }) => (
849
+ <FormItem>
850
+ <FormLabel>{t('name')}</FormLabel>
851
+ <FormControl>
852
+ <Input {...field} placeholder={t('namePlaceholder')} />
853
+ </FormControl>
854
+ <FormMessage />
855
+ </FormItem>
856
+ )}
857
+ />
858
+
859
+ <FormField
860
+ control={form.control}
861
+ name="status"
862
+ render={({ field }) => (
863
+ <FormItem>
864
+ <FormLabel>{t('status')}</FormLabel>
865
+ <Select
866
+ onValueChange={field.onChange}
867
+ defaultValue={field.value}
868
+ >
869
+ <FormControl>
870
+ <SelectTrigger className="w-full">
871
+ <SelectValue placeholder={t('selectStatus')} />
872
+ </SelectTrigger>
873
+ </FormControl>
874
+ <SelectContent>
875
+ <SelectItem value="active">{t('active')}</SelectItem>
876
+ <SelectItem value="inactive">
877
+ {t('inactive')}
878
+ </SelectItem>
879
+ </SelectContent>
880
+ </Select>
881
+ <FormMessage />
882
+ </FormItem>
883
+ )}
884
+ />
885
+
886
+ <div className="flex justify-end gap-2">
887
+ <Button
888
+ type="button"
889
+ variant="outline"
890
+ onClick={() => setIsDialogOpen(false)}
891
+ >
892
+ {t('cancel')}
893
+ </Button>
894
+ <Button type="submit">{t('createPerson')}</Button>
895
+ </div>
896
+ </form>
897
+ </Form>
898
+ </DialogContent>
899
+ </Dialog>
900
+
901
+ <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
902
+ <SheetContent className="sm:max-w-lg">
903
+ <SheetHeader>
904
+ <SheetTitle>{t('editPerson')}</SheetTitle>
905
+ <SheetDescription>{t('editPersonDescription')}</SheetDescription>
906
+ </SheetHeader>
907
+ <Form {...editForm}>
908
+ <form
909
+ onSubmit={editForm.handleSubmit(onEdit)}
910
+ className="mt-4 px-4 space-y-0"
911
+ >
912
+ <Tabs defaultValue="detalhes" className="w-full">
913
+ <TabsList className="w-full">
914
+ <TabsTrigger value="detalhes">
915
+ {t('editTabDetails')}
916
+ </TabsTrigger>
917
+ <TabsTrigger value="dados">
918
+ {t('editTabBaseData')}
919
+ </TabsTrigger>
920
+ <TabsTrigger value="contatos">
921
+ {t('editTabContacts')}
922
+ </TabsTrigger>
923
+ <TabsTrigger value="enderecos">
924
+ {t('editTabAddresses')}
925
+ </TabsTrigger>
926
+ <TabsTrigger value="documentos">
927
+ {t('editTabDocuments')}
928
+ </TabsTrigger>
929
+ </TabsList>
930
+ <TabsContent value="detalhes">
931
+ <Card className="mt-4">
932
+ <CardContent className="space-y-6 overflow-y-auto max-h-[62vh]">
933
+ <div>
934
+ <h3 className="mb-3 text-lg font-semibold">
935
+ {t('dialogBasicInformationTitle')}
936
+ </h3>
937
+ <div className="grid gap-3 rounded-lg border p-4">
938
+ <div className="flex items-center justify-between">
939
+ <span className="text-sm text-muted-foreground">
940
+ {t('type')}:
941
+ </span>
942
+ <Badge variant="outline">
943
+ {editingPerson?.type === 'individual' ? (
944
+ <>
945
+ <User className="mr-1 h-3 w-3" />
946
+ {t('individual')}
947
+ </>
948
+ ) : (
949
+ <>
950
+ <Building2 className="mr-1 h-3 w-3" />
951
+ {t('company')}
952
+ </>
953
+ )}
954
+ </Badge>
955
+ </div>
956
+ <div className="flex items-center justify-between">
957
+ <span className="text-sm text-muted-foreground">
958
+ {t('name')}:
959
+ </span>
960
+ <div className="flex items-center gap-2">
961
+ <span className="text-sm">
962
+ {editingPerson?.name || '-'}
963
+ </span>
964
+ {editingPerson?.name && (
965
+ <Button
966
+ variant="ghost"
967
+ size="icon"
968
+ className="h-6 w-6"
969
+ onClick={() => {
970
+ navigator.clipboard.writeText(
971
+ editingPerson.name
972
+ );
973
+ toast.success(
974
+ t('copiedToClipboard') || 'Copiado!'
975
+ );
976
+ }}
977
+ >
978
+ <Copy className="h-3 w-3" />
979
+ </Button>
980
+ )}
981
+ </div>
982
+ </div>
983
+ <div className="flex items-center justify-between">
984
+ <span className="text-sm text-muted-foreground">
985
+ {t('status')}:
986
+ </span>
987
+ <Badge variant="outline">
988
+ {editingPerson?.status === 'active'
989
+ ? t('active')
990
+ : t('inactive')}
991
+ </Badge>
992
+ </div>
993
+ <div className="flex items-center justify-between">
994
+ <span className="text-sm text-muted-foreground">
995
+ {t('createdAt')}:
996
+ </span>
997
+ <span className="text-sm">
998
+ {editingPerson?.created_at &&
999
+ formatDate(
1000
+ editingPerson.created_at,
1001
+ getSettingValue,
1002
+ currentLocaleCode
1003
+ )}
1004
+ </span>
1005
+ </div>
1006
+ </div>
1007
+ </div>
1008
+
1009
+ <div>
1010
+ <h3 className="mb-3 flex items-center gap-2 text-lg font-semibold">
1011
+ <Mail className="h-5 w-5" />
1012
+ {t('tabContacts')}
1013
+ </h3>
1014
+ {contacts && contacts.length > 0 ? (
1015
+ <div className="space-y-2 rounded-lg border p-4">
1016
+ {contacts.map((contact, idx) => (
1017
+ <div
1018
+ key={idx}
1019
+ className="flex items-center justify-between rounded-md border p-3"
1020
+ >
1021
+ <div className="flex items-center gap-3">
1022
+ <Badge variant="outline">
1023
+ {getContactTypeName(
1024
+ contact.contact_type_id
1025
+ )}
1026
+ </Badge>
1027
+ <span className="font-medium">
1028
+ {contact.value}
1029
+ </span>
1030
+ <Button
1031
+ variant="ghost"
1032
+ size="icon"
1033
+ className="h-6 w-6"
1034
+ onClick={() => {
1035
+ navigator.clipboard.writeText(
1036
+ contact.value
1037
+ );
1038
+ toast.success(
1039
+ t('copiedToClipboard') || 'Copiado!'
1040
+ );
1041
+ }}
1042
+ >
1043
+ <Copy className="h-3 w-3" />
1044
+ </Button>
1045
+ </div>
1046
+ {contact.is_primary && (
1047
+ <Badge variant="default">Principal</Badge>
1048
+ )}
1049
+ </div>
1050
+ ))}
1051
+ </div>
1052
+ ) : (
1053
+ <p className="rounded-lg border p-4 text-center text-sm text-muted-foreground">
1054
+ {t('noContacts')}
1055
+ </p>
1056
+ )}
1057
+ </div>
1058
+
1059
+ <div>
1060
+ <h3 className="mb-3 flex items-center gap-2 text-lg font-semibold">
1061
+ <MapPin className="h-5 w-5" />
1062
+ {t('tabAddresses')}
1063
+ </h3>
1064
+ {addresses && addresses.length > 0 ? (
1065
+ <div className="space-y-2 rounded-lg border p-4">
1066
+ {addresses.map((address, idx) => (
1067
+ <div
1068
+ key={idx}
1069
+ className="flex flex-col gap-2 rounded-md border p-3"
1070
+ >
1071
+ <div className="flex items-center justify-between">
1072
+ <div className="flex items-center gap-2">
1073
+ <Badge variant="outline">
1074
+ {getAddressTypeName(
1075
+ address.address_type_id
1076
+ )}
1077
+ </Badge>
1078
+ <Button
1079
+ variant="ghost"
1080
+ size="icon"
1081
+ className="h-6 w-6"
1082
+ onClick={() => {
1083
+ const fullAddress = `${address.line1}, ${address.city}, ${address.state}`;
1084
+ navigator.clipboard.writeText(
1085
+ fullAddress
1086
+ );
1087
+ toast.success(
1088
+ t('copiedToClipboard') || 'Copiado!'
1089
+ );
1090
+ }}
1091
+ >
1092
+ <Copy className="h-3 w-3" />
1093
+ </Button>
1094
+ </div>
1095
+ {address.is_primary && (
1096
+ <Badge variant="default">Principal</Badge>
1097
+ )}
1098
+ </div>
1099
+ <div className="text-sm">
1100
+ <p className="font-medium">{address.line1}</p>
1101
+ <p className="text-muted-foreground">
1102
+ {address.city}, {address.state}
1103
+ </p>
1104
+ </div>
1105
+ </div>
1106
+ ))}
1107
+ </div>
1108
+ ) : (
1109
+ <p className="rounded-lg border p-4 text-center text-sm text-muted-foreground">
1110
+ {t('noAddresses')}
1111
+ </p>
1112
+ )}
1113
+ </div>
1114
+
1115
+ <div>
1116
+ <h3 className="mb-3 flex items-center gap-2 text-lg font-semibold">
1117
+ <FileText className="h-5 w-5" />
1118
+ {t('tabDocuments')}
1119
+ </h3>
1120
+ {documents && documents.length > 0 ? (
1121
+ <div className="space-y-2 rounded-lg border p-4">
1122
+ {documents.map((document, idx) => (
1123
+ <div
1124
+ key={idx}
1125
+ className="flex items-center justify-between rounded-md border p-3"
1126
+ >
1127
+ <div className="flex items-center gap-3">
1128
+ <Badge variant="outline">
1129
+ {getDocumentTypeName(
1130
+ document.document_type_id
1131
+ )}
1132
+ </Badge>
1133
+ <span className="font-mono font-medium">
1134
+ {document.value}
1135
+ </span>
1136
+ </div>
1137
+ </div>
1138
+ ))}
1139
+ </div>
1140
+ ) : (
1141
+ <p className="rounded-lg border p-4 text-center text-sm text-muted-foreground">
1142
+ {t('noDocuments')}
1143
+ </p>
1144
+ )}
1145
+ </div>
1146
+ </CardContent>
1147
+ </Card>
1148
+ </TabsContent>
1149
+ <TabsContent value="dados">
1150
+ <Card className="mt-4">
1151
+ <CardContent className="space-y-4">
1152
+ <FormField
1153
+ control={editForm.control}
1154
+ name="type"
1155
+ render={({ field }) => (
1156
+ <FormItem>
1157
+ <FormLabel>{t('typeOfPerson')}</FormLabel>
1158
+ <Select
1159
+ onValueChange={field.onChange}
1160
+ defaultValue={field.value}
1161
+ >
1162
+ <FormControl>
1163
+ <SelectTrigger className="w-full">
1164
+ <SelectValue placeholder={t('selectType')} />
1165
+ </SelectTrigger>
1166
+ </FormControl>
1167
+ <SelectContent>
1168
+ <SelectItem value="individual">
1169
+ {t('individual')}
1170
+ </SelectItem>
1171
+ <SelectItem value="company">
1172
+ {t('company')}
1173
+ </SelectItem>
1174
+ </SelectContent>
1175
+ </Select>
1176
+ <FormMessage />
1177
+ </FormItem>
1178
+ )}
1179
+ />
1180
+ <FormField
1181
+ control={editForm.control}
1182
+ name="name"
1183
+ render={({ field }) => (
1184
+ <FormItem>
1185
+ <FormLabel>{t('name')}</FormLabel>
1186
+ <FormControl>
1187
+ <Input
1188
+ {...field}
1189
+ placeholder={t('namePlaceholder')}
1190
+ />
1191
+ </FormControl>
1192
+ <FormMessage />
1193
+ </FormItem>
1194
+ )}
1195
+ />
1196
+ <FormField
1197
+ control={editForm.control}
1198
+ name="status"
1199
+ render={({ field }) => (
1200
+ <FormItem>
1201
+ <FormLabel>{t('status')}</FormLabel>
1202
+ <Select
1203
+ onValueChange={field.onChange}
1204
+ defaultValue={field.value}
1205
+ >
1206
+ <FormControl>
1207
+ <SelectTrigger className="w-full">
1208
+ <SelectValue
1209
+ placeholder={t('selectStatus')}
1210
+ />
1211
+ </SelectTrigger>
1212
+ </FormControl>
1213
+ <SelectContent>
1214
+ <SelectItem value="active">
1215
+ {t('active')}
1216
+ </SelectItem>
1217
+ <SelectItem value="inactive">
1218
+ {t('inactive')}
1219
+ </SelectItem>
1220
+ </SelectContent>
1221
+ </Select>
1222
+ <FormMessage />
1223
+ </FormItem>
1224
+ )}
1225
+ />
1226
+ </CardContent>
1227
+ </Card>
1228
+ </TabsContent>
1229
+ <TabsContent value="contatos">
1230
+ <div className="space-y-2 pt-4 max-h-[70vh] overflow-y-auto">
1231
+ <div className="flex mb-6 items-center justify-between">
1232
+ <h4 className="font-semibold">{t('tabContacts')}</h4>
1233
+ <Button
1234
+ type="button"
1235
+ size="sm"
1236
+ onClick={handleAddContact}
1237
+ >
1238
+ {t('addContact')}
1239
+ </Button>
1240
+ </div>
1241
+ {contacts.length === 0 && (
1242
+ <p className="text-muted-foreground text-sm">
1243
+ {t('noContacts')}
1244
+ </p>
1245
+ )}
1246
+ {contacts.map((c, idx) => (
1247
+ <div
1248
+ key={idx}
1249
+ className="rounded-md border p-4 mb-4 bg-muted/40 flex flex-col gap-3"
1250
+ >
1251
+ <div>
1252
+ <label className="block text-xs font-medium mb-1">
1253
+ {t('contactType')}
1254
+ </label>
1255
+ <Select
1256
+ value={String(c.contact_type_id)}
1257
+ onValueChange={(v) =>
1258
+ handleContactChange(
1259
+ idx,
1260
+ 'contact_type_id',
1261
+ Number(v)
1262
+ )
1263
+ }
1264
+ >
1265
+ <SelectTrigger className="w-full">
1266
+ <SelectValue
1267
+ placeholder={t('selectContactType')}
1268
+ />
1269
+ </SelectTrigger>
1270
+ <SelectContent>
1271
+ {contactTypes.map((ct: any) => (
1272
+ <SelectItem
1273
+ key={ct.contact_type_id}
1274
+ value={String(ct.contact_type_id)}
1275
+ >
1276
+ {ct.name}
1277
+ </SelectItem>
1278
+ ))}
1279
+ </SelectContent>
1280
+ </Select>
1281
+ </div>
1282
+ <div>
1283
+ <label className="block text-xs font-medium mb-1">
1284
+ {t('contactValue')}
1285
+ </label>
1286
+ <Input
1287
+ className="w-full"
1288
+ value={c.value}
1289
+ onChange={(e) =>
1290
+ handleContactChange(idx, 'value', e.target.value)
1291
+ }
1292
+ />
1293
+ </div>
1294
+ <div className="flex items-center gap-3 justify-between mt-2">
1295
+ <label className="flex items-center gap-1 text-xs">
1296
+ <input
1297
+ type="checkbox"
1298
+ checked={!!c.is_primary}
1299
+ onChange={(e) =>
1300
+ handleContactChange(
1301
+ idx,
1302
+ 'is_primary',
1303
+ e.target.checked
1304
+ )
1305
+ }
1306
+ />{' '}
1307
+ {t('main')}
1308
+ </label>
1309
+ <Button
1310
+ type="button"
1311
+ size="icon"
1312
+ variant="ghost"
1313
+ onClick={() => handleRemoveContact(idx)}
1314
+ >
1315
+ <Trash2 className="w-4 h-4" />
1316
+ </Button>
1317
+ </div>
1318
+ </div>
1319
+ ))}
1320
+ </div>
1321
+ </TabsContent>
1322
+ <TabsContent value="enderecos">
1323
+ <div className="space-y-2 pt-4 max-h-[70vh] overflow-y-auto">
1324
+ <div className="flex mb-6 items-center justify-between">
1325
+ <h4 className="font-semibold">{t('editTabAddresses')}</h4>
1326
+ <Button
1327
+ type="button"
1328
+ size="sm"
1329
+ onClick={handleAddAddress}
1330
+ >
1331
+ {t('addAddress')}
1332
+ </Button>
1333
+ </div>
1334
+ {addresses.length === 0 && (
1335
+ <p className="text-muted-foreground text-sm">
1336
+ {t('noAddresses')}
1337
+ </p>
1338
+ )}
1339
+ {addresses.map((a, idx) => (
1340
+ <div
1341
+ key={idx}
1342
+ className="rounded-md border p-4 mb-4 bg-muted/40 flex flex-col gap-3"
1343
+ >
1344
+ <div>
1345
+ <label className="block text-xs font-medium mb-1">
1346
+ {t('addressType')}
1347
+ </label>
1348
+ <Select
1349
+ value={String(a.address_type_id)}
1350
+ onValueChange={(v) =>
1351
+ handleAddressChange(
1352
+ idx,
1353
+ 'address_type_id',
1354
+ Number(v)
1355
+ )
1356
+ }
1357
+ >
1358
+ <SelectTrigger className="w-full">
1359
+ <SelectValue
1360
+ placeholder={t('selectAddressType')}
1361
+ />
1362
+ </SelectTrigger>
1363
+ <SelectContent>
1364
+ {addressTypes.map((at: any) => (
1365
+ <SelectItem
1366
+ key={at.address_type_id}
1367
+ value={String(at.address_type_id)}
1368
+ >
1369
+ {at.name}
1370
+ </SelectItem>
1371
+ ))}
1372
+ </SelectContent>
1373
+ </Select>
1374
+ </div>
1375
+ <div>
1376
+ <label className="block text-xs font-medium mb-1">
1377
+ {t('zipCode')}
1378
+ </label>
1379
+ <Input
1380
+ className="w-full"
1381
+ value={a.postal_code || ''}
1382
+ maxLength={9}
1383
+ onChange={(e) => handleCEP(e, idx)}
1384
+ placeholder={t('zipCodePlaceholder')}
1385
+ />
1386
+ </div>
1387
+ <div>
1388
+ <label className="block text-xs font-medium mb-1">
1389
+ {t('address')}
1390
+ </label>
1391
+ <div className="relative">
1392
+ <Input
1393
+ className="w-full"
1394
+ value={a.line1}
1395
+ onChange={(e) =>
1396
+ handleAddressChange(
1397
+ idx,
1398
+ 'line1',
1399
+ e.target.value
1400
+ )
1401
+ }
1402
+ placeholder={t('addressPlaceholder')}
1403
+ disabled={loadingCEP[idx]}
1404
+ />
1405
+ {loadingCEP[idx] && (
1406
+ <Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-muted-foreground" />
1407
+ )}
1408
+ </div>
1409
+ </div>
1410
+ <div>
1411
+ <label className="block text-xs font-medium mb-1">
1412
+ {t('addressComplement')}
1413
+ </label>
1414
+ <Input
1415
+ className="w-full"
1416
+ value={a.line2}
1417
+ onChange={(e) =>
1418
+ handleAddressChange(idx, 'line2', e.target.value)
1419
+ }
1420
+ placeholder={t('addressComplementPlaceholder')}
1421
+ />
1422
+ </div>
1423
+ <div className="flex flex-col md:flex-row gap-3">
1424
+ <div className="flex-1">
1425
+ <label className="block text-xs font-medium mb-1">
1426
+ {t('addressCity')}
1427
+ </label>
1428
+ <div className="relative">
1429
+ <Input
1430
+ className="w-full"
1431
+ value={a.city}
1432
+ onChange={(e) =>
1433
+ handleAddressChange(
1434
+ idx,
1435
+ 'city',
1436
+ e.target.value
1437
+ )
1438
+ }
1439
+ placeholder={t('addressCityPlaceholder')}
1440
+ disabled={loadingCEP[idx]}
1441
+ />
1442
+ {loadingCEP[idx] && (
1443
+ <Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-muted-foreground" />
1444
+ )}
1445
+ </div>
1446
+ </div>
1447
+ <div className="w-full md:w-32">
1448
+ <label className="block text-xs font-medium mb-1">
1449
+ {t('addressState')}
1450
+ </label>
1451
+ <div className="relative">
1452
+ <Input
1453
+ className="w-full"
1454
+ value={a.state}
1455
+ onChange={(e) =>
1456
+ handleAddressChange(
1457
+ idx,
1458
+ 'state',
1459
+ e.target.value
1460
+ )
1461
+ }
1462
+ placeholder={t('addressStatePlaceholder')}
1463
+ disabled={loadingCEP[idx]}
1464
+ />
1465
+ {loadingCEP[idx] && (
1466
+ <Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-muted-foreground" />
1467
+ )}
1468
+ </div>
1469
+ </div>
1470
+ </div>
1471
+ <div className="w-full">
1472
+ <label className="block text-xs font-medium mb-1">
1473
+ {t('addressCountry')}
1474
+ </label>
1475
+ <Select
1476
+ value={a.country_code || ''}
1477
+ onValueChange={(v) =>
1478
+ handleAddressChange(idx, 'country_code', v)
1479
+ }
1480
+ >
1481
+ <SelectTrigger className="w-full">
1482
+ <SelectValue
1483
+ placeholder={t('addressCountryPlaceholder')}
1484
+ />
1485
+ </SelectTrigger>
1486
+ <SelectContent>
1487
+ {COUNTRIES.map((country) => (
1488
+ <SelectItem
1489
+ key={country.code}
1490
+ value={country.code}
1491
+ >
1492
+ {country.name}
1493
+ </SelectItem>
1494
+ ))}
1495
+ </SelectContent>
1496
+ </Select>
1497
+ </div>
1498
+ <div className="flex items-center gap-3 justify-between mt-2">
1499
+ <label className="flex items-center gap-1 text-xs">
1500
+ <input
1501
+ type="checkbox"
1502
+ checked={!!a.is_primary}
1503
+ onChange={(e) =>
1504
+ handleAddressChange(
1505
+ idx,
1506
+ 'is_primary',
1507
+ e.target.checked
1508
+ )
1509
+ }
1510
+ />{' '}
1511
+ {t('main')}
1512
+ </label>
1513
+ <Button
1514
+ type="button"
1515
+ size="icon"
1516
+ variant="ghost"
1517
+ onClick={() => handleRemoveAddress(idx)}
1518
+ >
1519
+ <Trash2 className="w-4 h-4" />
1520
+ </Button>
1521
+ </div>
1522
+ </div>
1523
+ ))}
1524
+ </div>
1525
+ </TabsContent>
1526
+ <TabsContent value="documentos">
1527
+ <div className="space-y-2 pt-4 max-h-[70vh] overflow-y-auto">
1528
+ <div className="flex mb-6 items-center justify-between">
1529
+ <h4 className="font-semibold">{t('editTabDocuments')}</h4>
1530
+ <Button
1531
+ type="button"
1532
+ size="sm"
1533
+ onClick={handleAddDocument}
1534
+ >
1535
+ {t('addDocument')}
1536
+ </Button>
1537
+ </div>
1538
+ {documents.length === 0 && (
1539
+ <p className="text-muted-foreground text-sm">
1540
+ {t('noDocuments')}
1541
+ </p>
1542
+ )}
1543
+ {documents.map((d, idx) => (
1544
+ <div
1545
+ key={idx}
1546
+ className="rounded-md border p-4 mb-4 bg-muted/40 flex flex-col gap-3"
1547
+ >
1548
+ <div>
1549
+ <label className="block text-xs font-medium mb-1">
1550
+ {t('documentType')}
1551
+ </label>
1552
+ <Select
1553
+ value={String(d.document_type_id)}
1554
+ onValueChange={(v) =>
1555
+ handleDocumentChange(
1556
+ idx,
1557
+ 'document_type_id',
1558
+ Number(v)
1559
+ )
1560
+ }
1561
+ >
1562
+ <SelectTrigger className="w-full">
1563
+ <SelectValue placeholder="Selecione o tipo" />
1564
+ </SelectTrigger>
1565
+ <SelectContent>
1566
+ {documentTypes.map((dt: any) => (
1567
+ <SelectItem
1568
+ key={dt.document_type_id}
1569
+ value={String(dt.document_type_id)}
1570
+ >
1571
+ {dt.name}
1572
+ </SelectItem>
1573
+ ))}
1574
+ </SelectContent>
1575
+ </Select>
1576
+ </div>
1577
+ <div>
1578
+ <label className="block text-xs font-medium mb-1">
1579
+ {t('documentValue')}
1580
+ </label>
1581
+ <Input
1582
+ className="w-full"
1583
+ value={d.value}
1584
+ onChange={(e) =>
1585
+ handleDocumentChange(idx, 'value', e.target.value)
1586
+ }
1587
+ placeholder={t('documentValuePlaceholder')}
1588
+ />
1589
+ </div>
1590
+ <div className="flex items-center justify-end mt-2">
1591
+ <Button
1592
+ type="button"
1593
+ size="icon"
1594
+ variant="ghost"
1595
+ onClick={() => handleRemoveDocument(idx)}
1596
+ >
1597
+ <Trash2 className="w-4 h-4" />
1598
+ </Button>
1599
+ </div>
1600
+ </div>
1601
+ ))}
1602
+ </div>
1603
+ </TabsContent>
1604
+ </Tabs>
1605
+ <div className="flex justify-end gap-2 pt-6">
1606
+ <Button
1607
+ type="button"
1608
+ variant="outline"
1609
+ onClick={() => setIsSheetOpen(false)}
1610
+ >
1611
+ {t('cancel')}
1612
+ </Button>
1613
+ <Button type="submit">{t('saveChanges')}</Button>
1614
+ </div>
1615
+ </form>
1616
+ </Form>
1617
+ </SheetContent>
1618
+ </Sheet>
1619
+ </div>
1620
+ );
1621
+ }