@controleonline/ui-customers 1.0.81

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.
Files changed (102) hide show
  1. package/.github/agents/developer.agent.md +30 -0
  2. package/.github/agents/devops.agent.md +30 -0
  3. package/.github/agents/qa.agent.md +30 -0
  4. package/.github/agents/security.agent.md +30 -0
  5. package/.scrutinizer.yml +20 -0
  6. package/AGENTS.md +50 -0
  7. package/FUNDING.yml +1 -0
  8. package/README.md +23 -0
  9. package/docs/technical/Client-Details-Franquia-Filial.md +18 -0
  10. package/docs/technical/Client-Details-People-Name-Case.md +103 -0
  11. package/docs/technical/Cliente-Vendedor-Vinculo-e-Permissoes.md +152 -0
  12. package/docs/wiki.md +1 -0
  13. package/package.json +32 -0
  14. package/scripts/validate-wiki-pointer.cjs +13 -0
  15. package/src/react/components/tabs/AddressesTab.js +494 -0
  16. package/src/react/components/tabs/CategoriesTab.js +196 -0
  17. package/src/react/components/tabs/ClassificationChips.js +494 -0
  18. package/src/react/components/tabs/ContactTab.js +621 -0
  19. package/src/react/components/tabs/ContactTab.styles.js +119 -0
  20. package/src/react/components/tabs/ContractsTab.js +13 -0
  21. package/src/react/components/tabs/DocumentsTab.js +463 -0
  22. package/src/react/components/tabs/DocumentsTab.styles.js +133 -0
  23. package/src/react/components/tabs/EmployeeContactRow.js +83 -0
  24. package/src/react/components/tabs/EmployeesTab.js +454 -0
  25. package/src/react/components/tabs/EmployeesTab.styles.js +172 -0
  26. package/src/react/components/tabs/FiscalTab.js +6 -0
  27. package/src/react/components/tabs/FranchiseCommissionSection.js +307 -0
  28. package/src/react/components/tabs/FranchiseLinksManageModal.js +215 -0
  29. package/src/react/components/tabs/FranchiseLinksTab.js +282 -0
  30. package/src/react/components/tabs/GeneralTab.js +466 -0
  31. package/src/react/components/tabs/GeneralTab.styles.js +101 -0
  32. package/src/react/components/tabs/MediaTab.js +356 -0
  33. package/src/react/components/tabs/PeopleCategoriesPanel.js +601 -0
  34. package/src/react/components/tabs/ProductsTab.js +236 -0
  35. package/src/react/components/tabs/ProductsTab.styles.js +143 -0
  36. package/src/react/components/tabs/SalesmanCommissionBlock.js +120 -0
  37. package/src/react/components/tabs/SalesmanManageModal.js +208 -0
  38. package/src/react/components/tabs/SalesmanTab.js +436 -0
  39. package/src/react/components/tabs/SalesmanTab.styles.js +4 -0
  40. package/src/react/components/tabs/UserApiKeyModal.js +90 -0
  41. package/src/react/components/tabs/UserFormModal.js +301 -0
  42. package/src/react/components/tabs/UsersTab.js +426 -0
  43. package/src/react/components/tabs/UsersTab.styles.js +283 -0
  44. package/src/react/components/tabs/UsersTab.styles.test.js +42 -0
  45. package/src/react/components/tabs/contactMutationMessage.js +18 -0
  46. package/src/react/components/tabs/employeeContacts.js +214 -0
  47. package/src/react/components/tabs/employeeUnlink.js +68 -0
  48. package/src/react/components/tabs/employeesTabHelpers.js +227 -0
  49. package/src/react/components/tabs/franchiseCommissionHelpers.js +134 -0
  50. package/src/react/components/tabs/franchiseLinksTab.helpers.js +247 -0
  51. package/src/react/components/tabs/generalTabHelpers.js +120 -0
  52. package/src/react/components/tabs/salesmanTab.helpers.js +114 -0
  53. package/src/react/components/tabs/salesmanTabHelpers.js +173 -0
  54. package/src/react/components/tabs/salesmanTabMedia.js +55 -0
  55. package/src/react/components/tabs/salesmanTabSession.js +39 -0
  56. package/src/react/components/tabs/useEmployeeUnlink.js +58 -0
  57. package/src/react/components/tabs/useFranchiseLinksManage.js +215 -0
  58. package/src/react/components/tabs/useSalesmanManage.js +222 -0
  59. package/src/react/components/tabs/usersTabHelpers.js +221 -0
  60. package/src/react/domain/productSupplierFlow.js +37 -0
  61. package/src/react/index.js +4 -0
  62. package/src/react/pages/ClientDetailsSkeleton.js +84 -0
  63. package/src/react/pages/clientDetailsHelpers.js +17 -0
  64. package/src/react/pages/clients.js +27 -0
  65. package/src/react/pages/details.js +529 -0
  66. package/src/react/pages/details.page.styles.js +105 -0
  67. package/src/react/pages/details.styles.js +15 -0
  68. package/src/react/pages/employees.js +69 -0
  69. package/src/react/pages/franchisees.js +30 -0
  70. package/src/react/pages/prospects.js +21 -0
  71. package/src/react/pages/provider.js +21 -0
  72. package/src/react/router/routes.js +104 -0
  73. package/src/react/styles/details.js +571 -0
  74. package/src/store/people_categories/index.js +72 -0
  75. package/src/tests/browser/manager/client-details-create-user.spec.js +321 -0
  76. package/src/tests/browser/manager/client-details-employee-unlink.spec.js +215 -0
  77. package/src/tests/browser/manager/client-details-employees-refresh.spec.js +271 -0
  78. package/src/tests/browser/manager/client-details-enable.spec.js +241 -0
  79. package/src/tests/browser/manager/client-details-fiscal-tabs.spec.js +410 -0
  80. package/src/tests/browser/manager/client-details-franchise-commission.spec.js +318 -0
  81. package/src/tests/browser/manager/client-details-franchise-links.spec.js +365 -0
  82. package/src/tests/browser/manager/client-details-media.spec.js +405 -0
  83. package/src/tests/browser/manager/client-details-relationships.spec.js +313 -0
  84. package/src/tests/browser/manager/client-details-salesman-commission.spec.js +291 -0
  85. package/src/tests/browser/manager/client-details-salesman-manage.spec.js +448 -0
  86. package/src/tests/react/components/tabs/contactMutationMessage.test.js +25 -0
  87. package/src/tests/react/components/tabs/employeeContacts.test.js +266 -0
  88. package/src/tests/react/components/tabs/employeeUnlink.test.js +67 -0
  89. package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +78 -0
  90. package/src/tests/react/components/tabs/employeesTabHelpers.test.js +64 -0
  91. package/src/tests/react/components/tabs/fiscalTabContract.test.mjs +29 -0
  92. package/src/tests/react/components/tabs/franchiseCommissionHelpers.test.js +128 -0
  93. package/src/tests/react/components/tabs/franchiseLinksTab.helpers.test.mjs +166 -0
  94. package/src/tests/react/components/tabs/generalTabHelpers.test.js +130 -0
  95. package/src/tests/react/components/tabs/peopleMediaTabs.test.js +322 -0
  96. package/src/tests/react/components/tabs/salesmanTab.helpers.test.mjs +75 -0
  97. package/src/tests/react/components/tabs/salesmanTabHelpers.test.js +142 -0
  98. package/src/tests/react/components/tabs/salesmanTabHelpers.test.mjs +92 -0
  99. package/src/tests/react/components/tabs/usersTabHelpers.test.js +59 -0
  100. package/src/tests/react/domain/productSupplierFlow.test.mjs +55 -0
  101. package/src/tests/react/pages/clientDetailsHelpers.test.js +98 -0
  102. package/src/tests/react/pages/employees.test.js +44 -0
