@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.
- package/hedhog/frontend/app/address-type/page.tsx.ejs +480 -0
- package/hedhog/frontend/app/contact-type/page.tsx.ejs +480 -0
- package/hedhog/frontend/app/document-type/page.tsx.ejs +616 -0
- package/hedhog/frontend/app/person/page.tsx.ejs +1621 -0
- package/hedhog/frontend/messages/en.json +242 -0
- package/hedhog/frontend/messages/pt.json +241 -0
- package/package.json +4 -4
|
@@ -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
|
+
}
|