@controleonline/ui-customers 1.0.81 → 1.0.83

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 (64) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/publish-npm.yml +91 -0
  3. package/AGENTS.md +12 -6
  4. package/docs/technical/Client-Details-Criar-Usuario.md +113 -0
  5. package/package.json +5 -4
  6. package/src/react/components/tabs/AddressesTab.js +240 -335
  7. package/src/react/components/tabs/DocumentAttachments.js +270 -0
  8. package/src/react/components/tabs/DocumentsTab.js +355 -463
  9. package/src/react/components/tabs/DocumentsTab.styles.js +123 -0
  10. package/src/react/components/tabs/EmployeeCreateFormFields.js +203 -0
  11. package/src/react/components/tabs/EmployeesTab.js +17 -171
  12. package/src/react/components/tabs/EmployeesTab.styles.js +220 -172
  13. package/src/react/components/tabs/GeneralTab.js +84 -46
  14. package/src/react/components/tabs/GeneralTab.styles.js +81 -77
  15. package/src/react/components/tabs/MediaTab.js +50 -5
  16. package/src/react/components/tabs/PeopleCategoriesPanel.js +161 -78
  17. package/src/react/components/tabs/SalesmanTab.js +1 -1
  18. package/src/react/components/tabs/UserFormModal.js +26 -2
  19. package/src/react/components/tabs/UsersTab.js +30 -19
  20. package/src/react/components/tabs/UsersTab.styles.js +257 -257
  21. package/src/react/components/tabs/UsersTabApiKeyModal.js +115 -0
  22. package/src/react/components/tabs/UsersTabUserModal.js +222 -0
  23. package/src/react/components/tabs/addressesTabHelpers.js +190 -0
  24. package/src/react/components/tabs/documentsTabHelpers.js +142 -0
  25. package/src/react/components/tabs/employeeContacts.js +3 -23
  26. package/src/react/components/tabs/employeeUnlink.js +7 -4
  27. package/src/react/components/tabs/employeesTabHelpers.js +33 -9
  28. package/src/react/components/tabs/generalTabHelpers.js +30 -8
  29. package/src/react/components/tabs/useGeneralTabPeopleLink.js +68 -0
  30. package/src/react/components/tabs/usersTabHelpers.js +24 -23
  31. package/src/react/pages/details.page.styles.js +114 -100
  32. package/src/react/pages/employees.js +4 -0
  33. package/src/react/utils/humanCompanyLinkTypes.js +28 -0
  34. package/src/tests/browser/manager/client-details-add-user-modal.spec.js +281 -0
  35. package/src/tests/browser/manager/client-details-address-create-coords.spec.js +414 -0
  36. package/src/tests/browser/manager/client-details-address-edit.spec.js +331 -0
  37. package/src/tests/browser/manager/client-details-contact-edit.spec.js +198 -0
  38. package/src/tests/browser/manager/client-details-create-contact.spec.js +306 -0
  39. package/src/tests/browser/manager/client-details-documents.spec.js +346 -0
  40. package/src/tests/browser/manager/client-details-identity-case.spec.js +227 -0
  41. package/src/tests/browser/manager/client-details-people-type.manifest.md +18 -0
  42. package/src/tests/browser/manager/client-details-people-type.spec.js +345 -0
  43. package/src/tests/browser/manager/client-details-supplier-product-create.spec.js +334 -0
  44. package/src/tests/browser/manager/client-details-users-tab-load.spec.js +194 -0
  45. package/src/tests/browser/manager/employees-salesman-after-sales.spec.js +441 -0
  46. package/src/tests/react/components/SalesmanTab.company.test.js +40 -0
  47. package/src/tests/react/components/tabs/UsersTab.styles.test.js +39 -0
  48. package/src/tests/react/components/tabs/addressesTabHelpers.test.js +78 -0
  49. package/src/tests/react/components/tabs/documentsTabContract.test.mjs +37 -0
  50. package/src/tests/react/components/tabs/documentsTabHelpers.test.mjs +93 -0
  51. package/src/tests/react/components/tabs/employeeContacts.test.js +46 -4
  52. package/src/tests/react/components/tabs/employeeLinkTypes.contract.test.mjs +52 -0
  53. package/src/tests/react/components/tabs/employeeUnlink.test.js +11 -11
  54. package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +2 -23
  55. package/src/tests/react/components/tabs/employeesTabHelpers.test.js +59 -0
  56. package/src/tests/react/components/tabs/generalTabHelpers.test.js +35 -0
  57. package/src/tests/react/components/tabs/peopleMediaTabs.test.js +217 -217
  58. package/src/tests/react/components/tabs/peopleType.contract.test.mjs +47 -0
  59. package/src/tests/react/components/tabs/resolveSeededLinkType.test.mjs +20 -0
  60. package/src/tests/react/components/tabs/salesmanTabMedia.no-redeclare.test.mjs +33 -0
  61. package/src/tests/react/components/tabs/usersTabHelpers.test.js +20 -0
  62. package/src/tests/react/components/tabs/usersTabHelpers.test.mjs +63 -0
  63. package/src/tests/react/pages/employees.test.js +2 -0
  64. package/src/tests/react/utils/humanCompanyLinkTypes.test.js +50 -0