@@ -0,0 +1,621 @@
1
+ import React, { useMemo, useState, useEffect } from 'react';
2
+
3
+ import {
4
+ View,
5
+ Text,
6
+ TouchableOpacity,
7
+ TextInput,
8
+ } from 'react-native';
9
+
10
+ import AnimatedModal from '@controleonline/ui-common/src/react/components/AnimatedModal';
11
+ import FeatherIcon from 'react-native-vector-icons/Feather';
12
+ import Icon from 'react-native-vector-icons/MaterialIcons';
13
+ import { useStores } from '@store';
14
+ import {useMessage} from '@controleonline/ui-common/src/react/components/MessageService';
15
+ import {resolveContactMutationMessage} from './contactMutationMessage';
16
+
17
+ import {
18
+ inlineStyle_363_18,
19
+ inlineStyle_365_14,
20
+ inlineStyle_382_18,
21
+ inlineStyle_384_14,
22
+ inlineStyle_404_8,
23
+ inlineStyle_405_14,
24
+ inlineStyle_418_16,
25
+ inlineStyle_427_18,
26
+ inlineStyle_430_51,
27
+ inlineStyle_437_16,
28
+ inlineStyle_439_18,
29
+ inlineStyle_441_16,
30
+ inlineStyle_445_22,
31
+ inlineStyle_448_16,
32
+ inlineStyle_452_22,
33
+ } from './ContactTab.styles';
34
+
35
+ const STORE_ERROR_META_KEY = '__storeMeta';
36
+
37
+ const withHandledStoreError = payload => ({
38
+ ...(payload || {}),
39
+ [STORE_ERROR_META_KEY]: {
40
+ skipSystemError: true,
41
+ },
42
+ });
43
+
44
+ const ContactTab = ({ client, customStyles, isEditing, onUpdateClient }) => {
45
+ const {showError, showSuccess, showDialog} = useMessage();
46
+ const [phones, setPhones] = useState([]);
47
+ const [emails, setEmails] = useState([]);
48
+ const [showModal, setShowModal] = useState(false);
49
+ const [modalType, setModalType] = useState('');
50
+ const [editingItem, setEditingItem] = useState(null);
51
+ const [formData, setFormData] = useState({});
52
+
53
+ const phonesStore = useStores(state => state.phones) || {};
54
+ const actionsPhones = phonesStore.actions || {};
55
+ const emailsStore = useStores(state => state.emails) || {};
56
+ const actionsEmails = emailsStore.actions || {};
57
+ const extractId = value => String(value || '').replace(/\D/g, '');
58
+
59
+ const toPeopleIri = person => {
60
+ const directIri = String(person?.['@id'] || '').trim();
61
+ if (directIri.startsWith('/people/')) {
62
+ return directIri;
63
+ }
64
+
65
+ const nestedIri = String(person?.people?.['@id'] || person?.people || '').trim();
66
+ if (nestedIri.startsWith('/people/')) {
67
+ return nestedIri;
68
+ }
69
+
70
+ const rawId = extractId(person?.id || person?.people?.id || directIri || nestedIri);
71
+ return rawId ? `/people/${rawId}` : '';
72
+ };
73
+
74
+ const peopleIri = useMemo(() => toPeopleIri(client), [client]);
75
+
76
+ const extractPhoneDigits = value =>
77
+ String(value || '')
78
+ .replace(/\D/g, '')
79
+ .slice(0, 11);
80
+
81
+ const splitPhoneValue = value => {
82
+ const digits = extractPhoneDigits(value);
83
+ if (digits.length < 2) {
84
+ return { ddd: '', phone: '' };
85
+ }
86
+
87
+ return {
88
+ ddd: digits.slice(0, 2),
89
+ phone: digits.slice(2),
90
+ };
91
+ };
92
+
93
+ const formatPhoneValue = value => {
94
+ const digits = extractPhoneDigits(value);
95
+ if (!digits) {
96
+ return '';
97
+ }
98
+
99
+ if (digits.length <= 2) {
100
+ return `(${digits}`;
101
+ }
102
+
103
+ const ddd = digits.slice(0, 2);
104
+ const phoneNumber = digits.slice(2);
105
+
106
+ if (phoneNumber.length <= 4) {
107
+ return `(${ddd}) ${phoneNumber}`;
108
+ }
109
+
110
+ if (phoneNumber.length <= 8) {
111
+ return `(${ddd}) ${phoneNumber.slice(0, phoneNumber.length - 4)}-${phoneNumber.slice(-4)}`;
112
+ }
113
+
114
+ return `(${ddd}) ${phoneNumber.slice(0, 5)}-${phoneNumber.slice(5, 9)}`;
115
+ };
116
+
117
+ useEffect(() => {
118
+ const rawPhones = Array.isArray(client?.phone)
119
+ ? client.phone.map(p => ({
120
+ id: p.id || p['@id'],
121
+ value: formatPhoneValue(`${p.ddd || ''}${p.phone || ''}`),
122
+ }))
123
+ : [];
124
+ const rawEmails = Array.isArray(client?.email)
125
+ ? client.email.map(e => ({ id: e.id || e['@id'], value: e.email }))
126
+ : [];
127
+
128
+ setPhones(rawPhones);
129
+ setEmails(rawEmails);
130
+ }, [client]);
131
+
132
+ const validateEmail = email => {
133
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
134
+ return emailRegex.test(email);
135
+ };
136
+
137
+ const normalizeEmail = value => String(value || '').trim().toLowerCase();
138
+
139
+ const resolveItemId = value => String(extractId(value) || value || '');
140
+
141
+ const openModal = (type, item = null) => {
142
+ setModalType(type);
143
+ setEditingItem(item);
144
+
145
+ if (type === 'phone' && item) {
146
+ setFormData({
147
+ ...item,
148
+ phone: formatPhoneValue(item?.value || ''),
149
+ });
150
+ } else {
151
+ setFormData(item || {});
152
+ }
153
+
154
+ setShowModal(true);
155
+ };
156
+
157
+ const closeModal = () => {
158
+ setShowModal(false);
159
+ setModalType('');
160
+ setEditingItem(null);
161
+ setFormData({});
162
+ };
163
+
164
+ const handleSave = async () => {
165
+ if (modalType === 'phone') {
166
+ const phoneDigits = extractPhoneDigits(formData.phone);
167
+ if (phoneDigits.length !== 10 && phoneDigits.length !== 11) {
168
+ showError('Telefone com DDD é obrigatório.');
169
+ return;
170
+ }
171
+
172
+ const currentEditId = resolveItemId(editingItem?.id);
173
+ const hasDuplicatePhone = phones.some(item => {
174
+ const itemDigits = extractPhoneDigits(item?.value || '');
175
+ const itemId = resolveItemId(item?.id);
176
+
177
+ if (!itemDigits) {
178
+ return false;
179
+ }
180
+
181
+ if (currentEditId && itemId === currentEditId) {
182
+ return false;
183
+ }
184
+
185
+ return itemDigits === phoneDigits;
186
+ });
187
+
188
+ if (hasDuplicatePhone) {
189
+ showError('Telefone ja cadastrado para este cliente.');
190
+ return;
191
+ }
192
+
193
+ const { ddd, phone } = splitPhoneValue(phoneDigits);
194
+
195
+ try {
196
+ if (!actionsPhones?.save) {
197
+ showError('Servico de telefones indisponivel no momento.');
198
+ return;
199
+ }
200
+
201
+ if (!peopleIri) {
202
+ showError('Nao foi possivel identificar o cliente para salvar o telefone.');
203
+ return;
204
+ }
205
+
206
+ const phoneData = {
207
+ phone: parseInt(phone, 10),
208
+ ddi: 55,
209
+ ddd: parseInt(ddd, 10),
210
+ people: peopleIri,
211
+ };
212
+
213
+ if (editingItem) {
214
+ phoneData.id = editingItem.id;
215
+ }
216
+
217
+ const saved = await actionsPhones.save(
218
+ withHandledStoreError(phoneData),
219
+ );
220
+ const savedId = extractId(saved?.id || saved?.['@id'] || editingItem?.id);
221
+
222
+ const phoneItem = {
223
+ id: savedId || editingItem?.id || Date.now(),
224
+ value: formatPhoneValue(`${saved?.ddd || ddd}${saved?.phone || phone}`),
225
+ };
226
+
227
+ const updatedPhones = editingItem
228
+ ? phones.map(p => (p.id === editingItem.id ? phoneItem : p))
229
+ : [...phones, phoneItem];
230
+
231
+ setPhones(updatedPhones);
232
+
233
+ // Atualizar o cliente com a estrutura completa
234
+ if (onUpdateClient) {
235
+ const fullPhoneData = updatedPhones.map(p => {
236
+ const phoneParts = splitPhoneValue(p.value);
237
+ return {
238
+ id: p.id,
239
+ '@id': p.id,
240
+ ddd: phoneParts.ddd,
241
+ phone: phoneParts.phone,
242
+ ddi: 55,
243
+ };
244
+ });
245
+ onUpdateClient('phone', fullPhoneData);
246
+ }
247
+
248
+ showSuccess(
249
+ `Telefone ${editingItem ? 'atualizado' : 'adicionado'} com sucesso!`,
250
+ );
251
+ closeModal();
252
+ } catch (error) {
253
+ showError(
254
+ resolveContactMutationMessage(
255
+ error,
256
+ `Falha ao ${editingItem ? 'atualizar' : 'adicionar'} telefone. Tente novamente.`,
257
+ ),
258
+ );
259
+ }
260
+ } else if (modalType === 'email') {
261
+ const normalizedEmail = normalizeEmail(formData.value);
262
+ if (!normalizedEmail || !validateEmail(normalizedEmail)) {
263
+ showError('Email valido e obrigatorio.');
264
+ return;
265
+ }
266
+
267
+ const currentEditId = resolveItemId(editingItem?.id);
268
+ const hasDuplicateEmail = emails.some(item => {
269
+ const itemEmail = normalizeEmail(item?.value || '');
270
+ const itemId = resolveItemId(item?.id);
271
+
272
+ if (!itemEmail) {
273
+ return false;
274
+ }
275
+
276
+ if (currentEditId && itemId === currentEditId) {
277
+ return false;
278
+ }
279
+
280
+ return itemEmail === normalizedEmail;
281
+ });
282
+
283
+ if (hasDuplicateEmail) {
284
+ showError('E-mail ja cadastrado para este cliente.');
285
+ return;
286
+ }
287
+
288
+ try {
289
+ if (!actionsEmails?.save) {
290
+ showError('Servico de emails indisponivel no momento.');
291
+ return;
292
+ }
293
+
294
+ if (!peopleIri) {
295
+ showError('Nao foi possivel identificar o cliente para salvar o email.');
296
+ return;
297
+ }
298
+
299
+ const emailData = {
300
+ email: normalizedEmail,
301
+ people: peopleIri,
302
+ };
303
+
304
+ if (editingItem) {
305
+ emailData.id = editingItem.id;
306
+ }
307
+
308
+ const saved = await actionsEmails.save(
309
+ withHandledStoreError(emailData),
310
+ );
311
+ const savedId = extractId(saved?.id || saved?.['@id'] || editingItem?.id);
312
+
313
+ const emailItem = {
314
+ id: savedId || editingItem?.id || Date.now(),
315
+ value: String(saved?.email || emailData.email),
316
+ };
317
+
318
+ const updatedEmails = editingItem
319
+ ? emails.map(e => (e.id === editingItem.id ? emailItem : e))
320
+ : [...emails, emailItem];
321
+
322
+ setEmails(updatedEmails);
323
+
324
+ // Atualizar o cliente com a estrutura completa
325
+ if (onUpdateClient) {
326
+ const fullEmailData = updatedEmails.map(e => ({
327
+ id: e.id,
328
+ '@id': e.id,
329
+ email: e.value,
330
+ }));
331
+ onUpdateClient('email', fullEmailData);
332
+ }
333
+
334
+ showSuccess(
335
+ `Email ${editingItem ? 'atualizado' : 'adicionado'} com sucesso!`,
336
+ );
337
+ closeModal();
338
+ } catch (error) {
339
+ showError(
340
+ resolveContactMutationMessage(
341
+ error,
342
+ `Falha ao ${editingItem ? 'atualizar' : 'adicionar'} email. Tente novamente.`,
343
+ ),
344
+ );
345
+ }
346
+ }
347
+ };
348
+
349
+ const handleDelete = (type, id) => {
350
+ showDialog({
351
+ title: 'Confirmar exclusão',
352
+ message: 'Deseja realmente remover este item?',
353
+ confirmLabel: 'Remover',
354
+ cancelLabel: 'Cancelar',
355
+ onConfirm: async () => {
356
+ try {
357
+ if (type === 'phone') {
358
+ await actionsPhones.remove(
359
+ withHandledStoreError({id}),
360
+ );
361
+ const updatedPhones = phones.filter(p => p.id !== id);
362
+ setPhones(updatedPhones);
363
+ if (onUpdateClient) {
364
+ const fullPhoneData = updatedPhones.map(p => {
365
+ const phoneParts = splitPhoneValue(p.value);
366
+ return {
367
+ id: p.id,
368
+ '@id': p.id,
369
+ ddd: phoneParts.ddd,
370
+ phone: phoneParts.phone,
371
+ ddi: 55,
372
+ };
373
+ });
374
+ onUpdateClient('phone', fullPhoneData);
375
+ }
376
+ showSuccess('Telefone removido com sucesso!');
377
+ } else if (type === 'email') {
378
+ await actionsEmails.remove(
379
+ withHandledStoreError({id}),
380
+ );
381
+ const updatedEmails = emails.filter(e => e.id !== id);
382
+ setEmails(updatedEmails);
383
+ if (onUpdateClient) {
384
+ const fullEmailData = updatedEmails.map(e => ({
385
+ id: e.id,
386
+ '@id': e.id,
387
+ email: e.value,
388
+ }));
389
+ onUpdateClient('email', fullEmailData);
390
+ }
391
+ showSuccess('Email removido com sucesso!');
392
+ }
393
+ } catch (error) {
394
+ showError(
395
+ resolveContactMutationMessage(
396
+ error,
397
+ `Falha ao remover ${type === 'phone' ? 'telefone' : 'email'}. Tente novamente.`,
398
+ ),
399
+ );
400
+ }
401
+ },
402
+ });
403
+ };
404
+
405
+ const renderModal = () => {
406
+ const renderModalContent = () => {
407
+ if (modalType === 'phone') {
408
+ return (
409
+ <View>
410
+ <Text style={inlineStyle_363_18}>Telefone</Text>
411
+ <TextInput
412
+ style={inlineStyle_365_14}
413
+ placeholder="Ex: (11) 99999-9999"
414
+ value={formData.phone || ''}
415
+ onChangeText={text =>
416
+ setFormData({ ...formData, phone: formatPhoneValue(text) })
417
+ }
418
+ keyboardType="phone-pad"
419
+ maxLength={15}
420
+ />
421
+ </View>
422
+ );
423
+ } else if (modalType === 'email') {
424
+ return (
425
+ <View>
426
+ <Text style={inlineStyle_382_18}>Email</Text>
427
+ <TextInput
428
+ style={inlineStyle_384_14}
429
+ placeholder="email@exemplo.com"
430
+ value={formData.value || ''}
431
+ onChangeText={text => setFormData({ ...formData, value: text })}
432
+ keyboardType="email-address"
433
+ autoCapitalize="none"
434
+ />
435
+ </View>
436
+ );
437
+ }
438
+ return null;
439
+ };
440
+
441
+ return (
442
+ <AnimatedModal
443
+ visible={showModal}
444
+ onRequestClose={closeModal}
445
+ style={inlineStyle_404_8}>
446
+ <View style={inlineStyle_405_14}>
447
+ {/* Header */}
448
+ <View style={inlineStyle_418_16}>
449
+ <Text style={inlineStyle_427_18}>
450
+ {editingItem ? (modalType === 'phone' ? 'Editar Telefone' : 'Editar Email') : (modalType === 'phone' ? 'Adicionar Telefone' : 'Adicionar Email')}
451
+ </Text>
452
+ <TouchableOpacity onPress={closeModal} style={inlineStyle_430_51}>
453
+ <Icon name="close" size={20} color="#64748B" />
454
+ </TouchableOpacity>
455
+ </View>
456
+
457
+ <View style={inlineStyle_437_16}>
458
+ {renderModalContent()}
459
+ <View style={inlineStyle_439_18}>
460
+ <TouchableOpacity
461
+ style={inlineStyle_441_16}
462
+ onPress={closeModal}>
463
+ <Text style={inlineStyle_445_22}>Cancelar</Text>
464
+ </TouchableOpacity>
465
+ <TouchableOpacity
466
+ style={inlineStyle_448_16}
467
+ onPress={handleSave}>
468
+ <Text style={inlineStyle_452_22}>Salvar</Text>
469
+ </TouchableOpacity>
470
+ </View>
471
+ </View>
472
+ </View>
473
+ </AnimatedModal>
474
+ );
475
+
476
+ };
477
+ return (
478
+ <>
479
+ <View style={customStyles.tabContent}>
480
+ {/* Telefones */}
481
+ <View style={customStyles.section}>
482
+ <View style={customStyles.sectionHeader}>
483
+ <Text style={customStyles.sectionTitle}>Telefones</Text>
484
+ {isEditing && (
485
+ <TouchableOpacity
486
+ onPress={() => openModal('phone')}
487
+ style={customStyles.iconButtonPrimary}>
488
+ <Icon
489
+ name="add"
490
+ size={20}
491
+ color={customStyles.iconButtonPrimaryIcon.color}
492
+ />
493
+ </TouchableOpacity>
494
+ )}
495
+ </View>
496
+ {phones.length === 0 ? (
497
+ <Text style={customStyles.emptyText}>
498
+ Nenhum telefone cadastrado
499
+ </Text>
500
+ ) : (
501
+ phones.map(phone => (
502
+ <View
503
+ key={phone.id}
504
+ style={[
505
+ customStyles.listItem,
506
+ customStyles.listItemWithActions,
507
+ ]}>
508
+ <View style={customStyles.itemContent}>
509
+ <Icon
510
+ name="phone"
511
+ size={20}
512
+ color={customStyles.cardItemIcon.color}
513
+ />
514
+ <Text style={customStyles.itemText}>
515
+ {String(phone.value || '')}
516
+ </Text>
517
+ </View>
518
+ {isEditing && (
519
+ <View
520
+ style={[
521
+ customStyles.itemActions,
522
+ customStyles.itemActionsPinned,
523
+ ]}>
524
+ <TouchableOpacity
525
+ onPress={() => openModal('phone', phone)}
526
+ style={customStyles.iconButtonGhost}>
527
+ <FeatherIcon
528
+ name="edit-2"
529
+ size={16}
530
+ color={customStyles.iconButtonGhostIcon.color}
531
+ />
532
+ </TouchableOpacity>
533
+ <TouchableOpacity
534
+ onPress={() => handleDelete('phone', phone.id)}
535
+ style={customStyles.iconButtonGhost}>
536
+ <FeatherIcon
537
+ name="trash-2"
538
+ size={16}
539
+ color={customStyles.iconButtonGhostIcon.color}
540
+ />
541
+ </TouchableOpacity>
542
+ </View>
543
+ )}
544
+ </View>
545
+ ))
546
+ )}
547
+ </View>
548
+
549
+ {/* Emails */}
550
+ <View style={customStyles.section}>
551
+ <View style={customStyles.sectionHeader}>
552
+ <Text style={customStyles.sectionTitle}>Emails</Text>
553
+ {isEditing && (
554
+ <TouchableOpacity
555
+ onPress={() => openModal('email')}
556
+ style={customStyles.iconButtonPrimary}>
557
+ <Icon
558
+ name="add"
559
+ size={20}
560
+ color={customStyles.iconButtonPrimaryIcon.color}
561
+ />
562
+ </TouchableOpacity>
563
+ )}
564
+ </View>
565
+ {emails.length === 0 ? (
566
+ <Text style={customStyles.emptyText}>Nenhum email cadastrado</Text>
567
+ ) : (
568
+ emails.map(email => (
569
+ <View
570
+ key={email.id}
571
+ style={[
572
+ customStyles.listItem,
573
+ customStyles.listItemWithActions,
574
+ ]}>
575
+ <View style={customStyles.itemContent}>
576
+ <Icon
577
+ name="email"
578
+ size={20}
579
+ color={customStyles.cardItemIcon.color}
580
+ />
581
+ <Text style={customStyles.itemText}>
582
+ {String(email.value || '')}
583
+ </Text>
584
+ </View>
585
+ {isEditing && (
586
+ <View
587
+ style={[
588
+ customStyles.itemActions,
589
+ customStyles.itemActionsPinned,
590
+ ]}>
591
+ <TouchableOpacity
592
+ onPress={() => openModal('email', email)}
593
+ style={customStyles.iconButtonGhost}>
594
+ <FeatherIcon
595
+ name="edit-2"
596
+ size={16}
597
+ color={customStyles.iconButtonGhostIcon.color}
598
+ />
599
+ </TouchableOpacity>
600
+ <TouchableOpacity
601
+ onPress={() => handleDelete('email', email.id)}
602
+ style={customStyles.iconButtonGhost}>
603
+ <FeatherIcon
604
+ name="trash-2"
605
+ size={16}
606
+ color={customStyles.iconButtonGhostIcon.color}
607
+ />
608
+ </TouchableOpacity>
609
+ </View>
610
+ )}
611
+ </View>
612
+ ))
613
+ )}
614
+ </View>
615
+ </View>
616
+ {renderModal()}
617
+ </>
618
+ );
619
+ };
620
+
621
+ export default ContactTab;