@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.
- package/.github/agents/developer.agent.md +30 -0
- package/.github/agents/devops.agent.md +30 -0
- package/.github/agents/qa.agent.md +30 -0
- package/.github/agents/security.agent.md +30 -0
- package/.scrutinizer.yml +20 -0
- package/AGENTS.md +50 -0
- package/FUNDING.yml +1 -0
- package/README.md +23 -0
- package/docs/technical/Client-Details-Franquia-Filial.md +18 -0
- package/docs/technical/Client-Details-People-Name-Case.md +103 -0
- package/docs/technical/Cliente-Vendedor-Vinculo-e-Permissoes.md +152 -0
- package/docs/wiki.md +1 -0
- package/package.json +32 -0
- package/scripts/validate-wiki-pointer.cjs +13 -0
- package/src/react/components/tabs/AddressesTab.js +494 -0
- package/src/react/components/tabs/CategoriesTab.js +196 -0
- package/src/react/components/tabs/ClassificationChips.js +494 -0
- package/src/react/components/tabs/ContactTab.js +621 -0
- package/src/react/components/tabs/ContactTab.styles.js +119 -0
- package/src/react/components/tabs/ContractsTab.js +13 -0
- package/src/react/components/tabs/DocumentsTab.js +463 -0
- package/src/react/components/tabs/DocumentsTab.styles.js +133 -0
- package/src/react/components/tabs/EmployeeContactRow.js +83 -0
- package/src/react/components/tabs/EmployeesTab.js +454 -0
- package/src/react/components/tabs/EmployeesTab.styles.js +172 -0
- package/src/react/components/tabs/FiscalTab.js +6 -0
- package/src/react/components/tabs/FranchiseCommissionSection.js +307 -0
- package/src/react/components/tabs/FranchiseLinksManageModal.js +215 -0
- package/src/react/components/tabs/FranchiseLinksTab.js +282 -0
- package/src/react/components/tabs/GeneralTab.js +466 -0
- package/src/react/components/tabs/GeneralTab.styles.js +101 -0
- package/src/react/components/tabs/MediaTab.js +356 -0
- package/src/react/components/tabs/PeopleCategoriesPanel.js +601 -0
- package/src/react/components/tabs/ProductsTab.js +236 -0
- package/src/react/components/tabs/ProductsTab.styles.js +143 -0
- package/src/react/components/tabs/SalesmanCommissionBlock.js +120 -0
- package/src/react/components/tabs/SalesmanManageModal.js +208 -0
- package/src/react/components/tabs/SalesmanTab.js +436 -0
- package/src/react/components/tabs/SalesmanTab.styles.js +4 -0
- package/src/react/components/tabs/UserApiKeyModal.js +90 -0
- package/src/react/components/tabs/UserFormModal.js +301 -0
- package/src/react/components/tabs/UsersTab.js +426 -0
- package/src/react/components/tabs/UsersTab.styles.js +283 -0
- package/src/react/components/tabs/UsersTab.styles.test.js +42 -0
- package/src/react/components/tabs/contactMutationMessage.js +18 -0
- package/src/react/components/tabs/employeeContacts.js +214 -0
- package/src/react/components/tabs/employeeUnlink.js +68 -0
- package/src/react/components/tabs/employeesTabHelpers.js +227 -0
- package/src/react/components/tabs/franchiseCommissionHelpers.js +134 -0
- package/src/react/components/tabs/franchiseLinksTab.helpers.js +247 -0
- package/src/react/components/tabs/generalTabHelpers.js +120 -0
- package/src/react/components/tabs/salesmanTab.helpers.js +114 -0
- package/src/react/components/tabs/salesmanTabHelpers.js +173 -0
- package/src/react/components/tabs/salesmanTabMedia.js +55 -0
- package/src/react/components/tabs/salesmanTabSession.js +39 -0
- package/src/react/components/tabs/useEmployeeUnlink.js +58 -0
- package/src/react/components/tabs/useFranchiseLinksManage.js +215 -0
- package/src/react/components/tabs/useSalesmanManage.js +222 -0
- package/src/react/components/tabs/usersTabHelpers.js +221 -0
- package/src/react/domain/productSupplierFlow.js +37 -0
- package/src/react/index.js +4 -0
- package/src/react/pages/ClientDetailsSkeleton.js +84 -0
- package/src/react/pages/clientDetailsHelpers.js +17 -0
- package/src/react/pages/clients.js +27 -0
- package/src/react/pages/details.js +529 -0
- package/src/react/pages/details.page.styles.js +105 -0
- package/src/react/pages/details.styles.js +15 -0
- package/src/react/pages/employees.js +69 -0
- package/src/react/pages/franchisees.js +30 -0
- package/src/react/pages/prospects.js +21 -0
- package/src/react/pages/provider.js +21 -0
- package/src/react/router/routes.js +104 -0
- package/src/react/styles/details.js +571 -0
- package/src/store/people_categories/index.js +72 -0
- package/src/tests/browser/manager/client-details-create-user.spec.js +321 -0
- package/src/tests/browser/manager/client-details-employee-unlink.spec.js +215 -0
- package/src/tests/browser/manager/client-details-employees-refresh.spec.js +271 -0
- package/src/tests/browser/manager/client-details-enable.spec.js +241 -0
- package/src/tests/browser/manager/client-details-fiscal-tabs.spec.js +410 -0
- package/src/tests/browser/manager/client-details-franchise-commission.spec.js +318 -0
- package/src/tests/browser/manager/client-details-franchise-links.spec.js +365 -0
- package/src/tests/browser/manager/client-details-media.spec.js +405 -0
- package/src/tests/browser/manager/client-details-relationships.spec.js +313 -0
- package/src/tests/browser/manager/client-details-salesman-commission.spec.js +291 -0
- package/src/tests/browser/manager/client-details-salesman-manage.spec.js +448 -0
- package/src/tests/react/components/tabs/contactMutationMessage.test.js +25 -0
- package/src/tests/react/components/tabs/employeeContacts.test.js +266 -0
- package/src/tests/react/components/tabs/employeeUnlink.test.js +67 -0
- package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +78 -0
- package/src/tests/react/components/tabs/employeesTabHelpers.test.js +64 -0
- package/src/tests/react/components/tabs/fiscalTabContract.test.mjs +29 -0
- package/src/tests/react/components/tabs/franchiseCommissionHelpers.test.js +128 -0
- package/src/tests/react/components/tabs/franchiseLinksTab.helpers.test.mjs +166 -0
- package/src/tests/react/components/tabs/generalTabHelpers.test.js +130 -0
- package/src/tests/react/components/tabs/peopleMediaTabs.test.js +322 -0
- package/src/tests/react/components/tabs/salesmanTab.helpers.test.mjs +75 -0
- package/src/tests/react/components/tabs/salesmanTabHelpers.test.js +142 -0
- package/src/tests/react/components/tabs/salesmanTabHelpers.test.mjs +92 -0
- package/src/tests/react/components/tabs/usersTabHelpers.test.js +59 -0
- package/src/tests/react/domain/productSupplierFlow.test.mjs +55 -0
- package/src/tests/react/pages/clientDetailsHelpers.test.js +98 -0
- package/src/tests/react/pages/employees.test.js +44 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import React, {useEffect, useMemo, useState} from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Linking,
|
|
4
|
+
Modal,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
View,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
11
|
+
import FeatherIcon from 'react-native-vector-icons/Feather';
|
|
12
|
+
import {useStores} from '@store';
|
|
13
|
+
import {useMessage} from '@controleonline/ui-common/src/react/components/MessageService';
|
|
14
|
+
import DefaultAddress from '@controleonline/ui-default/src/react/components/address/DefaultAddress';
|
|
15
|
+
import {
|
|
16
|
+
buildGoogleMapsNavigationUrl,
|
|
17
|
+
buildNavigationMapQuery,
|
|
18
|
+
buildWazeNavigationUrl,
|
|
19
|
+
} from '@controleonline/ui-common/src/react/utils/mapNavigation';
|
|
20
|
+
|
|
21
|
+
const extractId = value => String(value || '').replace(/\D/g, '');
|
|
22
|
+
const normalizeId = value => extractId(value) || value || Date.now();
|
|
23
|
+
|
|
24
|
+
const toPeopleIri = person => {
|
|
25
|
+
const rawIri = String(person?.['@id'] || '').trim();
|
|
26
|
+
if (rawIri.startsWith('/people/')) {
|
|
27
|
+
return rawIri;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const nestedIri = String(person?.people?.['@id'] || person?.people || '').trim();
|
|
31
|
+
if (nestedIri.startsWith('/people/')) {
|
|
32
|
+
return nestedIri;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const id = extractId(person?.id || person?.people?.id || rawIri || nestedIri);
|
|
36
|
+
return id ? `/people/${id}` : '';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const normalizeString = value => {
|
|
40
|
+
const text = String(value || '').trim();
|
|
41
|
+
return text.length > 0 ? text : undefined;
|
|
42
|
+
};
|
|
43
|
+
const normalizeZipCode = value =>
|
|
44
|
+
String(value || '')
|
|
45
|
+
.replace(/\D/g, '')
|
|
46
|
+
.slice(0, 8);
|
|
47
|
+
|
|
48
|
+
const buildAddressMapQuery = address => {
|
|
49
|
+
const streetLine = [normalizeString(address?.street), normalizeString(address?.number)]
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join(', ');
|
|
52
|
+
const cityStateLine = [normalizeString(address?.city), normalizeString(address?.state)]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(' - ');
|
|
55
|
+
|
|
56
|
+
return buildNavigationMapQuery([
|
|
57
|
+
streetLine,
|
|
58
|
+
normalizeString(address?.district),
|
|
59
|
+
cityStateLine,
|
|
60
|
+
normalizeString(address?.country),
|
|
61
|
+
normalizeZipCode(address?.zipCode),
|
|
62
|
+
normalizeString(address?.complement),
|
|
63
|
+
]);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const buildAddressPrimaryLine = address =>
|
|
67
|
+
[normalizeString(address?.street), normalizeString(address?.number)]
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join(', ');
|
|
70
|
+
|
|
71
|
+
const buildAddressSecondaryLine = address =>
|
|
72
|
+
[
|
|
73
|
+
normalizeString(address?.district),
|
|
74
|
+
[normalizeString(address?.city), normalizeString(address?.state)]
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.join(' - '),
|
|
77
|
+
normalizeString(address?.country),
|
|
78
|
+
]
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join(' • ');
|
|
81
|
+
|
|
82
|
+
const normalizeAddress = address => ({
|
|
83
|
+
id: normalizeId(address?.id || address?.['@id']),
|
|
84
|
+
street: address?.street?.street || address?.street || '',
|
|
85
|
+
number: String(address?.number || '').trim(),
|
|
86
|
+
city:
|
|
87
|
+
address?.street?.district?.city?.city ||
|
|
88
|
+
address?.street?.city?.city ||
|
|
89
|
+
address?.city ||
|
|
90
|
+
'',
|
|
91
|
+
state:
|
|
92
|
+
address?.street?.district?.city?.state?.uf ||
|
|
93
|
+
address?.street?.district?.city?.state?.state ||
|
|
94
|
+
address?.street?.city?.state?.uf ||
|
|
95
|
+
address?.street?.city?.state?.state ||
|
|
96
|
+
address?.state ||
|
|
97
|
+
'',
|
|
98
|
+
zipCode:
|
|
99
|
+
(typeof address?.zipCode === 'object'
|
|
100
|
+
? address?.zipCode?.cep
|
|
101
|
+
: address?.zipCode) ||
|
|
102
|
+
address?.street?.cep?.cep ||
|
|
103
|
+
address?.postal_code ||
|
|
104
|
+
address?.cep ||
|
|
105
|
+
'',
|
|
106
|
+
complement: address?.complement || '',
|
|
107
|
+
district:
|
|
108
|
+
address?.street?.district?.district ||
|
|
109
|
+
address?.district ||
|
|
110
|
+
'',
|
|
111
|
+
country:
|
|
112
|
+
address?.street?.district?.city?.state?.country?.countrycode ||
|
|
113
|
+
address?.street?.district?.city?.state?.country?.countryname ||
|
|
114
|
+
address?.street?.city?.state?.country?.countrycode ||
|
|
115
|
+
address?.street?.city?.state?.country?.countryname ||
|
|
116
|
+
address?.country ||
|
|
117
|
+
'',
|
|
118
|
+
nickname: address?.nickname || '',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const mapAddressesForClient = list =>
|
|
122
|
+
list.map(item => ({
|
|
123
|
+
id: item.id,
|
|
124
|
+
'@id': String(item?.id || '').startsWith('/addresses/')
|
|
125
|
+
? String(item.id)
|
|
126
|
+
: `/addresses/${extractId(item.id || '')}`,
|
|
127
|
+
street: item.street,
|
|
128
|
+
number: item.number,
|
|
129
|
+
complement: item.complement,
|
|
130
|
+
district: item.district,
|
|
131
|
+
city: item.city,
|
|
132
|
+
state: item.state,
|
|
133
|
+
zipCode: item.zipCode,
|
|
134
|
+
country: item.country,
|
|
135
|
+
nickname: item.nickname,
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
const AddressesTab = ({client, customStyles, isEditing, onUpdateClient}) => {
|
|
139
|
+
const {showError, showSuccess, showDialog} = useMessage();
|
|
140
|
+
const [addresses, setAddresses] = useState([]);
|
|
141
|
+
const [showModal, setShowModal] = useState(false);
|
|
142
|
+
const [editingItem, setEditingItem] = useState(null);
|
|
143
|
+
const [showNavigationModal, setShowNavigationModal] = useState(false);
|
|
144
|
+
const [navigationAddress, setNavigationAddress] = useState(null);
|
|
145
|
+
|
|
146
|
+
const addressStore = useStores(state => state.address) || {};
|
|
147
|
+
const actions = addressStore.actions || {};
|
|
148
|
+
const peopleIri = useMemo(() => toPeopleIri(client), [client]);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const rawAddresses = Array.isArray(client?.address)
|
|
152
|
+
? client.address.map(item => normalizeAddress(item))
|
|
153
|
+
: [];
|
|
154
|
+
|
|
155
|
+
setAddresses(rawAddresses);
|
|
156
|
+
}, [client]);
|
|
157
|
+
|
|
158
|
+
const openModal = item => {
|
|
159
|
+
const normalizedItem = item ? normalizeAddress(item) : {};
|
|
160
|
+
setEditingItem(item ? normalizedItem : null);
|
|
161
|
+
setShowModal(true);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const closeModal = () => {
|
|
165
|
+
setShowModal(false);
|
|
166
|
+
setEditingItem(null);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const closeNavigationModal = () => {
|
|
170
|
+
setShowNavigationModal(false);
|
|
171
|
+
setNavigationAddress(null);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const openNavigationModal = address => {
|
|
175
|
+
const normalizedAddress = normalizeAddress(address);
|
|
176
|
+
const mapQuery = buildAddressMapQuery(normalizedAddress);
|
|
177
|
+
|
|
178
|
+
if (!mapQuery) {
|
|
179
|
+
showError('Nao foi possivel montar a navegacao para este endereco.');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
setNavigationAddress(normalizedAddress);
|
|
184
|
+
setShowNavigationModal(true);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const handleOpenNavigation = async appName => {
|
|
188
|
+
const mapQuery = buildAddressMapQuery(navigationAddress);
|
|
189
|
+
const url =
|
|
190
|
+
appName === 'waze'
|
|
191
|
+
? buildWazeNavigationUrl({mapQuery})
|
|
192
|
+
: buildGoogleMapsNavigationUrl({mapQuery});
|
|
193
|
+
|
|
194
|
+
if (!url) {
|
|
195
|
+
showError('Nao foi possivel montar a navegacao para este endereco.');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
closeNavigationModal();
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
await Linking.openURL(url);
|
|
203
|
+
} catch {
|
|
204
|
+
showError('Nao foi possivel abrir o aplicativo de navegacao.');
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const saveAddress = async payload => {
|
|
209
|
+
if (!actions?.save) {
|
|
210
|
+
throw new Error('Servico de enderecos indisponivel no momento.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!peopleIri) {
|
|
214
|
+
throw new Error('Nao foi possivel identificar o cliente para salvar o endereco.');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return actions.save({
|
|
218
|
+
...payload,
|
|
219
|
+
people: peopleIri,
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const handleAddressSaved = saved => {
|
|
224
|
+
const normalizedSaved = normalizeAddress(saved || {});
|
|
225
|
+
const updatedAddresses = editingItem
|
|
226
|
+
? addresses.map(item =>
|
|
227
|
+
item.id === editingItem.id ? normalizedSaved : item,
|
|
228
|
+
)
|
|
229
|
+
: [...addresses, normalizedSaved];
|
|
230
|
+
|
|
231
|
+
setAddresses(updatedAddresses);
|
|
232
|
+
onUpdateClient?.('address', mapAddressesForClient(updatedAddresses));
|
|
233
|
+
|
|
234
|
+
showSuccess(
|
|
235
|
+
editingItem
|
|
236
|
+
? 'Endereco atualizado com sucesso!'
|
|
237
|
+
: 'Endereco criado com sucesso!',
|
|
238
|
+
);
|
|
239
|
+
closeModal();
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const handleDelete = id => {
|
|
243
|
+
showDialog({
|
|
244
|
+
title: 'Confirmar exclusao',
|
|
245
|
+
message: 'Deseja realmente remover este item?',
|
|
246
|
+
confirmLabel: 'Remover',
|
|
247
|
+
cancelLabel: 'Cancelar',
|
|
248
|
+
onConfirm: async () => {
|
|
249
|
+
try {
|
|
250
|
+
if (!actions?.remove) {
|
|
251
|
+
showError('Servico de enderecos indisponivel no momento.');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
await actions.remove(id);
|
|
255
|
+
const updatedAddresses = addresses.filter(item => item.id !== id);
|
|
256
|
+
setAddresses(updatedAddresses);
|
|
257
|
+
onUpdateClient?.('address', mapAddressesForClient(updatedAddresses));
|
|
258
|
+
showSuccess('Endereco removido com sucesso!');
|
|
259
|
+
} catch {
|
|
260
|
+
showError('Falha ao remover endereco. Tente novamente.');
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const renderModal = () => (
|
|
267
|
+
<Modal visible={showModal} transparent animationType="slide">
|
|
268
|
+
<View style={[customStyles.modalOverlay, addressModalStyles.overlay]}>
|
|
269
|
+
<View style={addressModalStyles.container}>
|
|
270
|
+
<View style={addressModalStyles.header}>
|
|
271
|
+
<Text style={[customStyles.modalTitle, addressModalStyles.title]}>
|
|
272
|
+
{editingItem ? global.t?.t('address', 'title', 'editAddress') : global.t?.t('address', 'title', 'address')}
|
|
273
|
+
</Text>
|
|
274
|
+
<TouchableOpacity
|
|
275
|
+
onPress={closeModal}
|
|
276
|
+
style={addressModalStyles.closeButton}>
|
|
277
|
+
<FeatherIcon name="x" size={20} color="#0F172A" />
|
|
278
|
+
</TouchableOpacity>
|
|
279
|
+
</View>
|
|
280
|
+
<DefaultAddress
|
|
281
|
+
mode={editingItem ? 'edit' : 'create'}
|
|
282
|
+
row={editingItem
|
|
283
|
+
? {
|
|
284
|
+
...editingItem,
|
|
285
|
+
cep: editingItem.zipCode,
|
|
286
|
+
uf: editingItem.state,
|
|
287
|
+
country: editingItem.country,
|
|
288
|
+
}
|
|
289
|
+
: null}
|
|
290
|
+
peopleIri={peopleIri}
|
|
291
|
+
saveAction={saveAddress}
|
|
292
|
+
onCancel={closeModal}
|
|
293
|
+
onSaved={handleAddressSaved}
|
|
294
|
+
submitLabel={global.t?.t('address', 'button', 'save') || 'Salvar'}
|
|
295
|
+
/>
|
|
296
|
+
</View>
|
|
297
|
+
</View>
|
|
298
|
+
</Modal>
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const renderNavigationModal = () => {
|
|
302
|
+
const primaryLine = buildAddressPrimaryLine(navigationAddress);
|
|
303
|
+
const secondaryLine = buildAddressSecondaryLine(navigationAddress);
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<Modal
|
|
307
|
+
visible={showNavigationModal}
|
|
308
|
+
transparent
|
|
309
|
+
animationType="fade"
|
|
310
|
+
onRequestClose={closeNavigationModal}>
|
|
311
|
+
<View style={customStyles.modalOverlay}>
|
|
312
|
+
<View style={customStyles.modalContainer}>
|
|
313
|
+
<Text style={customStyles.modalTitle}>Abrir endereco</Text>
|
|
314
|
+
<Text style={customStyles.navigationModalDescription}>
|
|
315
|
+
Escolha qual aplicativo deseja usar para navegar ate este endereco.
|
|
316
|
+
</Text>
|
|
317
|
+
<View style={customStyles.navigationAddressCard}>
|
|
318
|
+
<Text style={customStyles.navigationAddressTitle}>
|
|
319
|
+
{primaryLine || 'Endereco'}
|
|
320
|
+
</Text>
|
|
321
|
+
{secondaryLine ? (
|
|
322
|
+
<Text style={customStyles.navigationAddressSubtitle}>
|
|
323
|
+
{secondaryLine}
|
|
324
|
+
</Text>
|
|
325
|
+
) : null}
|
|
326
|
+
</View>
|
|
327
|
+
<View style={customStyles.navigationOptions}>
|
|
328
|
+
<TouchableOpacity
|
|
329
|
+
style={[
|
|
330
|
+
customStyles.navigationOptionButton,
|
|
331
|
+
customStyles.navigationOptionButtonPrimary,
|
|
332
|
+
]}
|
|
333
|
+
onPress={() => handleOpenNavigation('google')}>
|
|
334
|
+
<Text
|
|
335
|
+
style={[
|
|
336
|
+
customStyles.navigationOptionButtonText,
|
|
337
|
+
customStyles.navigationOptionButtonTextPrimary,
|
|
338
|
+
]}>
|
|
339
|
+
Google Maps
|
|
340
|
+
</Text>
|
|
341
|
+
</TouchableOpacity>
|
|
342
|
+
<TouchableOpacity
|
|
343
|
+
style={customStyles.navigationOptionButton}
|
|
344
|
+
onPress={() => handleOpenNavigation('waze')}>
|
|
345
|
+
<Text style={customStyles.navigationOptionButtonText}>
|
|
346
|
+
Waze
|
|
347
|
+
</Text>
|
|
348
|
+
</TouchableOpacity>
|
|
349
|
+
</View>
|
|
350
|
+
<TouchableOpacity
|
|
351
|
+
style={customStyles.modalCancelButton}
|
|
352
|
+
onPress={closeNavigationModal}>
|
|
353
|
+
<Text style={customStyles.modalCancelText}>Cancelar</Text>
|
|
354
|
+
</TouchableOpacity>
|
|
355
|
+
</View>
|
|
356
|
+
</View>
|
|
357
|
+
</Modal>
|
|
358
|
+
);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<>
|
|
363
|
+
<View style={customStyles.tabContent}>
|
|
364
|
+
<View style={customStyles.section}>
|
|
365
|
+
<View style={customStyles.sectionHeader}>
|
|
366
|
+
<Text style={customStyles.sectionTitle}>{global.t?.t('address', 'title', 'addresses')}</Text>
|
|
367
|
+
{isEditing && (
|
|
368
|
+
<TouchableOpacity
|
|
369
|
+
onPress={() => openModal(null)}
|
|
370
|
+
style={customStyles.iconButtonPrimary}>
|
|
371
|
+
<Icon
|
|
372
|
+
name="add"
|
|
373
|
+
size={20}
|
|
374
|
+
color={customStyles.iconButtonPrimaryIcon.color}
|
|
375
|
+
/>
|
|
376
|
+
</TouchableOpacity>
|
|
377
|
+
)}
|
|
378
|
+
</View>
|
|
379
|
+
{addresses.length === 0 ? (
|
|
380
|
+
<Text style={customStyles.emptyText}>{global.t?.t('address', 'label', 'noAddressesFound')}</Text>
|
|
381
|
+
) : (
|
|
382
|
+
addresses.map(address => (
|
|
383
|
+
<View
|
|
384
|
+
key={address.id}
|
|
385
|
+
style={[
|
|
386
|
+
customStyles.listItem,
|
|
387
|
+
customStyles.listItemWithActions,
|
|
388
|
+
]}>
|
|
389
|
+
<View style={customStyles.itemContent}>
|
|
390
|
+
<TouchableOpacity
|
|
391
|
+
onPress={() => openNavigationModal(address)}
|
|
392
|
+
style={[
|
|
393
|
+
customStyles.iconButtonLocation,
|
|
394
|
+
customStyles.locationButton,
|
|
395
|
+
]}>
|
|
396
|
+
<Icon
|
|
397
|
+
name="location-on"
|
|
398
|
+
size={20}
|
|
399
|
+
color={customStyles.iconButtonPrimaryIcon.color}
|
|
400
|
+
/>
|
|
401
|
+
</TouchableOpacity>
|
|
402
|
+
<View>
|
|
403
|
+
<Text style={customStyles.itemText}>
|
|
404
|
+
{address.street}
|
|
405
|
+
{address.number ? `, ${address.number}` : ''}
|
|
406
|
+
</Text>
|
|
407
|
+
<Text style={customStyles.itemSubtext}>
|
|
408
|
+
{address.district ? `${address.district}, ` : ''}
|
|
409
|
+
{address.city}
|
|
410
|
+
{address.state ? ` - ${address.state}` : ''}
|
|
411
|
+
{address.country ? ` (${address.country})` : ''}
|
|
412
|
+
{address.complement ? `\n${address.complement}` : ''}
|
|
413
|
+
{address.nickname && address.nickname !== 'DEFAULT'
|
|
414
|
+
? `\n${address.nickname}`
|
|
415
|
+
: ''}
|
|
416
|
+
</Text>
|
|
417
|
+
</View>
|
|
418
|
+
</View>
|
|
419
|
+
{isEditing && (
|
|
420
|
+
<View
|
|
421
|
+
style={[
|
|
422
|
+
customStyles.itemActions,
|
|
423
|
+
customStyles.itemActionsPinned,
|
|
424
|
+
]}>
|
|
425
|
+
<TouchableOpacity
|
|
426
|
+
onPress={() => openModal(address)}
|
|
427
|
+
style={customStyles.iconButtonGhost}>
|
|
428
|
+
<FeatherIcon
|
|
429
|
+
name="edit-2"
|
|
430
|
+
size={16}
|
|
431
|
+
color={customStyles.iconButtonGhostIcon.color}
|
|
432
|
+
/>
|
|
433
|
+
</TouchableOpacity>
|
|
434
|
+
<TouchableOpacity
|
|
435
|
+
onPress={() => handleDelete(address.id)}
|
|
436
|
+
style={customStyles.iconButtonGhost}>
|
|
437
|
+
<FeatherIcon
|
|
438
|
+
name="trash-2"
|
|
439
|
+
size={16}
|
|
440
|
+
color={customStyles.iconButtonGhostIcon.color}
|
|
441
|
+
/>
|
|
442
|
+
</TouchableOpacity>
|
|
443
|
+
</View>
|
|
444
|
+
)}
|
|
445
|
+
</View>
|
|
446
|
+
))
|
|
447
|
+
)}
|
|
448
|
+
</View>
|
|
449
|
+
</View>
|
|
450
|
+
{renderModal()}
|
|
451
|
+
{renderNavigationModal()}
|
|
452
|
+
</>
|
|
453
|
+
);
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const addressModalStyles = StyleSheet.create({
|
|
457
|
+
overlay: {
|
|
458
|
+
justifyContent: 'flex-start',
|
|
459
|
+
alignItems: 'stretch',
|
|
460
|
+
backgroundColor: '#F8FAFC',
|
|
461
|
+
},
|
|
462
|
+
container: {
|
|
463
|
+
flex: 1,
|
|
464
|
+
width: '100%',
|
|
465
|
+
maxWidth: '100%',
|
|
466
|
+
borderRadius: 0,
|
|
467
|
+
backgroundColor: '#FFFFFF',
|
|
468
|
+
padding: 0,
|
|
469
|
+
},
|
|
470
|
+
header: {
|
|
471
|
+
minHeight: 64,
|
|
472
|
+
paddingHorizontal: 24,
|
|
473
|
+
borderBottomWidth: 1,
|
|
474
|
+
borderBottomColor: '#E2E8F0',
|
|
475
|
+
flexDirection: 'row',
|
|
476
|
+
alignItems: 'center',
|
|
477
|
+
justifyContent: 'space-between',
|
|
478
|
+
backgroundColor: '#FFFFFF',
|
|
479
|
+
},
|
|
480
|
+
title: {
|
|
481
|
+
textAlign: 'left',
|
|
482
|
+
marginBottom: 0,
|
|
483
|
+
},
|
|
484
|
+
closeButton: {
|
|
485
|
+
width: 40,
|
|
486
|
+
height: 40,
|
|
487
|
+
borderRadius: 20,
|
|
488
|
+
alignItems: 'center',
|
|
489
|
+
justifyContent: 'center',
|
|
490
|
+
backgroundColor: '#F1F5F9',
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
export default AddressesTab;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CategoriesTab — classificação de pessoas (PF: profession/position; PJ: sector/activity_branch)
|
|
3
|
+
* com badges e período (start_date / end_date). Issue #377.
|
|
4
|
+
*/
|
|
5
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
ActivityIndicator,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
Text,
|
|
10
|
+
TouchableOpacity,
|
|
11
|
+
View,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { useStore } from '@store';
|
|
14
|
+
import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
|
|
15
|
+
|
|
16
|
+
const normalizeCollection = payload => {
|
|
17
|
+
if (Array.isArray(payload)) return payload;
|
|
18
|
+
if (!payload || typeof payload !== 'object') return [];
|
|
19
|
+
if (Array.isArray(payload.member)) return payload.member;
|
|
20
|
+
if (Array.isArray(payload['hydra:member'])) return payload['hydra:member'];
|
|
21
|
+
if (Array.isArray(payload.items)) return payload.items;
|
|
22
|
+
return [];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const extractId = value => {
|
|
26
|
+
if (!value && value !== 0) return null;
|
|
27
|
+
if (typeof value === 'number') return value;
|
|
28
|
+
const raw = typeof value === 'string' ? value : value?.id || value?.['@id'];
|
|
29
|
+
if (!raw) return null;
|
|
30
|
+
const match = String(raw).match(/(\d+)$/);
|
|
31
|
+
return match ? parseInt(match[1], 10) : null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const PF_CONTEXTS = [
|
|
35
|
+
{ key: 'profession', label: 'Profissão' },
|
|
36
|
+
{ key: 'position', label: 'Cargo' },
|
|
37
|
+
];
|
|
38
|
+
const PJ_CONTEXTS = [
|
|
39
|
+
{ key: 'sector', label: 'Setor' },
|
|
40
|
+
{ key: 'activity_branch', label: 'Ramo de Atividade' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const formatDate = value => {
|
|
44
|
+
if (!value) return '—';
|
|
45
|
+
try {
|
|
46
|
+
const d = new Date(value);
|
|
47
|
+
if (Number.isNaN(d.getTime())) return String(value);
|
|
48
|
+
return d.toLocaleDateString('pt-BR');
|
|
49
|
+
} catch {
|
|
50
|
+
return String(value);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const CategoriesTab = ({ client }) => {
|
|
55
|
+
const peopleCategoryStore = useStore('people_category');
|
|
56
|
+
const categoryStore = useStore('category');
|
|
57
|
+
const { showError, showSuccess } = useMessage?.() || {};
|
|
58
|
+
const clientId = extractId(client?.id || client?.['@id']);
|
|
59
|
+
const peopleType = String(client?.peopleType || 'J').trim().toUpperCase() || 'J';
|
|
60
|
+
const isPF = peopleType === 'F';
|
|
61
|
+
const contexts = isPF ? PF_CONTEXTS : PJ_CONTEXTS;
|
|
62
|
+
|
|
63
|
+
const [items, setItems] = useState([]);
|
|
64
|
+
const [loading, setLoading] = useState(false);
|
|
65
|
+
const [removingId, setRemovingId] = useState(null);
|
|
66
|
+
|
|
67
|
+
const load = useCallback(async () => {
|
|
68
|
+
if (!clientId || !peopleCategoryStore?.actions?.getItems) return;
|
|
69
|
+
setLoading(true);
|
|
70
|
+
try {
|
|
71
|
+
const res = await peopleCategoryStore.actions.getItems({
|
|
72
|
+
people: `/people/${clientId}`,
|
|
73
|
+
active: true,
|
|
74
|
+
});
|
|
75
|
+
setItems(normalizeCollection(res));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
showError?.(e?.message || 'Falha ao carregar categorias');
|
|
78
|
+
setItems([]);
|
|
79
|
+
} finally {
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}
|
|
82
|
+
}, [clientId, peopleCategoryStore, showError]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
load();
|
|
86
|
+
}, [load]);
|
|
87
|
+
|
|
88
|
+
const handleRemove = useCallback(
|
|
89
|
+
async row => {
|
|
90
|
+
const id = extractId(row?.id || row?.['@id']);
|
|
91
|
+
if (!id || !peopleCategoryStore?.actions?.remove) return;
|
|
92
|
+
setRemovingId(id);
|
|
93
|
+
try {
|
|
94
|
+
await peopleCategoryStore.actions.remove(id);
|
|
95
|
+
showSuccess?.('Categoria removida.');
|
|
96
|
+
await load();
|
|
97
|
+
} catch (e) {
|
|
98
|
+
showError?.(e?.message || 'Falha ao remover categoria');
|
|
99
|
+
} finally {
|
|
100
|
+
setRemovingId(null);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[peopleCategoryStore, load, showError, showSuccess],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const badgeLabel = row => {
|
|
107
|
+
const cat = row?.category;
|
|
108
|
+
const name = cat?.name || cat?.['@id'] || 'Categoria';
|
|
109
|
+
const ctx = cat?.context || '';
|
|
110
|
+
const ctxLabel = contexts.find(c => c.key === ctx)?.label || ctx;
|
|
111
|
+
return ctxLabel ? `${name} · ${ctxLabel}` : name;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (!clientId) {
|
|
115
|
+
return (
|
|
116
|
+
<View style={styles.container}>
|
|
117
|
+
<Text style={styles.muted}>Selecione uma pessoa para ver categorias.</Text>
|
|
118
|
+
</View>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<View style={styles.container}>
|
|
124
|
+
<Text style={styles.title}>Classificação</Text>
|
|
125
|
+
<Text style={styles.subtitle}>
|
|
126
|
+
{isPF
|
|
127
|
+
? 'Profissão e cargo (com período).'
|
|
128
|
+
: 'Setor e ramo de atividade (com período).'}
|
|
129
|
+
</Text>
|
|
130
|
+
|
|
131
|
+
{loading ? (
|
|
132
|
+
<ActivityIndicator style={{ marginTop: 16 }} />
|
|
133
|
+
) : items.length === 0 ? (
|
|
134
|
+
<Text style={styles.muted}>Nenhuma categoria associada.</Text>
|
|
135
|
+
) : (
|
|
136
|
+
<View style={styles.badgeList}>
|
|
137
|
+
{items.map(row => {
|
|
138
|
+
const id = extractId(row?.id || row?.['@id']);
|
|
139
|
+
return (
|
|
140
|
+
<View key={id || Math.random()} style={styles.badge}>
|
|
141
|
+
<View style={{ flex: 1 }}>
|
|
142
|
+
<Text style={styles.badgeText}>{badgeLabel(row)}</Text>
|
|
143
|
+
<Text style={styles.dates}>
|
|
144
|
+
{formatDate(row?.startDate || row?.start_date)} →{' '}
|
|
145
|
+
{formatDate(row?.endDate || row?.end_date)}
|
|
146
|
+
</Text>
|
|
147
|
+
</View>
|
|
148
|
+
<TouchableOpacity
|
|
149
|
+
onPress={() => handleRemove(row)}
|
|
150
|
+
disabled={removingId === id}
|
|
151
|
+
style={styles.removeBtn}
|
|
152
|
+
>
|
|
153
|
+
<Text style={styles.removeText}>
|
|
154
|
+
{removingId === id ? '…' : 'Remover'}
|
|
155
|
+
</Text>
|
|
156
|
+
</TouchableOpacity>
|
|
157
|
+
</View>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
160
|
+
</View>
|
|
161
|
+
)}
|
|
162
|
+
|
|
163
|
+
<Text style={[styles.muted, { marginTop: 16 }]}>
|
|
164
|
+
Para adicionar: use o cadastro de categorias (context{' '}
|
|
165
|
+
{contexts.map(c => c.key).join(', ')}) e associe via API{' '}
|
|
166
|
+
<Text style={{ fontFamily: 'monospace' }}>/people_categories</Text> com
|
|
167
|
+
start_date / end_date
|
|
168
|
+
{isPF ? ' e people_company_id no cargo.' : '.'}
|
|
169
|
+
</Text>
|
|
170
|
+
</View>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const styles = StyleSheet.create({
|
|
175
|
+
container: { padding: 16 },
|
|
176
|
+
title: { fontSize: 16, fontWeight: '600', marginBottom: 4 },
|
|
177
|
+
subtitle: { fontSize: 13, color: '#64748B', marginBottom: 12 },
|
|
178
|
+
muted: { fontSize: 13, color: '#94A3B8' },
|
|
179
|
+
badgeList: { gap: 8 },
|
|
180
|
+
badge: {
|
|
181
|
+
flexDirection: 'row',
|
|
182
|
+
alignItems: 'center',
|
|
183
|
+
backgroundColor: '#EEF2FF',
|
|
184
|
+
borderRadius: 8,
|
|
185
|
+
paddingHorizontal: 12,
|
|
186
|
+
paddingVertical: 10,
|
|
187
|
+
borderWidth: 1,
|
|
188
|
+
borderColor: '#C7D2FE',
|
|
189
|
+
},
|
|
190
|
+
badgeText: { fontSize: 14, fontWeight: '500', color: '#312E81' },
|
|
191
|
+
dates: { fontSize: 12, color: '#6366F1', marginTop: 2 },
|
|
192
|
+
removeBtn: { paddingHorizontal: 8, paddingVertical: 4 },
|
|
193
|
+
removeText: { color: '#DC2626', fontSize: 13 },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export default CategoriesTab;
|