@@ -1,463 +1,355 @@
1
- import { useStores } from '@store';
2
- import React, { useEffect, useState } from 'react';
3
-
4
- import {
5
- Modal,
6
- Text,
7
- TextInput,
8
- TouchableOpacity,
9
- ScrollView,
10
- View,
11
- Keyboard,
12
- } from 'react-native';
13
-
14
- import AnimatedModal from '@controleonline/ui-common/src/react/components/AnimatedModal';
15
- import FeatherIcon from 'react-native-vector-icons/Feather';
16
- import Icon from 'react-native-vector-icons/MaterialIcons';
17
- import {useMessage} from '@controleonline/ui-common/src/react/components/MessageService';
18
-
19
- import {
20
- inlineStyle_265_6,
21
- inlineStyle_266_12,
22
- inlineStyle_279_14,
23
- inlineStyle_288_16,
24
- inlineStyle_291_49,
25
- inlineStyle_299_10,
26
- inlineStyle_302_16,
27
- inlineStyle_303_18,
28
- inlineStyle_304_18,
29
- inlineStyle_308_18,
30
- inlineStyle_314_24,
31
- inlineStyle_324_16,
32
- inlineStyle_325_18,
33
- inlineStyle_327_14,
34
- inlineStyle_341_16,
35
- inlineStyle_343_14,
36
- inlineStyle_350_20,
37
- } from './DocumentsTab.styles';
38
-
39
- const DocumentsTab = ({ client, customStyles, isEditing, onUpdateClient }) => {
40
- const {showError, showSuccess, showDialog} = useMessage();
41
- const [documents, setDocuments] = useState([]);
42
- const [showModal, setShowModal] = useState(false);
43
- const [editingItem, setEditingItem] = useState(null);
44
- const [formData, setFormData] = useState({});
45
- const peopleStore = useStores(state => state.people);
46
- const peopleGetters = peopleStore.getters;
47
- const { currentCompany } = peopleGetters;
48
-
49
- const documentsStore = useStores(state => state.documents) || {};
50
- const actionsDocuments = documentsStore.actions || {};
51
- const documentsTypesStore = useStores(state => state.documentsTypes) || {};
52
- const actionsDocumentsType = documentsTypesStore.actions || {};
53
- const getters = documentsTypesStore.getters || {};
54
- const { items = [] } = getters;
55
-
56
- // Funções de máscara
57
- const applyMask = (value, type) => {
58
- if (!value) {
59
- return '';
60
- }
61
- const numbers = value.replace(/\D/g, '');
62
-
63
- const docTypeItem = items.find(item => item['@id'] === type);
64
- const docType = docTypeItem?.documentType?.toUpperCase();
65
-
66
- if (docType === 'CPF') {
67
- // Máscara CPF: 000.000.000-00
68
- return numbers
69
- .slice(0, 11)
70
- .replace(/(\d{3})(\d)/, '$1.$2')
71
- .replace(/(\d{3})(\d)/, '$1.$2')
72
- .replace(/(\d{3})(\d{1,2})$/, '$1-$2');
73
- }
74
-
75
- if (docType === 'CNPJ') {
76
- // Máscara CNPJ: 00.000.000/0000-00
77
- return numbers
78
- .slice(0, 14)
79
- .replace(/(\d{2})(\d)/, '$1.$2')
80
- .replace(/(\d{3})(\d)/, '$1.$2')
81
- .replace(/(\d{3})(\d)/, '$1/$2')
82
- .replace(/(\d{4})(\d{1,2})$/, '$1-$2');
83
- }
84
-
85
- if (docType === 'RG') {
86
- // Máscara RG: 00.000.000-0
87
- return numbers
88
- .slice(0, 9)
89
- .replace(/(\d{2})(\d)/, '$1.$2')
90
- .replace(/(\d{3})(\d)/, '$1.$2')
91
- .replace(/(\d{3})(\d{1})$/, '$1-$2');
92
- }
93
-
94
- if (docType === 'IE') {
95
- // Máscara IE: 000.000.000.000 (varia por estado, usando formato genérico)
96
- return numbers
97
- .slice(0, 12)
98
- .replace(/(\d{3})(\d)/, '$1.$2')
99
- .replace(/(\d{3})(\d)/, '$1.$2')
100
- .replace(/(\d{3})(\d)/, '$1.$2');
101
- }
102
-
103
- if (docType === 'IM') {
104
- // Máscara IM: 00.000.000-0 (varia por município, usando formato genérico)
105
- return numbers
106
- .slice(0, 9)
107
- .replace(/(\d{2})(\d)/, '$1.$2')
108
- .replace(/(\d{3})(\d)/, '$1.$2')
109
- .replace(/(\d{3})(\d{1})$/, '$1-$2');
110
- }
111
-
112
- return value;
113
- };
114
-
115
- const removeMask = value => {
116
- return value ? value.replace(/\D/g, '') : '';
117
- };
118
-
119
- useEffect(() => {
120
- if (!client) return;
121
- const rawDocuments = Array.isArray(client?.document)
122
- ? client.document.map(d => ({
123
- id: d.id || d['@id'],
124
- type:
125
- typeof d.documentType === 'object'
126
- ? d.documentType?.['@id'] || d.documentType?.id || 'Documento'
127
- : d.documentType || 'Documento',
128
- value: d.document,
129
- }))
130
- : [];
131
-
132
- // Always load the full document_types catalog so the type selector is available
133
- // for new companies/people (no company_document links yet). Filtering by peopleType
134
- // happens in getAvailableDocumentTypes.
135
- actionsDocumentsType.getItems({});
136
- setDocuments(rawDocuments);
137
- }, [client, currentCompany]);
138
-
139
- const openModal = (item = null) => {
140
- setEditingItem(item);
141
- setFormData(item || {});
142
- setShowModal(true);
143
- };
144
-
145
- const closeModal = () => {
146
- setShowModal(false);
147
- setEditingItem(null);
148
- setFormData({});
149
- };
150
-
151
- const normalizePeopleType = (value) => {
152
- const v = String(value || '').toLowerCase();
153
- if (v === 'f' || v === 'fisica' || v === 'pf' || v === 'pessoa_fisica') return 'F';
154
- if (v === 'j' || v === 'juridica' || v === 'pj' || v === 'pessoa_juridica') return 'J';
155
- return String(value || '');
156
- };
157
-
158
- const getAvailableDocumentTypes = () => {
159
- const clientPeopleType = normalizePeopleType(client?.peopleType);
160
- const availableTypes = items.filter(type => {
161
- const typePeople = normalizePeopleType(type.peopleType);
162
- // If type has no peopleType, keep it available (catalog may omit the field)
163
- if (!typePeople) return true;
164
- return typePeople === clientPeopleType;
165
- });
166
-
167
- // Se estiver editando, permite o tipo atual + tipos não utilizados
168
- if (editingItem) {
169
- return availableTypes.filter(type => {
170
- const isCurrentType = type['@id'] === editingItem.type;
171
- const isAlreadyUsed = documents.some(doc => doc.type === type['@id']);
172
- return isCurrentType || !isAlreadyUsed;
173
- });
174
- }
175
-
176
- // Se estiver adicionando, mostra apenas tipos não utilizados
177
- return availableTypes.filter(type => {
178
- return !documents.some(doc => doc.type === type['@id']);
179
- });
180
- };
181
-
182
- const getFilteredDocuments = () => {
183
- const isPessoaFisica =
184
- client?.peopleType === 'F' || client?.peopleType === 'fisica';
185
- const isPessoaJuridica =
186
- client?.peopleType === 'J' || client?.peopleType === 'juridica';
187
-
188
- return documents.filter(doc => {
189
- const docTypeItem = items.find(item => item['@id'] === doc.type);
190
- const docType = docTypeItem?.documentType?.toUpperCase();
191
-
192
- if (isPessoaFisica) {
193
- return docType === 'RG' || docType === 'CPF';
194
- }
195
-
196
- if (isPessoaJuridica) {
197
- return docType === 'CNPJ' || docType === 'IE' || docType === 'IM';
198
- }
199
-
200
- return true;
201
- });
202
- };
203
- const handleSave = async () => {
204
- if (!formData.value || !formData.type) {
205
- showError('Documento e tipo são obrigatórios.');
206
- return;
207
- }
208
-
209
- if (!editingItem) {
210
- const existingDoc = documents.find(doc => doc.type === formData.type);
211
- if (existingDoc) {
212
- const selectedType = items.find(item => item['@id'] === formData.type);
213
- showError(`Já existe um documento do tipo ${selectedType?.documentType}.`);
214
- return;
215
- }
216
- }
217
-
218
- try {
219
- // Remove a máscara antes de salvar
220
- const cleanValue = removeMask(formData.value);
221
-
222
- const documentData = {
223
- document: cleanValue,
224
- documentType: formData.type,
225
- people: client['@id'],
226
- };
227
-
228
- if (editingItem) {
229
- documentData.id = editingItem.id;
230
- }
231
-
232
- await actionsDocuments.save(documentData);
233
-
234
- const documentItem = {
235
- id: editingItem?.id || Date.now(),
236
- value: cleanValue,
237
- type: formData.type,
238
- };
239
-
240
- const updatedDocuments = editingItem
241
- ? documents.map(d => (d.id === editingItem.id ? documentItem : d))
242
- : [...documents, documentItem];
243
-
244
- setDocuments(updatedDocuments);
245
- if (onUpdateClient) {
246
- const fullDocumentData = updatedDocuments.map(d => ({
247
- id: d.id,
248
- '@id': d.id,
249
- document: d.value,
250
- documentType: d.type,
251
- }));
252
- onUpdateClient('document', fullDocumentData);
253
- }
254
-
255
- showSuccess(
256
- `Documento ${editingItem ? 'atualizado' : 'adicionado'} com sucesso!`,
257
- );
258
- closeModal();
259
- } catch (error) {
260
- showError(
261
- `Falha ao ${editingItem ? 'atualizar' : 'adicionar'} documento. Tente novamente.`,
262
- );
263
- }
264
- };
265
-
266
- const handleDelete = id => {
267
- showDialog({
268
- title: 'Confirmar exclusão',
269
- message: 'Deseja realmente remover este item?',
270
- confirmLabel: 'Remover',
271
- cancelLabel: 'Cancelar',
272
- onConfirm: async () => {
273
- try {
274
- await actionsDocuments.remove(id);
275
- const updatedDocuments = documents.filter(d => d.id !== id);
276
- setDocuments(updatedDocuments);
277
- if (onUpdateClient) {
278
- const fullDocumentData = updatedDocuments.map(d => ({
279
- id: d.id,
280
- '@id': d.id,
281
- document: d.value,
282
- documentType: d.type,
283
- }));
284
- onUpdateClient('document', fullDocumentData);
285
- }
286
- showSuccess('Documento removido com sucesso!');
287
- } catch (error) {
288
- console.log('Delete error:', error);
289
- showError('Falha ao remover documento. Tente novamente.');
290
- }
291
- },
292
- });
293
- };
294
-
295
- const renderModal = () => (
296
- <AnimatedModal
297
- visible={showModal}
298
- onRequestClose={closeModal}
299
- style={inlineStyle_265_6}>
300
- <View style={inlineStyle_266_12}>
301
- {/* Header */}
302
- <View style={inlineStyle_279_14}>
303
- <Text style={inlineStyle_288_16}>
304
- {editingItem ? 'Editar Documento' : 'Adicionar Documento'}
305
- </Text>
306
- <TouchableOpacity onPress={closeModal} style={inlineStyle_291_49}>
307
- <Icon name="close" size={20} color="#64748B" />
308
- </TouchableOpacity>
309
- </View>
310
-
311
- <ScrollView
312
- style={inlineStyle_299_10}
313
- keyboardShouldPersistTaps="handled"
314
- keyboardDismissMode="on-drag">
315
- <View style={inlineStyle_302_16}>
316
- <Text style={inlineStyle_303_18}>Tipo</Text>
317
- <View style={inlineStyle_304_18}>
318
- {getAvailableDocumentTypes().map(type => (
319
- <TouchableOpacity
320
- key={type.documentType}
321
- style={inlineStyle_308_18({
322
- formData: formData,
323
- type: type,
324
- })}
325
- onPress={() => setFormData({ ...formData, type: type['@id'] })}>
326
- <Text style={inlineStyle_314_24({
327
- formData: formData,
328
- type: type,
329
- })}>
330
- {type.documentType}
331
- </Text>
332
- </TouchableOpacity>
333
- ))}
334
- </View>
335
- </View>
336
-
337
- <View style={inlineStyle_324_16}>
338
- <Text style={inlineStyle_325_18}>Número do Documento</Text>
339
- <TextInput
340
- style={inlineStyle_327_14}
341
- placeholder="Número do documento"
342
- value={applyMask(formData.value || '', formData.type)}
343
- onChangeText={text => {
344
- const cleanText = removeMask(text);
345
- setFormData({ ...formData, value: cleanText });
346
- }}
347
- keyboardType="numeric"
348
- />
349
- </View>
350
-
351
- <View style={inlineStyle_341_16}>
352
- <TouchableOpacity
353
- style={inlineStyle_343_14}
354
- onPress={() => {
355
- Keyboard.dismiss();
356
- closeModal();
357
- }}>
358
- <Text style={inlineStyle_350_20}>Cancelar</Text>
359
- </TouchableOpacity>
360
- <TouchableOpacity
361
- style={customStyles.saveButton}
362
- onPress={() => {
363
- Keyboard.dismiss();
364
- handleSave();
365
- }}>
366
- <FeatherIcon
367
- name="save"
368
- size={16}
369
- color={customStyles.iconButtonPrimaryIcon.color}
370
- />
371
- <Text style={customStyles.saveButtonText}>Salvar</Text>
372
- </TouchableOpacity>
373
- </View>
374
- </ScrollView>
375
- </View>
376
- </AnimatedModal>
377
- );
378
- return (
379
- <>
380
- <View style={customStyles.tabContent}>
381
- <View style={customStyles.section}>
382
- <View style={customStyles.sectionHeader}>
383
- <Text style={customStyles.sectionTitle} numberOfLines={1}>
384
- Documentos
385
- </Text>
386
- {isEditing && (
387
- <TouchableOpacity
388
- onPress={() => openModal()}
389
- style={customStyles.iconButtonPrimary}>
390
- <Icon
391
- name="add"
392
- size={20}
393
- color={customStyles.iconButtonPrimaryIcon.color}
394
- />
395
- </TouchableOpacity>
396
- )}
397
- </View>
398
- {getFilteredDocuments().length === 0 ? (
399
- <Text style={customStyles.emptyText}>
400
- Nenhum documento cadastrado
401
- </Text>
402
- ) : (
403
- getFilteredDocuments().map(doc => (
404
- <View
405
- key={doc.id}
406
- style={[
407
- customStyles.listItem,
408
- customStyles.listItemWithActions,
409
- ]}>
410
- <View style={customStyles.itemContent}>
411
- <Icon
412
- name="description"
413
- size={20}
414
- color={customStyles.cardItemIcon.color}
415
- />
416
- <View>
417
- <Text style={customStyles.itemText}>
418
- {applyMask(String(doc.value || ''), doc.type)}
419
- </Text>
420
- <Text style={customStyles.itemSubtext}>
421
- {items.find(i => i['@id'] === doc.type)?.documentType ||
422
- String(doc.type || '')}
423
- </Text>
424
- </View>
425
- </View>
426
- {isEditing && (
427
- <View
428
- style={[
429
- customStyles.itemActions,
430
- customStyles.itemActionsPinned,
431
- ]}>
432
- <TouchableOpacity
433
- onPress={() => openModal(doc)}
434
- style={customStyles.iconButtonGhost}>
435
- <FeatherIcon
436
- name="edit-2"
437
- size={16}
438
- color={customStyles.iconButtonGhostIcon.color}
439
- />
440
- </TouchableOpacity>
441
- <TouchableOpacity
442
- onPress={() => handleDelete(doc.id)}
443
- style={customStyles.iconButtonGhost}>
444
- <FeatherIcon
445
- name="trash-2"
446
- size={16}
447
- color={customStyles.iconButtonGhostIcon.color}
448
- />
449
- </TouchableOpacity>
450
- </View>
451
- )}
452
- </View>
453
- ))
454
- )}
455
- </View>
456
- </View>
457
- {renderModal()}
458
- </>
459
- );
460
- };
461
-
462
- export default DocumentsTab;
463
-
1
+ import { useStores } from '@store';
2
+ import React, { useEffect, useState } from 'react';
3
+
4
+ import {
5
+ Modal,
6
+ Text,
7
+ TextInput,
8
+ TouchableOpacity,
9
+ ScrollView,
10
+ View,
11
+ Keyboard,
12
+ } from 'react-native';
13
+
14
+ import AnimatedModal from '@controleonline/ui-common/src/react/components/AnimatedModal';
15
+ import FeatherIcon from 'react-native-vector-icons/Feather';
16
+ import Icon from 'react-native-vector-icons/MaterialIcons';
17
+ import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
18
+
19
+ import DocumentAttachments from './DocumentAttachments';
20
+ import {
21
+ applyDocumentMask,
22
+ extractId,
23
+ normalizeDocumentFiles,
24
+ removeMask,
25
+ toDocumentItem,
26
+ } from './documentsTabHelpers';
27
+ import { resolveFileDownloadUrl } from '@controleonline/ui-common/src/react/utils/fileUrl';
28
+
29
+ import {
30
+ inlineStyle_265_6,
31
+ inlineStyle_266_12,
32
+ inlineStyle_279_14,
33
+ inlineStyle_288_16,
34
+ inlineStyle_291_49,
35
+ inlineStyle_299_10,
36
+ inlineStyle_302_16,
37
+ inlineStyle_303_18,
38
+ inlineStyle_304_18,
39
+ inlineStyle_308_18,
40
+ inlineStyle_314_24,
41
+ inlineStyle_324_16,
42
+ inlineStyle_325_18,
43
+ inlineStyle_327_14,
44
+ inlineStyle_341_16,
45
+ inlineStyle_343_14,
46
+ inlineStyle_350_20,
47
+ } from './DocumentsTab.styles';
48
+
49
+ const DocumentsTab = ({ client, customStyles, isEditing, onUpdateClient }) => {
50
+ const { showError, showSuccess, showDialog } = useMessage();
51
+ const [documents, setDocuments] = useState([]);
52
+ const [showModal, setShowModal] = useState(false);
53
+ const [editingItem, setEditingItem] = useState(null);
54
+ const [formData, setFormData] = useState({});
55
+ const peopleStore = useStores(state => state.people);
56
+ const peopleGetters = peopleStore.getters;
57
+ const { currentCompany } = peopleGetters;
58
+
59
+ const documentsStore = useStores(state => state.documents) || {};
60
+ const actionsDocuments = documentsStore.actions || {};
61
+ const documentsTypesStore = useStores(state => state.documentsTypes) || {};
62
+ const actionsDocumentsType = documentsTypesStore.actions || {};
63
+ const getters = documentsTypesStore.getters || {};
64
+ const { items = [] } = getters;
65
+
66
+ const resolveTypeLabel = typeIri =>
67
+ items.find(item => item['@id'] === typeIri)?.documentType || String(typeIri || '');
68
+
69
+ const applyMask = (value, type) =>
70
+ applyDocumentMask(value, resolveTypeLabel(type));
71
+
72
+ const syncClientDocuments = nextDocuments => {
73
+ if (typeof onUpdateClient !== 'function') {
74
+ return;
75
+ }
76
+ const fullDocumentData = nextDocuments.map(documentItem => ({
77
+ '@id': `/documents/${documentItem.id}`,
78
+ id: documentItem.id,
79
+ document: documentItem.value,
80
+ documentType: documentItem.type,
81
+ documentFiles: documentItem.files || [],
82
+ file: documentItem.files?.[0]?.file || null,
83
+ }));
84
+ onUpdateClient({ document: fullDocumentData });
85
+ };
86
+
87
+ const updateDocumentFiles = (documentId, nextFiles) => {
88
+ setDocuments(previousDocuments => {
89
+ const nextDocuments = previousDocuments.map(documentItem => {
90
+ if (String(documentItem.id) !== String(documentId)) {
91
+ return documentItem;
92
+ }
93
+ return { ...documentItem, files: nextFiles };
94
+ });
95
+ syncClientDocuments(nextDocuments);
96
+ return nextDocuments;
97
+ });
98
+ };
99
+
100
+ useEffect(() => {
101
+ const rawDocuments = Array.isArray(client?.document)
102
+ ? client.document
103
+ : Array.isArray(client?.documents)
104
+ ? client.documents
105
+ : [];
106
+ setDocuments(
107
+ rawDocuments.map(document => {
108
+ const item = toDocumentItem(document);
109
+ item.files = normalizeDocumentFiles(document, resolveFileDownloadUrl);
110
+ return item;
111
+ }),
112
+ );
113
+ }, [client?.id, client?.document, client?.documents]);
114
+
115
+ useEffect(() => {
116
+ if (actionsDocumentsType?.getItems) {
117
+ actionsDocumentsType.getItems({ itemsPerPage: 100 });
118
+ }
119
+ }, []);
120
+
121
+ const openModal = (item = null) => {
122
+ setEditingItem(item);
123
+ setFormData(
124
+ item
125
+ ? { type: item.type, value: applyMask(String(item.value || ''), item.type) }
126
+ : { type: '', value: '' },
127
+ );
128
+ setShowModal(true);
129
+ };
130
+
131
+ const closeModal = () => {
132
+ setShowModal(false);
133
+ setEditingItem(null);
134
+ setFormData({});
135
+ Keyboard.dismiss();
136
+ };
137
+
138
+ const handleSave = async () => {
139
+ if (!formData.type || !formData.value) {
140
+ showError(global.t?.t('customers', 'error', 'Preencha tipo e número do documento.'));
141
+ return;
142
+ }
143
+ const cleanValue = removeMask(formData.value);
144
+ if (!cleanValue) {
145
+ showError(global.t?.t('customers', 'error', 'Número do documento inválido.'));
146
+ return;
147
+ }
148
+
149
+ try {
150
+ const peopleIri = client?.['@id'] || `/people/${extractId(client?.id)}`;
151
+ let documentId = editingItem?.id;
152
+ const previousFiles = editingItem?.files || [];
153
+
154
+ if (editingItem?.id) {
155
+ await actionsDocuments.save({
156
+ id: editingItem.id,
157
+ document: cleanValue,
158
+ documentType: formData.type,
159
+ people: peopleIri,
160
+ });
161
+ } else {
162
+ const created = await actionsDocuments.save({
163
+ document: cleanValue,
164
+ documentType: formData.type,
165
+ people: peopleIri,
166
+ });
167
+ documentId = extractId(created?.id || created?.['@id'] || created);
168
+ }
169
+
170
+ const documentItem = {
171
+ id: documentId,
172
+ value: cleanValue,
173
+ type: formData.type,
174
+ files: previousFiles,
175
+ };
176
+
177
+ const updatedDocuments = editingItem
178
+ ? documents.map(documentValue =>
179
+ String(documentValue.id) === String(editingItem.id) ? documentItem : documentValue,
180
+ )
181
+ : [...documents, documentItem];
182
+
183
+ setDocuments(updatedDocuments);
184
+ syncClientDocuments(updatedDocuments);
185
+ showSuccess(
186
+ global.t?.t(
187
+ 'customers',
188
+ 'success',
189
+ `Documento ${editingItem ? 'atualizado' : 'adicionado'} com sucesso!`,
190
+ ),
191
+ );
192
+ closeModal();
193
+ } catch (error) {
194
+ showError(
195
+ global.t?.t(
196
+ 'customers',
197
+ 'error',
198
+ `Falha ao ${editingItem ? 'atualizar' : 'adicionar'} documento. Tente novamente.`,
199
+ ),
200
+ );
201
+ }
202
+ };
203
+
204
+ const handleDelete = id => {
205
+ showDialog({
206
+ title: global.t?.t('customers', 'label', 'Confirmar exclusão'),
207
+ message: global.t?.t('customers', 'label', 'Deseja realmente remover este item?'),
208
+ confirmLabel: global.t?.t('customers', 'label', 'Remover'),
209
+ cancelLabel: global.t?.t('customers', 'label', 'Cancelar'),
210
+ onConfirm: async () => {
211
+ try {
212
+ await actionsDocuments.remove(id);
213
+ const updatedDocuments = documents.filter(
214
+ documentItem => String(documentItem.id) !== String(id),
215
+ );
216
+ setDocuments(updatedDocuments);
217
+ syncClientDocuments(updatedDocuments);
218
+ showSuccess(global.t?.t('customers', 'success', 'Documento removido com sucesso!'));
219
+ } catch {
220
+ showError(global.t?.t('customers', 'error', 'Falha ao remover documento. Tente novamente.'));
221
+ }
222
+ },
223
+ });
224
+ };
225
+
226
+ const getFilteredDocuments = () => documents;
227
+
228
+ const renderModal = () => (
229
+ <Modal visible={showModal} transparent animationType="slide" onRequestClose={closeModal}>
230
+ <AnimatedModal style={inlineStyle_265_6}>
231
+ <View style={inlineStyle_266_12}>
232
+ <View style={inlineStyle_279_14}>
233
+ <Text style={inlineStyle_288_16}>
234
+ {editingItem
235
+ ? global.t?.t('customers', 'label', 'Editar documento')
236
+ : global.t?.t('customers', 'label', 'Novo documento')}
237
+ </Text>
238
+ <TouchableOpacity onPress={closeModal} style={inlineStyle_291_49}>
239
+ <Icon name="close" size={18} color="#64748B" />
240
+ </TouchableOpacity>
241
+ </View>
242
+ <ScrollView contentContainerStyle={inlineStyle_299_10}>
243
+ <View style={inlineStyle_302_16}>
244
+ <Text style={inlineStyle_303_18}>{global.t?.t('customers', 'label', 'Tipo')}</Text>
245
+ <View style={inlineStyle_304_18}>
246
+ {items.map(type => (
247
+ <TouchableOpacity
248
+ key={type['@id']}
249
+ style={inlineStyle_308_18({ formData, type })}
250
+ onPress={() =>
251
+ setFormData(previous => ({
252
+ ...previous,
253
+ type: type['@id'],
254
+ value: applyMask(removeMask(previous.value), type['@id']),
255
+ }))
256
+ }>
257
+ <Text style={inlineStyle_314_24({ formData, type })}>{type.documentType}</Text>
258
+ </TouchableOpacity>
259
+ ))}
260
+ </View>
261
+ </View>
262
+ <View style={inlineStyle_324_16}>
263
+ <Text style={inlineStyle_325_18}>{global.t?.t('customers', 'label', 'Número')}</Text>
264
+ <TextInput
265
+ style={inlineStyle_327_14}
266
+ value={formData.value || ''}
267
+ onChangeText={text =>
268
+ setFormData(previous => ({
269
+ ...previous,
270
+ value: applyMask(text, previous.type),
271
+ }))
272
+ }
273
+ keyboardType="numeric"
274
+ placeholder={global.t?.t('customers', 'label', 'Digite o número')}
275
+ />
276
+ </View>
277
+ <View style={inlineStyle_341_16}>
278
+ <TouchableOpacity style={inlineStyle_343_14} onPress={closeModal}>
279
+ <Text style={inlineStyle_350_20}>{global.t?.t('customers', 'label', 'Cancelar')}</Text>
280
+ </TouchableOpacity>
281
+ <TouchableOpacity
282
+ style={[inlineStyle_343_14, { backgroundColor: '#007bff', borderColor: '#007bff' }]}
283
+ onPress={handleSave}>
284
+ <Text style={[inlineStyle_350_20, { color: '#fff' }]}>
285
+ {global.t?.t('customers', 'label', 'Salvar')}
286
+ </Text>
287
+ </TouchableOpacity>
288
+ </View>
289
+ </ScrollView>
290
+ </View>
291
+ </AnimatedModal>
292
+ </Modal>
293
+ );
294
+
295
+ const companyId = extractId(currentCompany?.id || currentCompany?.['@id']);
296
+
297
+ return (
298
+ <>
299
+ <View style={customStyles.section}>
300
+ <View style={customStyles.card}>
301
+ <View style={customStyles.cardHeader}>
302
+ <Text style={customStyles.cardTitle}>
303
+ {global.t?.t('customers', 'label', 'Documentos')}
304
+ </Text>
305
+ {isEditing && (
306
+ <TouchableOpacity onPress={() => openModal()} style={customStyles.iconButtonPrimary}>
307
+ <Icon name="add" size={20} color={customStyles.iconButtonPrimaryIcon.color} />
308
+ </TouchableOpacity>
309
+ )}
310
+ </View>
311
+ {getFilteredDocuments().length === 0 ? (
312
+ <Text style={customStyles.emptyText}>Nenhum documento cadastrado</Text>
313
+ ) : (
314
+ getFilteredDocuments().map(doc => (
315
+ <View
316
+ key={doc.id}
317
+ style={[customStyles.listItem, customStyles.listItemWithActions, { flexDirection: 'column', alignItems: 'stretch' }]}>
318
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
319
+ <View style={customStyles.itemContent}>
320
+ <Icon name="description" size={20} color={customStyles.cardItemIcon.color} />
321
+ <View>
322
+ <Text style={customStyles.itemText}>
323
+ {applyMask(String(doc.value || ''), doc.type)}
324
+ </Text>
325
+ <Text style={customStyles.itemSubtext}>{resolveTypeLabel(doc.type)}</Text>
326
+ </View>
327
+ </View>
328
+ {isEditing && (
329
+ <View style={[customStyles.itemActions, customStyles.itemActionsPinned]}>
330
+ <TouchableOpacity onPress={() => openModal(doc)} style={customStyles.iconButtonGhost}>
331
+ <FeatherIcon name="edit-2" size={16} color={customStyles.iconButtonGhostIcon.color} />
332
+ </TouchableOpacity>
333
+ <TouchableOpacity onPress={() => handleDelete(doc.id)} style={customStyles.iconButtonGhost}>
334
+ <FeatherIcon name="trash-2" size={16} color={customStyles.iconButtonGhostIcon.color} />
335
+ </TouchableOpacity>
336
+ </View>
337
+ )}
338
+ </View>
339
+ <DocumentAttachments
340
+ documentItem={doc}
341
+ isEditing={isEditing}
342
+ companyId={companyId}
343
+ onFilesChanged={updateDocumentFiles}
344
+ />
345
+ </View>
346
+ ))
347
+ )}
348
+ </View>
349
+ </View>
350
+ {renderModal()}
351
+ </>
352
+ );
353
+ };
354
+
355
+ export default DocumentsTab;