@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.
- package/.github/workflows/integration-source-gate.yml +68 -0
- package/.github/workflows/publish-npm.yml +91 -0
- package/AGENTS.md +12 -6
- package/docs/technical/Client-Details-Criar-Usuario.md +113 -0
- package/package.json +5 -4
- package/src/react/components/tabs/AddressesTab.js +240 -335
- package/src/react/components/tabs/DocumentAttachments.js +270 -0
- package/src/react/components/tabs/DocumentsTab.js +355 -463
- package/src/react/components/tabs/DocumentsTab.styles.js +123 -0
- package/src/react/components/tabs/EmployeeCreateFormFields.js +203 -0
- package/src/react/components/tabs/EmployeesTab.js +17 -171
- package/src/react/components/tabs/EmployeesTab.styles.js +220 -172
- package/src/react/components/tabs/GeneralTab.js +84 -46
- package/src/react/components/tabs/GeneralTab.styles.js +81 -77
- package/src/react/components/tabs/MediaTab.js +50 -5
- package/src/react/components/tabs/PeopleCategoriesPanel.js +161 -78
- package/src/react/components/tabs/SalesmanTab.js +1 -1
- package/src/react/components/tabs/UserFormModal.js +26 -2
- package/src/react/components/tabs/UsersTab.js +30 -19
- package/src/react/components/tabs/UsersTab.styles.js +257 -257
- package/src/react/components/tabs/UsersTabApiKeyModal.js +115 -0
- package/src/react/components/tabs/UsersTabUserModal.js +222 -0
- package/src/react/components/tabs/addressesTabHelpers.js +190 -0
- package/src/react/components/tabs/documentsTabHelpers.js +142 -0
- package/src/react/components/tabs/employeeContacts.js +3 -23
- package/src/react/components/tabs/employeeUnlink.js +7 -4
- package/src/react/components/tabs/employeesTabHelpers.js +33 -9
- package/src/react/components/tabs/generalTabHelpers.js +30 -8
- package/src/react/components/tabs/useGeneralTabPeopleLink.js +68 -0
- package/src/react/components/tabs/usersTabHelpers.js +24 -23
- package/src/react/pages/details.page.styles.js +114 -100
- package/src/react/pages/employees.js +4 -0
- package/src/react/utils/humanCompanyLinkTypes.js +28 -0
- package/src/tests/browser/manager/client-details-add-user-modal.spec.js +281 -0
- package/src/tests/browser/manager/client-details-address-create-coords.spec.js +414 -0
- package/src/tests/browser/manager/client-details-address-edit.spec.js +331 -0
- package/src/tests/browser/manager/client-details-contact-edit.spec.js +198 -0
- package/src/tests/browser/manager/client-details-create-contact.spec.js +306 -0
- package/src/tests/browser/manager/client-details-documents.spec.js +346 -0
- package/src/tests/browser/manager/client-details-identity-case.spec.js +227 -0
- package/src/tests/browser/manager/client-details-people-type.manifest.md +18 -0
- package/src/tests/browser/manager/client-details-people-type.spec.js +345 -0
- package/src/tests/browser/manager/client-details-supplier-product-create.spec.js +334 -0
- package/src/tests/browser/manager/client-details-users-tab-load.spec.js +194 -0
- package/src/tests/browser/manager/employees-salesman-after-sales.spec.js +441 -0
- package/src/tests/react/components/SalesmanTab.company.test.js +40 -0
- package/src/tests/react/components/tabs/UsersTab.styles.test.js +39 -0
- package/src/tests/react/components/tabs/addressesTabHelpers.test.js +78 -0
- package/src/tests/react/components/tabs/documentsTabContract.test.mjs +37 -0
- package/src/tests/react/components/tabs/documentsTabHelpers.test.mjs +93 -0
- package/src/tests/react/components/tabs/employeeContacts.test.js +46 -4
- package/src/tests/react/components/tabs/employeeLinkTypes.contract.test.mjs +52 -0
- package/src/tests/react/components/tabs/employeeUnlink.test.js +11 -11
- package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +2 -23
- package/src/tests/react/components/tabs/employeesTabHelpers.test.js +59 -0
- package/src/tests/react/components/tabs/generalTabHelpers.test.js +35 -0
- package/src/tests/react/components/tabs/peopleMediaTabs.test.js +217 -217
- package/src/tests/react/components/tabs/peopleType.contract.test.mjs +47 -0
- package/src/tests/react/components/tabs/resolveSeededLinkType.test.mjs +20 -0
- package/src/tests/react/components/tabs/salesmanTabMedia.no-redeclare.test.mjs +33 -0
- package/src/tests/react/components/tabs/usersTabHelpers.test.js +20 -0
- package/src/tests/react/components/tabs/usersTabHelpers.test.mjs +63 -0
- package/src/tests/react/pages/employees.test.js +2 -0
- package/src/tests/react/utils/humanCompanyLinkTypes.test.js +50 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Text,
|
|
4
|
+
TouchableOpacity,
|
|
5
|
+
ScrollView,
|
|
6
|
+
TextInput,
|
|
7
|
+
View,
|
|
8
|
+
Keyboard,
|
|
9
|
+
} from 'react-native';
|
|
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 {
|
|
14
|
+
inlineStyle_174_6,
|
|
15
|
+
inlineStyle_175_12,
|
|
16
|
+
inlineStyle_188_14,
|
|
17
|
+
inlineStyle_197_16,
|
|
18
|
+
inlineStyle_200_49,
|
|
19
|
+
inlineStyle_208_10,
|
|
20
|
+
inlineStyle_213_20,
|
|
21
|
+
inlineStyle_214_22,
|
|
22
|
+
inlineStyle_216_18,
|
|
23
|
+
inlineStyle_224_20,
|
|
24
|
+
inlineStyle_225_22,
|
|
25
|
+
inlineStyle_226_22,
|
|
26
|
+
inlineStyle_228_20,
|
|
27
|
+
inlineStyle_239_20,
|
|
28
|
+
inlineStyle_248_20,
|
|
29
|
+
inlineStyle_249_22,
|
|
30
|
+
inlineStyle_250_22,
|
|
31
|
+
inlineStyle_252_20,
|
|
32
|
+
inlineStyle_263_20,
|
|
33
|
+
inlineStyle_275_20,
|
|
34
|
+
inlineStyle_276_22,
|
|
35
|
+
inlineStyle_278_18,
|
|
36
|
+
inlineStyle_288_20,
|
|
37
|
+
inlineStyle_289_22,
|
|
38
|
+
inlineStyle_290_22,
|
|
39
|
+
inlineStyle_292_20,
|
|
40
|
+
inlineStyle_303_20,
|
|
41
|
+
inlineStyle_312_20,
|
|
42
|
+
inlineStyle_313_22,
|
|
43
|
+
inlineStyle_314_22,
|
|
44
|
+
inlineStyle_316_20,
|
|
45
|
+
inlineStyle_327_20,
|
|
46
|
+
inlineStyle_339_16,
|
|
47
|
+
inlineStyle_341_14,
|
|
48
|
+
inlineStyle_348_20,
|
|
49
|
+
} from './UsersTab.styles';
|
|
50
|
+
|
|
51
|
+
const UsersTabUserModal = ({
|
|
52
|
+
visible,
|
|
53
|
+
onClose,
|
|
54
|
+
editingItem,
|
|
55
|
+
formData,
|
|
56
|
+
setFormData,
|
|
57
|
+
showPassword,
|
|
58
|
+
setShowPassword,
|
|
59
|
+
showConfirmPassword,
|
|
60
|
+
setShowConfirmPassword,
|
|
61
|
+
customStyles,
|
|
62
|
+
onSave,
|
|
63
|
+
}) => (
|
|
64
|
+
) => (
|
|
65
|
+
<AnimatedModal
|
|
66
|
+
visible={visible}
|
|
67
|
+
onRequestClose={onClose}
|
|
68
|
+
style={inlineStyle_174_6}>
|
|
69
|
+
<View style={inlineStyle_175_12}>
|
|
70
|
+
{/* Header */}
|
|
71
|
+
<View style={inlineStyle_188_14}>
|
|
72
|
+
<Text style={inlineStyle_197_16}>
|
|
73
|
+
{editingItem ? 'Editar Senha do Usuário' : 'Adicionar Usuário'}
|
|
74
|
+
</Text>
|
|
75
|
+
<TouchableOpacity onPress={onClose} style={inlineStyle_200_49}>
|
|
76
|
+
<Icon name="close" size={20} color="#64748B" />
|
|
77
|
+
</TouchableOpacity>
|
|
78
|
+
</View>
|
|
79
|
+
|
|
80
|
+
<ScrollView
|
|
81
|
+
style={inlineStyle_208_10}
|
|
82
|
+
keyboardShouldPersistTaps="handled"
|
|
83
|
+
keyboardDismissMode="on-drag">
|
|
84
|
+
{editingItem ? (
|
|
85
|
+
<View>
|
|
86
|
+
<View style={inlineStyle_213_20}>
|
|
87
|
+
<Text style={inlineStyle_214_22}>Usuário</Text>
|
|
88
|
+
<TextInput
|
|
89
|
+
style={inlineStyle_216_18}
|
|
90
|
+
value={formData.username}
|
|
91
|
+
editable={false}
|
|
92
|
+
/>
|
|
93
|
+
</View>
|
|
94
|
+
<View style={inlineStyle_224_20}>
|
|
95
|
+
<Text style={inlineStyle_225_22}>Nova senha</Text>
|
|
96
|
+
<View style={inlineStyle_226_22}>
|
|
97
|
+
<TextInput
|
|
98
|
+
style={inlineStyle_228_20}
|
|
99
|
+
placeholder="Nova senha"
|
|
100
|
+
value={formData.password}
|
|
101
|
+
onChangeText={text => setFormData({ ...formData, password: text })}
|
|
102
|
+
secureTextEntry={!showPassword}
|
|
103
|
+
/>
|
|
104
|
+
<TouchableOpacity
|
|
105
|
+
onPress={() => setShowPassword(prev => !prev)}
|
|
106
|
+
style={inlineStyle_239_20}>
|
|
107
|
+
<Icon
|
|
108
|
+
name={showPassword ? 'visibility-off' : 'visibility'}
|
|
109
|
+
size={20}
|
|
110
|
+
color="#6c757d"
|
|
111
|
+
/>
|
|
112
|
+
</TouchableOpacity>
|
|
113
|
+
</View>
|
|
114
|
+
</View>
|
|
115
|
+
<View style={inlineStyle_248_20}>
|
|
116
|
+
<Text style={inlineStyle_249_22}>Confirmar nova senha</Text>
|
|
117
|
+
<View style={inlineStyle_250_22}>
|
|
118
|
+
<TextInput
|
|
119
|
+
style={inlineStyle_252_20}
|
|
120
|
+
placeholder="Confirmar nova senha"
|
|
121
|
+
value={formData.confirmPassword}
|
|
122
|
+
onChangeText={text => setFormData({ ...formData, confirmPassword: text })}
|
|
123
|
+
secureTextEntry={!showConfirmPassword}
|
|
124
|
+
/>
|
|
125
|
+
<TouchableOpacity
|
|
126
|
+
onPress={() => setShowConfirmPassword(prev => !prev)}
|
|
127
|
+
style={inlineStyle_263_20}>
|
|
128
|
+
<Icon
|
|
129
|
+
name={showConfirmPassword ? 'visibility-off' : 'visibility'}
|
|
130
|
+
size={20}
|
|
131
|
+
color="#6c757d"
|
|
132
|
+
/>
|
|
133
|
+
</TouchableOpacity>
|
|
134
|
+
</View>
|
|
135
|
+
</View>
|
|
136
|
+
</View>
|
|
137
|
+
) : (
|
|
138
|
+
<View>
|
|
139
|
+
<View style={inlineStyle_275_20}>
|
|
140
|
+
<Text style={inlineStyle_276_22}>Usuário</Text>
|
|
141
|
+
<TextInput
|
|
142
|
+
style={inlineStyle_278_18}
|
|
143
|
+
placeholder="Nome de usuário"
|
|
144
|
+
value={formData.username}
|
|
145
|
+
onChangeText={text => setFormData({ ...formData, username: text })}
|
|
146
|
+
autoCapitalize="none"
|
|
147
|
+
/>
|
|
148
|
+
</View>
|
|
149
|
+
<View style={inlineStyle_288_20}>
|
|
150
|
+
<Text style={inlineStyle_289_22}>Senha</Text>
|
|
151
|
+
<View style={inlineStyle_290_22}>
|
|
152
|
+
<TextInput
|
|
153
|
+
style={inlineStyle_292_20}
|
|
154
|
+
placeholder="Senha"
|
|
155
|
+
value={formData.password}
|
|
156
|
+
onChangeText={text => setFormData({ ...formData, password: text })}
|
|
157
|
+
secureTextEntry={!showPassword}
|
|
158
|
+
/>
|
|
159
|
+
<TouchableOpacity
|
|
160
|
+
onPress={() => setShowPassword(prev => !prev)}
|
|
161
|
+
style={inlineStyle_303_20}>
|
|
162
|
+
<Icon
|
|
163
|
+
name={showPassword ? 'visibility-off' : 'visibility'}
|
|
164
|
+
size={20}
|
|
165
|
+
color="#6c757d"
|
|
166
|
+
/>
|
|
167
|
+
</TouchableOpacity>
|
|
168
|
+
</View>
|
|
169
|
+
</View>
|
|
170
|
+
<View style={inlineStyle_312_20}>
|
|
171
|
+
<Text style={inlineStyle_313_22}>Confirmar Senha</Text>
|
|
172
|
+
<View style={inlineStyle_314_22}>
|
|
173
|
+
<TextInput
|
|
174
|
+
style={inlineStyle_316_20}
|
|
175
|
+
placeholder="Confirmar senha"
|
|
176
|
+
value={formData.confirmPassword}
|
|
177
|
+
onChangeText={text => setFormData({ ...formData, confirmPassword: text })}
|
|
178
|
+
secureTextEntry={!showConfirmPassword}
|
|
179
|
+
/>
|
|
180
|
+
<TouchableOpacity
|
|
181
|
+
onPress={() => setShowConfirmPassword(prev => !prev)}
|
|
182
|
+
style={inlineStyle_327_20}>
|
|
183
|
+
<Icon
|
|
184
|
+
name={showConfirmPassword ? 'visibility-off' : 'visibility'}
|
|
185
|
+
size={20}
|
|
186
|
+
color="#6c757d"
|
|
187
|
+
/>
|
|
188
|
+
</TouchableOpacity>
|
|
189
|
+
</View>
|
|
190
|
+
</View>
|
|
191
|
+
</View>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
<View style={inlineStyle_339_16}>
|
|
195
|
+
<TouchableOpacity
|
|
196
|
+
style={inlineStyle_341_14}
|
|
197
|
+
onPress={() => {
|
|
198
|
+
Keyboard.dismiss();
|
|
199
|
+
onClose();
|
|
200
|
+
}}>
|
|
201
|
+
<Text style={inlineStyle_348_20}>Cancelar</Text>
|
|
202
|
+
</TouchableOpacity>
|
|
203
|
+
<TouchableOpacity
|
|
204
|
+
style={customStyles.saveButton}
|
|
205
|
+
onPress={() => {
|
|
206
|
+
Keyboard.dismiss();
|
|
207
|
+
onSave();
|
|
208
|
+
}}>
|
|
209
|
+
<FeatherIcon
|
|
210
|
+
name="save"
|
|
211
|
+
size={16}
|
|
212
|
+
color={customStyles.iconButtonPrimaryIcon.color}
|
|
213
|
+
/>
|
|
214
|
+
<Text style={customStyles.saveButtonText}>Salvar</Text>
|
|
215
|
+
</TouchableOpacity>
|
|
216
|
+
</View>
|
|
217
|
+
</ScrollView>
|
|
218
|
+
</View>
|
|
219
|
+
</AnimatedModal>
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
export default UsersTabUserModal;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for AddressesTab — keep the tab under the 500-line limit
|
|
3
|
+
* and make address id / IRI handling testable (app-community#282).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const extractId = value => String(value || '').replace(/\D/g, '');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize an address id to digits only.
|
|
10
|
+
* Never invents a timestamp id for missing values (that caused PUT 404
|
|
11
|
+
* and silent create paths when editing existing franchise addresses).
|
|
12
|
+
*/
|
|
13
|
+
export const normalizeId = value => {
|
|
14
|
+
const digits = extractId(value);
|
|
15
|
+
if (digits) return digits;
|
|
16
|
+
const raw = String(value || '').trim();
|
|
17
|
+
return raw || '';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const toPeopleIri = person => {
|
|
21
|
+
const rawIri = String(person?.['@id'] || '').trim();
|
|
22
|
+
if (rawIri.startsWith('/people/')) {
|
|
23
|
+
return rawIri;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const nestedIri = String(person?.people?.['@id'] || person?.people || '').trim();
|
|
27
|
+
if (nestedIri.startsWith('/people/')) {
|
|
28
|
+
return nestedIri;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const id = extractId(person?.id || person?.people?.id || rawIri || nestedIri);
|
|
32
|
+
return id ? `/people/${id}` : '';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const normalizeString = value => {
|
|
36
|
+
const text = String(value || '').trim();
|
|
37
|
+
return text.length > 0 ? text : undefined;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const normalizeZipCode = value =>
|
|
41
|
+
String(value || '')
|
|
42
|
+
.replace(/\D/g, '')
|
|
43
|
+
.slice(0, 8);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Flatten API/people address shapes into a stable client model.
|
|
47
|
+
* Always preserves a numeric `id` and an `/addresses/{id}` `@id` when known.
|
|
48
|
+
*/
|
|
49
|
+
export const normalizeAddress = address => {
|
|
50
|
+
if (!address || typeof address !== 'object') {
|
|
51
|
+
return {
|
|
52
|
+
id: '',
|
|
53
|
+
'@id': '',
|
|
54
|
+
street: '',
|
|
55
|
+
number: '',
|
|
56
|
+
city: '',
|
|
57
|
+
state: '',
|
|
58
|
+
zipCode: '',
|
|
59
|
+
complement: '',
|
|
60
|
+
district: '',
|
|
61
|
+
country: '',
|
|
62
|
+
nickname: '',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rawId = address?.id ?? address?.['@id'];
|
|
67
|
+
const id = normalizeId(rawId);
|
|
68
|
+
const iri =
|
|
69
|
+
String(address?.['@id'] || '').startsWith('/addresses/')
|
|
70
|
+
? String(address['@id'])
|
|
71
|
+
: id
|
|
72
|
+
? `/addresses/${id}`
|
|
73
|
+
: '';
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
id,
|
|
77
|
+
'@id': iri,
|
|
78
|
+
street: address?.street?.street || address?.street || '',
|
|
79
|
+
number: String(address?.number ?? '').trim(),
|
|
80
|
+
city:
|
|
81
|
+
address?.street?.district?.city?.city ||
|
|
82
|
+
address?.street?.city?.city ||
|
|
83
|
+
address?.city ||
|
|
84
|
+
'',
|
|
85
|
+
state:
|
|
86
|
+
address?.street?.district?.city?.state?.uf ||
|
|
87
|
+
address?.street?.district?.city?.state?.state ||
|
|
88
|
+
address?.street?.city?.state?.uf ||
|
|
89
|
+
address?.street?.city?.state?.state ||
|
|
90
|
+
address?.state ||
|
|
91
|
+
'',
|
|
92
|
+
zipCode:
|
|
93
|
+
(typeof address?.zipCode === 'object'
|
|
94
|
+
? address?.zipCode?.cep
|
|
95
|
+
: address?.zipCode) ||
|
|
96
|
+
address?.street?.cep?.cep ||
|
|
97
|
+
address?.postal_code ||
|
|
98
|
+
address?.cep ||
|
|
99
|
+
'',
|
|
100
|
+
complement: address?.complement || '',
|
|
101
|
+
district:
|
|
102
|
+
address?.street?.district?.district ||
|
|
103
|
+
address?.district ||
|
|
104
|
+
'',
|
|
105
|
+
country:
|
|
106
|
+
address?.street?.district?.city?.state?.country?.countrycode ||
|
|
107
|
+
address?.street?.district?.city?.state?.country?.countryname ||
|
|
108
|
+
address?.street?.city?.state?.country?.countrycode ||
|
|
109
|
+
address?.street?.city?.state?.country?.countryname ||
|
|
110
|
+
address?.country ||
|
|
111
|
+
'',
|
|
112
|
+
nickname: address?.nickname || '',
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const mapAddressesForClient = list =>
|
|
117
|
+
(Array.isArray(list) ? list : []).map(item => {
|
|
118
|
+
const id = normalizeId(item?.id || item?.['@id']);
|
|
119
|
+
const iri =
|
|
120
|
+
String(item?.['@id'] || '').startsWith('/addresses/')
|
|
121
|
+
? String(item['@id'])
|
|
122
|
+
: id
|
|
123
|
+
? `/addresses/${id}`
|
|
124
|
+
: '';
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
'@id': iri,
|
|
128
|
+
street: item.street,
|
|
129
|
+
number: item.number,
|
|
130
|
+
complement: item.complement,
|
|
131
|
+
district: item.district,
|
|
132
|
+
city: item.city,
|
|
133
|
+
state: item.state,
|
|
134
|
+
zipCode: item.zipCode,
|
|
135
|
+
country: item.country,
|
|
136
|
+
nickname: item.nickname,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the id/IRI that must be sent on edit so the default store
|
|
142
|
+
* issues PUT /addresses/{id} instead of POST /addresses.
|
|
143
|
+
*/
|
|
144
|
+
export const resolveAddressSaveId = (editingItem, payload = {}) => {
|
|
145
|
+
const candidates = [
|
|
146
|
+
payload?.id,
|
|
147
|
+
payload?.['@id'],
|
|
148
|
+
editingItem?.['@id'],
|
|
149
|
+
editingItem?.id,
|
|
150
|
+
];
|
|
151
|
+
for (const candidate of candidates) {
|
|
152
|
+
const digits = normalizeId(candidate);
|
|
153
|
+
if (digits) return digits;
|
|
154
|
+
}
|
|
155
|
+
return '';
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const buildAddressMapQueryParts = address => {
|
|
159
|
+
const streetLine = [normalizeString(address?.street), normalizeString(address?.number)]
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.join(', ');
|
|
162
|
+
const cityStateLine = [normalizeString(address?.city), normalizeString(address?.state)]
|
|
163
|
+
.filter(Boolean)
|
|
164
|
+
.join(' - ');
|
|
165
|
+
|
|
166
|
+
return [
|
|
167
|
+
streetLine,
|
|
168
|
+
normalizeString(address?.district),
|
|
169
|
+
cityStateLine,
|
|
170
|
+
normalizeString(address?.country),
|
|
171
|
+
normalizeZipCode(address?.zipCode),
|
|
172
|
+
normalizeString(address?.complement),
|
|
173
|
+
];
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const buildAddressPrimaryLine = address =>
|
|
177
|
+
[normalizeString(address?.street), normalizeString(address?.number)]
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.join(', ');
|
|
180
|
+
|
|
181
|
+
export const buildAddressSecondaryLine = address =>
|
|
182
|
+
[
|
|
183
|
+
normalizeString(address?.district),
|
|
184
|
+
[normalizeString(address?.city), normalizeString(address?.state)]
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.join(' - '),
|
|
187
|
+
normalizeString(address?.country),
|
|
188
|
+
]
|
|
189
|
+
.filter(Boolean)
|
|
190
|
+
.join(' • ');
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const IMAGE_FILE_PATTERN = /\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)(?:\?|$)/i;
|
|
2
|
+
const PDF_FILE_PATTERN = /\.pdf(?:\?|$)/i;
|
|
3
|
+
|
|
4
|
+
export const extractId = value => {
|
|
5
|
+
if (value === null || value === undefined || value === '') {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === 'number') {
|
|
9
|
+
return String(value);
|
|
10
|
+
}
|
|
11
|
+
const raw = typeof value === 'string' ? value : value?.id || value?.['@id'] || '';
|
|
12
|
+
const match = String(raw).match(/(\d+)(?:\D*)$/);
|
|
13
|
+
return match ? match[1] : String(raw).replace(/\D/g, '') || '';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const normalizeText = value => String(value || '').trim();
|
|
17
|
+
|
|
18
|
+
export const isImageFile = url => IMAGE_FILE_PATTERN.test(String(url || ''));
|
|
19
|
+
export const isPdfFile = url => PDF_FILE_PATTERN.test(String(url || ''));
|
|
20
|
+
|
|
21
|
+
export const applyDocumentMask = (value, documentType) => {
|
|
22
|
+
if (!value) {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
const numbers = String(value).replace(/\D/g, '');
|
|
26
|
+
const docType = String(documentType || '').toUpperCase();
|
|
27
|
+
|
|
28
|
+
if (docType === 'CPF') {
|
|
29
|
+
return numbers
|
|
30
|
+
.slice(0, 11)
|
|
31
|
+
.replace(/(\d{3})(\d)/, '$1.$2')
|
|
32
|
+
.replace(/(\d{3})(\d)/, '$1.$2')
|
|
33
|
+
.replace(/(\d{3})(\d{1,2})$/, '$1-$2');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (docType === 'CNPJ') {
|
|
37
|
+
return numbers
|
|
38
|
+
.slice(0, 14)
|
|
39
|
+
.replace(/(\d{2})(\d)/, '$1.$2')
|
|
40
|
+
.replace(/(\d{3})(\d)/, '$1.$2')
|
|
41
|
+
.replace(/(\d{3})(\d)/, '$1/$2')
|
|
42
|
+
.replace(/(\d{4})(\d{1,2})$/, '$1-$2');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return numbers;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const removeMask = value => String(value || '').replace(/\D/g, '');
|
|
49
|
+
|
|
50
|
+
export const toDocumentIri = documentId => {
|
|
51
|
+
const id = extractId(documentId);
|
|
52
|
+
return id ? `/documents/${id}` : '';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const toFileIri = fileValue => {
|
|
56
|
+
if (!fileValue) {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
if (typeof fileValue === 'string' && fileValue.startsWith('/files/')) {
|
|
60
|
+
return fileValue;
|
|
61
|
+
}
|
|
62
|
+
const id = extractId(fileValue?.id || fileValue?.['@id'] || fileValue);
|
|
63
|
+
return id ? `/files/${id}` : '';
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const normalizeAttachment = (attachment, resolveDownloadUrl) => {
|
|
67
|
+
if (!attachment) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const sourceFile = attachment.file || attachment;
|
|
71
|
+
const downloadUrl =
|
|
72
|
+
typeof resolveDownloadUrl === 'function'
|
|
73
|
+
? resolveDownloadUrl(sourceFile)
|
|
74
|
+
: sourceFile?.url || sourceFile?.downloadUrl || '';
|
|
75
|
+
const fileId = extractId(
|
|
76
|
+
sourceFile?.id || sourceFile?.['@id'] || sourceFile?.file_id || downloadUrl,
|
|
77
|
+
);
|
|
78
|
+
const name =
|
|
79
|
+
normalizeText(sourceFile?.name || sourceFile?.fileName || sourceFile?.originalName) ||
|
|
80
|
+
(fileId ? `arquivo-${fileId}` : 'arquivo');
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
id: attachment.id || attachment['@id'] || (fileId ? `legacy-${fileId}` : ''),
|
|
84
|
+
fileId,
|
|
85
|
+
name,
|
|
86
|
+
url: downloadUrl,
|
|
87
|
+
file: sourceFile,
|
|
88
|
+
legacy: Boolean(attachment.__legacy) || (!attachment.id && !attachment['@id']),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const normalizeDocumentFiles = (document, resolveDownloadUrl) => {
|
|
93
|
+
const linkedFiles = Array.isArray(document?.documentFiles)
|
|
94
|
+
? document.documentFiles
|
|
95
|
+
: Array.isArray(document?.document_files)
|
|
96
|
+
? document.document_files
|
|
97
|
+
: [];
|
|
98
|
+
|
|
99
|
+
const fromCollection = linkedFiles
|
|
100
|
+
.map(item => normalizeAttachment(item, resolveDownloadUrl))
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
|
|
103
|
+
if (fromCollection.length) {
|
|
104
|
+
return fromCollection;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const legacy = normalizeAttachment(
|
|
108
|
+
document?.file ? { ...document.file, __legacy: true } : null,
|
|
109
|
+
resolveDownloadUrl,
|
|
110
|
+
);
|
|
111
|
+
return legacy ? [{ ...legacy, legacy: true }] : [];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const toDocumentItem = document => ({
|
|
115
|
+
id: extractId(document?.id || document?.['@id']),
|
|
116
|
+
value: document?.document ?? document?.value ?? '',
|
|
117
|
+
type: document?.documentType?.['@id'] || document?.documentType || document?.type || '',
|
|
118
|
+
files: [],
|
|
119
|
+
raw: document,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export const unwrapUploadFile = payload => {
|
|
123
|
+
const data = payload?.response?.data ?? payload?.data ?? payload;
|
|
124
|
+
if (!data) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (data?.file) {
|
|
128
|
+
return data.file;
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(data)) {
|
|
131
|
+
return data[0] || null;
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(data?.member)) {
|
|
134
|
+
return data.member[0] || null;
|
|
135
|
+
}
|
|
136
|
+
if (Array.isArray(data?.['hydra:member'])) {
|
|
137
|
+
return data['hydra:member'][0] || null;
|
|
138
|
+
}
|
|
139
|
+
return data;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const ACCEPTED_DOCUMENT_FILE_TYPES = 'image/jpeg,image/png,image/jpg,application/pdf,.jpg,.jpeg,.png,.pdf';
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
'director',
|
|
5
|
-
'manager',
|
|
6
|
-
'salesman',
|
|
7
|
-
'after-sales',
|
|
8
|
-
'courier',
|
|
9
|
-
];
|
|
1
|
+
import {HUMAN_COMPANY_LINK_TYPES} from '../../utils/humanCompanyLinkTypes';
|
|
2
|
+
|
|
3
|
+
export const EMPLOYEE_CONTACT_LINK_TYPES = HUMAN_COMPANY_LINK_TYPES;
|
|
10
4
|
|
|
11
5
|
const extractId = value => {
|
|
12
6
|
if (value == null || value === '') {
|
|
@@ -181,19 +175,6 @@ export const buildEmployeeContactsFromPeopleLinks = (
|
|
|
181
175
|
return null;
|
|
182
176
|
}
|
|
183
177
|
|
|
184
|
-
if (
|
|
185
|
-
person &&
|
|
186
|
-
typeof person === 'object' &&
|
|
187
|
-
(person?.deleted === true || person?.deleted === 1 || person?.deleted === '1')
|
|
188
|
-
) {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const enableRaw = link?.enable ?? link?.enabled;
|
|
193
|
-
if (enableRaw === 0 || enableRaw === false || enableRaw === '0') {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
178
|
const base =
|
|
198
179
|
person && typeof person === 'object'
|
|
199
180
|
? {...person}
|
|
@@ -205,7 +186,6 @@ export const buildEmployeeContactsFromPeopleLinks = (
|
|
|
205
186
|
id: base.id != null && base.id !== '' ? base.id : personId,
|
|
206
187
|
linkType: normalizedLinkType,
|
|
207
188
|
peopleLink: link,
|
|
208
|
-
peopleLinkId: extractId(link?.id || link?.['@id']),
|
|
209
189
|
};
|
|
210
190
|
})
|
|
211
191
|
.filter(Boolean);
|
|
@@ -37,14 +37,17 @@ export const removeEmployeeFromCompany = async ({
|
|
|
37
37
|
removePeople,
|
|
38
38
|
removePeopleLink,
|
|
39
39
|
}) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
40
|
+
// Company Contatos: unlink people_links first. DELETE /people/{id} 404s when
|
|
41
|
+
// the item provider path is filtered (#694) and would soft-delete the person
|
|
42
|
+
// globally instead of only the company link.
|
|
44
43
|
if (linkId && typeof removePeopleLink === 'function') {
|
|
45
44
|
await removePeopleLink(linkId);
|
|
46
45
|
return {mode: 'link', peopleId, linkId};
|
|
47
46
|
}
|
|
47
|
+
if (peopleId && typeof removePeople === 'function') {
|
|
48
|
+
await removePeople(peopleId);
|
|
49
|
+
return {mode: 'people', peopleId, linkId};
|
|
50
|
+
}
|
|
48
51
|
const error = new Error(
|
|
49
52
|
'Não foi possível remover o colaborador (ação indisponível).',
|
|
50
53
|
);
|
|
@@ -2,6 +2,7 @@ import { resolveFileImageUrl } from '@controleonline/ui-common/src/react/utils/f
|
|
|
2
2
|
import {
|
|
3
3
|
} from '@controleonline/ui-common/src/react/utils/entityDisplay';
|
|
4
4
|
import { normalizeEmployeeLinkType } from './employeeContacts';
|
|
5
|
+
import { HUMAN_COMPANY_LINK_TYPE_OPTIONS } from '../../utils/humanCompanyLinkTypes';
|
|
5
6
|
|
|
6
7
|
export const extractId = value => String(value || '').replace(/\D/g, '');
|
|
7
8
|
export const normalizeIdentityValue = value => String(value);
|
|
@@ -139,14 +140,30 @@ export const parseBrDateToYmd = value => {
|
|
|
139
140
|
.padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
|
|
140
141
|
};
|
|
141
142
|
|
|
142
|
-
export const LINK_TYPE_OPTIONS =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
{ value: '
|
|
147
|
-
{ value: '
|
|
143
|
+
export const LINK_TYPE_OPTIONS = HUMAN_COMPANY_LINK_TYPE_OPTIONS;
|
|
144
|
+
|
|
145
|
+
/** Canonical People.peopleType values from API (enum F|J). Do not invent types. */
|
|
146
|
+
export const PEOPLE_TYPE_OPTIONS = [
|
|
147
|
+
{ value: 'F', label: 'Pessoa Física' },
|
|
148
|
+
{ value: 'J', label: 'Pessoa Jurídica' },
|
|
148
149
|
];
|
|
149
150
|
|
|
151
|
+
export const normalizePeopleType = value => {
|
|
152
|
+
const normalized = String(value || '')
|
|
153
|
+
.trim()
|
|
154
|
+
.toUpperCase();
|
|
155
|
+
if (normalized === 'J' || normalized === 'JURIDICA' || normalized === 'PJ') {
|
|
156
|
+
return 'J';
|
|
157
|
+
}
|
|
158
|
+
return 'F';
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const formatPeopleTypeLabel = value => {
|
|
162
|
+
const type = normalizePeopleType(value);
|
|
163
|
+
const option = PEOPLE_TYPE_OPTIONS.find(item => item.value === type);
|
|
164
|
+
return option?.label || type;
|
|
165
|
+
};
|
|
166
|
+
|
|
150
167
|
export const resolveEmployeeLinkType = employee =>
|
|
151
168
|
normalizeEmployeeLinkType(
|
|
152
169
|
employee?.linkType ||
|
|
@@ -199,8 +216,14 @@ export const formatEmployeeContactTitle = employee => {
|
|
|
199
216
|
export const formatEmployeeContactMeta = employee => {
|
|
200
217
|
const id = extractId(employee?.id || employee?.['@id']) || '-';
|
|
201
218
|
const linkType = resolveEmployeeContactLinkType(employee);
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
const peopleType = formatPeopleTypeLabel(
|
|
220
|
+
employee?.peopleType || employee?.people?.peopleType,
|
|
221
|
+
);
|
|
222
|
+
const parts = [`ID: ${id}`, peopleType];
|
|
223
|
+
if (linkType) {
|
|
224
|
+
parts.push(linkType);
|
|
225
|
+
}
|
|
226
|
+
return parts.join(' / ');
|
|
204
227
|
};
|
|
205
228
|
|
|
206
229
|
export const buildEmployeeCreatePayload = ({
|
|
@@ -209,11 +232,12 @@ export const buildEmployeeCreatePayload = ({
|
|
|
209
232
|
foundationDate,
|
|
210
233
|
linkType,
|
|
211
234
|
parentPeopleId,
|
|
235
|
+
peopleType,
|
|
212
236
|
}) => {
|
|
213
237
|
const payload = {
|
|
214
238
|
name,
|
|
215
239
|
alias,
|
|
216
|
-
peopleType:
|
|
240
|
+
peopleType: normalizePeopleType(peopleType),
|
|
217
241
|
company: `/people/${parentPeopleId}`,
|
|
218
242
|
linkType: normalizeEmployeeLinkType(linkType),
|
|
219
243
|
'extra-data': {},
|