@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
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* PF: profession, position (cargo requires people_company_id)
|
|
4
4
|
* PJ: sector, activity_branch
|
|
5
5
|
* Issue: ControleOnline/app-community#377
|
|
6
|
+
* Fix React #185 on add: store actions via ref (app-community#816)
|
|
6
7
|
*/
|
|
7
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
8
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
9
|
import {
|
|
9
10
|
ActivityIndicator,
|
|
10
11
|
Modal,
|
|
@@ -119,14 +120,24 @@ const PeopleCategoriesPanel = ({
|
|
|
119
120
|
const { showError, showSuccess } = useMessage();
|
|
120
121
|
const themeStore = useStore('theme');
|
|
121
122
|
const themeColors = themeStore?.getters?.colors || {};
|
|
123
|
+
// Primitive theme tokens — object identity of getters.colors must not churn styles every render.
|
|
124
|
+
const themePrimary = themeColors?.primary || '#3B82F6';
|
|
125
|
+
const themeText = themeColors?.text || '#1E293B';
|
|
126
|
+
const themeTextSecondary = themeColors?.textSecondary || '#64748B';
|
|
127
|
+
const themeSurface = themeColors?.surface || '#fff';
|
|
128
|
+
const themeBorder = themeColors?.border || '#E2E8F0';
|
|
122
129
|
const categoriesStore = useStore('categories');
|
|
123
130
|
const peopleCategoriesStore = useStore('people_categories');
|
|
124
131
|
|
|
125
132
|
const peopleType = String(client?.peopleType || 'J').toUpperCase();
|
|
126
133
|
const isPF = peopleType === 'F';
|
|
127
134
|
const contexts = isPF ? PF_CONTEXTS : PJ_CONTEXTS;
|
|
128
|
-
const peopleIri = useMemo(() => toPeopleIri(client), [client]);
|
|
129
135
|
const peopleId = extractId(client);
|
|
136
|
+
// Primitive id only — client object identity must not re-trigger load (React #185).
|
|
137
|
+
const peopleIri = useMemo(
|
|
138
|
+
() => (peopleId != null ? `/people/${peopleId}` : null),
|
|
139
|
+
[peopleId],
|
|
140
|
+
);
|
|
130
141
|
|
|
131
142
|
const [items, setItems] = useState([]);
|
|
132
143
|
const [loading, setLoading] = useState(false);
|
|
@@ -137,72 +148,125 @@ const PeopleCategoriesPanel = ({
|
|
|
137
148
|
const [categoryOptions, setCategoryOptions] = useState([]);
|
|
138
149
|
const [loadingCategories, setLoadingCategories] = useState(false);
|
|
139
150
|
|
|
151
|
+
// Store objects / message helpers must not be effect deps (React #185 / Maximum update depth).
|
|
152
|
+
// Same pattern as Profile.js and ClassificationChips (#377).
|
|
153
|
+
const actionsRef = useRef({});
|
|
154
|
+
actionsRef.current = {
|
|
155
|
+
getPeopleCategories: peopleCategoriesStore?.actions?.getItems,
|
|
156
|
+
savePeopleCategory: peopleCategoriesStore?.actions?.save,
|
|
157
|
+
removePeopleCategory: peopleCategoriesStore?.actions?.remove,
|
|
158
|
+
getPeopleCategoriesItems: () => peopleCategoriesStore?.getters?.items,
|
|
159
|
+
getCategories: categoriesStore?.actions?.getItems,
|
|
160
|
+
saveCategory: categoriesStore?.actions?.save,
|
|
161
|
+
showError,
|
|
162
|
+
showSuccess,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const itemsLoadGenRef = useRef(0);
|
|
166
|
+
|
|
140
167
|
const loadItems = useCallback(async () => {
|
|
141
|
-
if (!peopleId
|
|
168
|
+
if (!peopleId) return;
|
|
169
|
+
const getItems = actionsRef.current.getPeopleCategories;
|
|
170
|
+
if (!getItems) return;
|
|
171
|
+
const gen = ++itemsLoadGenRef.current;
|
|
142
172
|
setLoading(true);
|
|
143
173
|
try {
|
|
144
|
-
const result = await
|
|
174
|
+
const result = await getItems({
|
|
145
175
|
people: peopleIri,
|
|
146
176
|
itemsPerPage: 100,
|
|
147
177
|
});
|
|
178
|
+
if (gen !== itemsLoadGenRef.current) return;
|
|
148
179
|
const list = Array.isArray(result)
|
|
149
180
|
? result
|
|
150
|
-
: result?.['hydra:member'] ||
|
|
181
|
+
: result?.['hydra:member'] ||
|
|
182
|
+
result?.items ||
|
|
183
|
+
actionsRef.current.getPeopleCategoriesItems?.() ||
|
|
184
|
+
[];
|
|
151
185
|
setItems(Array.isArray(list) ? list : []);
|
|
152
186
|
} catch (err) {
|
|
187
|
+
if (gen !== itemsLoadGenRef.current) return;
|
|
153
188
|
console.warn('[PeopleCategoriesPanel] load failed', err);
|
|
154
189
|
setItems([]);
|
|
155
190
|
} finally {
|
|
156
|
-
|
|
191
|
+
if (gen === itemsLoadGenRef.current) {
|
|
192
|
+
setLoading(false);
|
|
193
|
+
}
|
|
157
194
|
}
|
|
158
|
-
}, [peopleId, peopleIri
|
|
195
|
+
}, [peopleId, peopleIri]);
|
|
159
196
|
|
|
160
197
|
useEffect(() => {
|
|
161
198
|
loadItems();
|
|
162
199
|
}, [loadItems]);
|
|
163
200
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
// Guard concurrent category fetches (store isLoading re-renders must not re-enter).
|
|
202
|
+
const categoryLoadGenRef = useRef(0);
|
|
203
|
+
const lastCategoryContextRef = useRef('');
|
|
204
|
+
|
|
205
|
+
const loadCategoryOptions = useCallback(async context => {
|
|
206
|
+
if (!context) {
|
|
207
|
+
setCategoryOptions([]);
|
|
208
|
+
lastCategoryContextRef.current = '';
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Skip if we already loaded this context and still have options in flight/done
|
|
212
|
+
if (lastCategoryContextRef.current === context) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const getCategories = actionsRef.current.getCategories;
|
|
216
|
+
if (!getCategories) {
|
|
217
|
+
setCategoryOptions([]);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const gen = ++categoryLoadGenRef.current;
|
|
221
|
+
lastCategoryContextRef.current = context;
|
|
222
|
+
setLoadingCategories(true);
|
|
223
|
+
try {
|
|
224
|
+
const result = await getCategories({
|
|
225
|
+
context,
|
|
226
|
+
itemsPerPage: 200,
|
|
227
|
+
});
|
|
228
|
+
if (gen !== categoryLoadGenRef.current) return;
|
|
229
|
+
const list = Array.isArray(result)
|
|
230
|
+
? result
|
|
231
|
+
: result?.['hydra:member'] || result?.items || [];
|
|
232
|
+
setCategoryOptions(Array.isArray(list) ? list : []);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (gen !== categoryLoadGenRef.current) return;
|
|
235
|
+
console.warn('[PeopleCategoriesPanel] categories load failed', err);
|
|
236
|
+
setCategoryOptions([]);
|
|
237
|
+
} finally {
|
|
238
|
+
if (gen === categoryLoadGenRef.current) {
|
|
184
239
|
setLoadingCategories(false);
|
|
185
240
|
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
);
|
|
241
|
+
}
|
|
242
|
+
}, []);
|
|
189
243
|
|
|
244
|
+
// Only fetch catalog when modal is open — avoids mount/parent churn loops.
|
|
190
245
|
useEffect(() => {
|
|
246
|
+
if (!modalVisible) {
|
|
247
|
+
lastCategoryContextRef.current = '';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
191
250
|
if (form.context) {
|
|
192
251
|
loadCategoryOptions(form.context);
|
|
193
252
|
} else {
|
|
194
253
|
setCategoryOptions([]);
|
|
254
|
+
lastCategoryContextRef.current = '';
|
|
195
255
|
}
|
|
196
|
-
}, [form.context, loadCategoryOptions]);
|
|
256
|
+
}, [modalVisible, form.context, loadCategoryOptions]);
|
|
197
257
|
|
|
198
258
|
const openCreate = () => {
|
|
199
259
|
setEditingItem(null);
|
|
260
|
+
lastCategoryContextRef.current = '';
|
|
261
|
+
categoryLoadGenRef.current += 1;
|
|
200
262
|
const defaultContext = contexts[0]?.value || '';
|
|
201
263
|
setForm({
|
|
202
264
|
...emptyForm(),
|
|
203
265
|
context: defaultContext,
|
|
204
266
|
startDateBr: formatDateBr(new Date().toISOString().slice(0, 10)),
|
|
205
267
|
});
|
|
268
|
+
setCategoryOptions([]);
|
|
269
|
+
setLoadingCategories(false);
|
|
206
270
|
setModalVisible(true);
|
|
207
271
|
};
|
|
208
272
|
|
|
@@ -222,9 +286,13 @@ const PeopleCategoriesPanel = ({
|
|
|
222
286
|
};
|
|
223
287
|
|
|
224
288
|
const closeModal = () => {
|
|
289
|
+
categoryLoadGenRef.current += 1;
|
|
290
|
+
lastCategoryContextRef.current = '';
|
|
225
291
|
setModalVisible(false);
|
|
226
292
|
setEditingItem(null);
|
|
227
293
|
setForm(emptyForm());
|
|
294
|
+
setCategoryOptions([]);
|
|
295
|
+
setLoadingCategories(false);
|
|
228
296
|
};
|
|
229
297
|
|
|
230
298
|
const handleSave = async () => {
|
|
@@ -249,9 +317,13 @@ const PeopleCategoriesPanel = ({
|
|
|
249
317
|
setSaving(true);
|
|
250
318
|
try {
|
|
251
319
|
let categoryIri = form.categoryId ? `/categories/${form.categoryId}` : null;
|
|
320
|
+
const saveCategory = actionsRef.current.saveCategory;
|
|
321
|
+
const savePeopleCategory = actionsRef.current.savePeopleCategory;
|
|
322
|
+
const showErr = actionsRef.current.showError;
|
|
323
|
+
const showOk = actionsRef.current.showSuccess;
|
|
252
324
|
|
|
253
|
-
if (!categoryIri && form.categoryName &&
|
|
254
|
-
const created = await
|
|
325
|
+
if (!categoryIri && form.categoryName && saveCategory) {
|
|
326
|
+
const created = await saveCategory({
|
|
255
327
|
name: form.categoryName.trim(),
|
|
256
328
|
context: form.context,
|
|
257
329
|
});
|
|
@@ -260,7 +332,7 @@ const PeopleCategoriesPanel = ({
|
|
|
260
332
|
}
|
|
261
333
|
|
|
262
334
|
if (!categoryIri) {
|
|
263
|
-
|
|
335
|
+
showErr?.('Categoria obrigatória');
|
|
264
336
|
setSaving(false);
|
|
265
337
|
return;
|
|
266
338
|
}
|
|
@@ -280,40 +352,43 @@ const PeopleCategoriesPanel = ({
|
|
|
280
352
|
}
|
|
281
353
|
}
|
|
282
354
|
|
|
283
|
-
if (
|
|
284
|
-
|
|
355
|
+
if (!savePeopleCategory) {
|
|
356
|
+
showErr?.('Store people_categories indisponível');
|
|
357
|
+
setSaving(false);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (editingItem?.id) {
|
|
362
|
+
await savePeopleCategory({
|
|
285
363
|
...payload,
|
|
286
364
|
id: editingItem.id,
|
|
287
365
|
});
|
|
288
|
-
|
|
289
|
-
} else if (peopleCategoriesStore?.actions?.save) {
|
|
290
|
-
await peopleCategoriesStore.actions.save(payload);
|
|
291
|
-
showSuccess?.('Categoria adicionada');
|
|
366
|
+
showOk?.('Categoria atualizada');
|
|
292
367
|
} else {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return;
|
|
368
|
+
await savePeopleCategory(payload);
|
|
369
|
+
showOk?.('Categoria adicionada');
|
|
296
370
|
}
|
|
297
371
|
|
|
298
372
|
closeModal();
|
|
299
373
|
await loadItems();
|
|
300
374
|
} catch (err) {
|
|
301
375
|
console.error('[PeopleCategoriesPanel] save failed', err);
|
|
302
|
-
showError?.(err?.message || 'Falha ao salvar categoria');
|
|
376
|
+
actionsRef.current.showError?.(err?.message || 'Falha ao salvar categoria');
|
|
303
377
|
} finally {
|
|
304
378
|
setSaving(false);
|
|
305
379
|
}
|
|
306
380
|
};
|
|
307
381
|
|
|
308
382
|
const handleDelete = async item => {
|
|
309
|
-
|
|
383
|
+
const remove = actionsRef.current.removePeopleCategory;
|
|
384
|
+
if (!item?.id || !remove) return;
|
|
310
385
|
setSaving(true);
|
|
311
386
|
try {
|
|
312
|
-
await
|
|
313
|
-
showSuccess?.('Categoria removida');
|
|
387
|
+
await remove(item.id);
|
|
388
|
+
actionsRef.current.showSuccess?.('Categoria removida');
|
|
314
389
|
await loadItems();
|
|
315
390
|
} catch (err) {
|
|
316
|
-
showError?.(err?.message || 'Falha ao remover');
|
|
391
|
+
actionsRef.current.showError?.(err?.message || 'Falha ao remover');
|
|
317
392
|
} finally {
|
|
318
393
|
setSaving(false);
|
|
319
394
|
}
|
|
@@ -331,7 +406,7 @@ const PeopleCategoriesPanel = ({
|
|
|
331
406
|
title: {
|
|
332
407
|
fontSize: 15,
|
|
333
408
|
fontWeight: '600',
|
|
334
|
-
color:
|
|
409
|
+
color: themeText,
|
|
335
410
|
},
|
|
336
411
|
badgesWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
|
337
412
|
badge: {
|
|
@@ -346,7 +421,7 @@ const PeopleCategoriesPanel = ({
|
|
|
346
421
|
badgeText: { color: '#fff', fontSize: 12, fontWeight: '600', maxWidth: 160 },
|
|
347
422
|
badgeMeta: { color: 'rgba(255,255,255,0.85)', fontSize: 10, marginLeft: 6 },
|
|
348
423
|
empty: {
|
|
349
|
-
color:
|
|
424
|
+
color: themeTextSecondary,
|
|
350
425
|
fontSize: 13,
|
|
351
426
|
fontStyle: 'italic',
|
|
352
427
|
},
|
|
@@ -356,7 +431,7 @@ const PeopleCategoriesPanel = ({
|
|
|
356
431
|
paddingHorizontal: 10,
|
|
357
432
|
paddingVertical: 6,
|
|
358
433
|
borderRadius: 8,
|
|
359
|
-
backgroundColor:
|
|
434
|
+
backgroundColor: themePrimary,
|
|
360
435
|
},
|
|
361
436
|
addBtnText: { color: '#fff', fontSize: 12, fontWeight: '600', marginLeft: 4 },
|
|
362
437
|
modalOverlay: {
|
|
@@ -366,7 +441,7 @@ const PeopleCategoriesPanel = ({
|
|
|
366
441
|
padding: 20,
|
|
367
442
|
},
|
|
368
443
|
modalCard: {
|
|
369
|
-
backgroundColor:
|
|
444
|
+
backgroundColor: themeSurface,
|
|
370
445
|
borderRadius: 12,
|
|
371
446
|
padding: 16,
|
|
372
447
|
maxHeight: '85%',
|
|
@@ -374,23 +449,23 @@ const PeopleCategoriesPanel = ({
|
|
|
374
449
|
modalTitle: {
|
|
375
450
|
fontSize: 16,
|
|
376
451
|
fontWeight: '700',
|
|
377
|
-
color:
|
|
452
|
+
color: themeText,
|
|
378
453
|
marginBottom: 12,
|
|
379
454
|
},
|
|
380
455
|
fieldLabel: {
|
|
381
456
|
fontSize: 12,
|
|
382
457
|
fontWeight: '600',
|
|
383
|
-
color:
|
|
458
|
+
color: themeTextSecondary,
|
|
384
459
|
marginBottom: 4,
|
|
385
460
|
marginTop: 10,
|
|
386
461
|
},
|
|
387
462
|
input: {
|
|
388
463
|
borderWidth: 1,
|
|
389
|
-
borderColor:
|
|
464
|
+
borderColor: themeBorder,
|
|
390
465
|
borderRadius: 8,
|
|
391
466
|
paddingHorizontal: 10,
|
|
392
467
|
paddingVertical: Platform.OS === 'ios' ? 10 : 6,
|
|
393
|
-
color:
|
|
468
|
+
color: themeText,
|
|
394
469
|
fontSize: 14,
|
|
395
470
|
},
|
|
396
471
|
actionsRow: {
|
|
@@ -404,17 +479,17 @@ const PeopleCategoriesPanel = ({
|
|
|
404
479
|
paddingVertical: 10,
|
|
405
480
|
borderRadius: 8,
|
|
406
481
|
borderWidth: 1,
|
|
407
|
-
borderColor:
|
|
482
|
+
borderColor: themeBorder,
|
|
408
483
|
},
|
|
409
484
|
btnPrimary: {
|
|
410
485
|
paddingHorizontal: 14,
|
|
411
486
|
paddingVertical: 10,
|
|
412
487
|
borderRadius: 8,
|
|
413
|
-
backgroundColor:
|
|
488
|
+
backgroundColor: themePrimary,
|
|
414
489
|
},
|
|
415
490
|
btnText: { fontSize: 13, fontWeight: '600' },
|
|
416
491
|
}),
|
|
417
|
-
[
|
|
492
|
+
[themePrimary, themeText, themeTextSecondary, themeSurface, themeBorder],
|
|
418
493
|
);
|
|
419
494
|
|
|
420
495
|
if (!peopleId) return null;
|
|
@@ -432,7 +507,7 @@ const PeopleCategoriesPanel = ({
|
|
|
432
507
|
</View>
|
|
433
508
|
|
|
434
509
|
{loading ? (
|
|
435
|
-
<ActivityIndicator size="small" color={
|
|
510
|
+
<ActivityIndicator size="small" color={themePrimary} />
|
|
436
511
|
) : items.length === 0 ? (
|
|
437
512
|
<Text style={styles.empty}>Nenhuma categoria associada</Text>
|
|
438
513
|
) : (
|
|
@@ -478,14 +553,19 @@ const PeopleCategoriesPanel = ({
|
|
|
478
553
|
<View style={styles.input}>
|
|
479
554
|
<Picker
|
|
480
555
|
selectedValue={form.context}
|
|
481
|
-
onValueChange={value =>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
context
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
556
|
+
onValueChange={value => {
|
|
557
|
+
// RN-web Picker often re-fires on reconcile; keep same prev to avoid #185.
|
|
558
|
+
setForm(prev => {
|
|
559
|
+
if (String(prev.context) === String(value)) return prev;
|
|
560
|
+
lastCategoryContextRef.current = '';
|
|
561
|
+
return {
|
|
562
|
+
...prev,
|
|
563
|
+
context: value,
|
|
564
|
+
categoryId: '',
|
|
565
|
+
categoryName: '',
|
|
566
|
+
};
|
|
567
|
+
});
|
|
568
|
+
}}
|
|
489
569
|
>
|
|
490
570
|
{contexts.map(c => (
|
|
491
571
|
<Picker.Item key={c.value} label={c.label} value={c.value} />
|
|
@@ -504,11 +584,14 @@ const PeopleCategoriesPanel = ({
|
|
|
504
584
|
const opt = categoryOptions.find(
|
|
505
585
|
o => String(extractId(o)) === String(value),
|
|
506
586
|
);
|
|
507
|
-
setForm(prev =>
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
587
|
+
setForm(prev => {
|
|
588
|
+
if (String(prev.categoryId) === String(value)) return prev;
|
|
589
|
+
return {
|
|
590
|
+
...prev,
|
|
591
|
+
categoryId: value,
|
|
592
|
+
categoryName: opt?.name || prev.categoryName,
|
|
593
|
+
};
|
|
594
|
+
});
|
|
512
595
|
}}
|
|
513
596
|
>
|
|
514
597
|
<Picker.Item label="— selecione —" value="" />
|
|
@@ -532,7 +615,7 @@ const PeopleCategoriesPanel = ({
|
|
|
532
615
|
onChangeText={text =>
|
|
533
616
|
setForm(prev => ({ ...prev, categoryName: text, categoryId: '' }))
|
|
534
617
|
}
|
|
535
|
-
placeholderTextColor={
|
|
618
|
+
placeholderTextColor={themeTextSecondary}
|
|
536
619
|
/>
|
|
537
620
|
)}
|
|
538
621
|
|
|
@@ -542,7 +625,7 @@ const PeopleCategoriesPanel = ({
|
|
|
542
625
|
value={form.startDateBr}
|
|
543
626
|
onChangeText={text => setForm(prev => ({ ...prev, startDateBr: text }))}
|
|
544
627
|
placeholder="01/01/2024"
|
|
545
|
-
placeholderTextColor={
|
|
628
|
+
placeholderTextColor={themeTextSecondary}
|
|
546
629
|
/>
|
|
547
630
|
|
|
548
631
|
<Text style={styles.fieldLabel}>Data fim (opcional)</Text>
|
|
@@ -551,7 +634,7 @@ const PeopleCategoriesPanel = ({
|
|
|
551
634
|
value={form.endDateBr}
|
|
552
635
|
onChangeText={text => setForm(prev => ({ ...prev, endDateBr: text }))}
|
|
553
636
|
placeholder="vazio = atual"
|
|
554
|
-
placeholderTextColor={
|
|
637
|
+
placeholderTextColor={themeTextSecondary}
|
|
555
638
|
/>
|
|
556
639
|
|
|
557
640
|
{form.context === 'position' && (
|
|
@@ -568,7 +651,7 @@ const PeopleCategoriesPanel = ({
|
|
|
568
651
|
? `Padrão: ${extractId(parentCompanyIri)}`
|
|
569
652
|
: 'ID da empresa'
|
|
570
653
|
}
|
|
571
|
-
placeholderTextColor={
|
|
654
|
+
placeholderTextColor={themeTextSecondary}
|
|
572
655
|
keyboardType="numeric"
|
|
573
656
|
/>
|
|
574
657
|
</>
|
|
@@ -576,7 +659,7 @@ const PeopleCategoriesPanel = ({
|
|
|
576
659
|
|
|
577
660
|
<View style={styles.actionsRow}>
|
|
578
661
|
<TouchableOpacity style={styles.btnSecondary} onPress={closeModal}>
|
|
579
|
-
<Text style={[styles.btnText, { color:
|
|
662
|
+
<Text style={[styles.btnText, { color: themeText }]}>Cancelar</Text>
|
|
580
663
|
</TouchableOpacity>
|
|
581
664
|
<TouchableOpacity
|
|
582
665
|
style={styles.btnPrimary}
|
|
@@ -73,7 +73,7 @@ const SalesmanTab = ({
|
|
|
73
73
|
const currentCompanyId = extractId(
|
|
74
74
|
peopleGetters?.currentCompany?.id ||
|
|
75
75
|
peopleGetters?.currentCompany?.['@id'] ||
|
|
76
|
-
peopleGetters?.
|
|
76
|
+
peopleGetters?.mainCompany?.id,
|
|
77
77
|
);
|
|
78
78
|
|
|
79
79
|
const appType = useMemo(() => resolveAppType(), []);
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
extractId,
|
|
16
16
|
toTimezoneItem,
|
|
17
17
|
} from './usersTabHelpers';
|
|
18
|
+
import {
|
|
19
|
+
PASSWORD_HELP_LINES,
|
|
20
|
+
} from '@controleonline/ui-common/src/react/utils/passwordPolicy';
|
|
18
21
|
import {
|
|
19
22
|
inlineStyle_174_6,
|
|
20
23
|
inlineStyle_175_12,
|
|
@@ -128,6 +131,17 @@ const UserFormModal = ({
|
|
|
128
131
|
<TouchableOpacity onPress={onClose} style={inlineStyle_200_49}>
|
|
129
132
|
<Icon name="close" size={20} color="#64748B" />
|
|
130
133
|
</TouchableOpacity>
|
|
134
|
+
|
|
135
|
+
<View style={{marginBottom: 12, padding: 10, borderRadius: 8, backgroundColor: '#F8FAFC', borderWidth: 1, borderColor: '#E2E8F0'}}>
|
|
136
|
+
<Text style={{fontSize: 13, fontWeight: '600', color: '#334155', marginBottom: 4}}>
|
|
137
|
+
Requisitos da senha
|
|
138
|
+
</Text>
|
|
139
|
+
{PASSWORD_HELP_LINES.map(line => (
|
|
140
|
+
<Text key={line} style={{fontSize: 12, color: '#64748B', lineHeight: 18}}>
|
|
141
|
+
• {line}
|
|
142
|
+
</Text>
|
|
143
|
+
))}
|
|
144
|
+
</View>
|
|
131
145
|
</View>
|
|
132
146
|
|
|
133
147
|
<ScrollView
|
|
@@ -286,10 +300,20 @@ const UserFormModal = ({
|
|
|
286
300
|
</ScrollView>
|
|
287
301
|
|
|
288
302
|
<View style={inlineStyle_339_16}>
|
|
289
|
-
<TouchableOpacity
|
|
303
|
+
<TouchableOpacity
|
|
304
|
+
onPress={onClose}
|
|
305
|
+
style={inlineStyle_341_14}
|
|
306
|
+
accessibilityLabel="Cancelar"
|
|
307
|
+
accessibilityRole="button"
|
|
308
|
+
testID="user-form-cancel">
|
|
290
309
|
<Text style={{color: '#64748B', fontWeight: '600'}}>Cancelar</Text>
|
|
291
310
|
</TouchableOpacity>
|
|
292
|
-
<TouchableOpacity
|
|
311
|
+
<TouchableOpacity
|
|
312
|
+
onPress={onSave}
|
|
313
|
+
style={inlineStyle_348_20}
|
|
314
|
+
accessibilityLabel="Salvar"
|
|
315
|
+
accessibilityRole="button"
|
|
316
|
+
testID="user-form-save">
|
|
293
317
|
<Text style={{color: '#FFFFFF', fontWeight: '600'}}>Salvar</Text>
|
|
294
318
|
</TouchableOpacity>
|
|
295
319
|
</View>
|
|
@@ -10,6 +10,10 @@ import FeatherIcon from 'react-native-vector-icons/Feather';
|
|
|
10
10
|
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
11
11
|
import { useStores } from '@store';
|
|
12
12
|
import {useMessage} from '@controleonline/ui-common/src/react/components/MessageService';
|
|
13
|
+
import {
|
|
14
|
+
mapPasswordErrorMessage,
|
|
15
|
+
validatePasswordClient,
|
|
16
|
+
} from '@controleonline/ui-common/src/react/utils/passwordPolicy';
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
import {
|
|
@@ -116,13 +120,17 @@ const UsersTab = ({ client, customStyles, isEditing, onUpdateClient }) => {
|
|
|
116
120
|
|
|
117
121
|
const handleSave = async () => {
|
|
118
122
|
if (!editingItem) {
|
|
119
|
-
if (formData.
|
|
120
|
-
showError('
|
|
123
|
+
if (!formData.username) {
|
|
124
|
+
showError('Nome de usuário e senha são obrigatórios.');
|
|
121
125
|
return;
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
const createPasswordError = validatePasswordClient(
|
|
129
|
+
formData.password,
|
|
130
|
+
formData.confirmPassword,
|
|
131
|
+
);
|
|
132
|
+
if (createPasswordError) {
|
|
133
|
+
showError(createPasswordError);
|
|
126
134
|
return;
|
|
127
135
|
}
|
|
128
136
|
|
|
@@ -159,20 +167,20 @@ const UsersTab = ({ client, customStyles, isEditing, onUpdateClient }) => {
|
|
|
159
167
|
showSuccess('Usuário criado com sucesso!');
|
|
160
168
|
closeModal();
|
|
161
169
|
} catch (error) {
|
|
162
|
-
showError(
|
|
170
|
+
showError(
|
|
171
|
+
mapPasswordErrorMessage(
|
|
172
|
+
extractErrorMessage(error) || 'Falha ao criar usuário. Tente novamente.',
|
|
173
|
+
),
|
|
174
|
+
);
|
|
163
175
|
}
|
|
164
176
|
} else {
|
|
165
177
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
formData.password &&
|
|
173
|
-
formData.password !== formData.confirmPassword
|
|
174
|
-
) {
|
|
175
|
-
showError('As senhas não coincidem.');
|
|
178
|
+
const changePasswordError = validatePasswordClient(
|
|
179
|
+
formData.password,
|
|
180
|
+
formData.confirmPassword,
|
|
181
|
+
);
|
|
182
|
+
if (changePasswordError) {
|
|
183
|
+
showError(changePasswordError);
|
|
176
184
|
return;
|
|
177
185
|
}
|
|
178
186
|
|
|
@@ -200,7 +208,7 @@ const UsersTab = ({ client, customStyles, isEditing, onUpdateClient }) => {
|
|
|
200
208
|
showSuccess('Senha do usuário atualizada com sucesso!');
|
|
201
209
|
closeModal();
|
|
202
210
|
} catch (error) {
|
|
203
|
-
showError(extractErrorMessage(error) || 'Falha ao atualizar usuário. Tente novamente.');
|
|
211
|
+
showError(mapPasswordErrorMessage(extractErrorMessage(error) || 'Falha ao atualizar usuário. Tente novamente.'));
|
|
204
212
|
}
|
|
205
213
|
}
|
|
206
214
|
};
|
|
@@ -221,7 +229,7 @@ const UsersTab = ({ client, customStyles, isEditing, onUpdateClient }) => {
|
|
|
221
229
|
|
|
222
230
|
showSuccess('Chave de API copiada com sucesso!');
|
|
223
231
|
} catch (error) {
|
|
224
|
-
showError(extractErrorMessage(error) || 'Nao foi possivel copiar a chave de API.');
|
|
232
|
+
showError(mapPasswordErrorMessage(extractErrorMessage(error) || 'Nao foi possivel copiar a chave de API.'));
|
|
225
233
|
}
|
|
226
234
|
};
|
|
227
235
|
|
|
@@ -295,7 +303,7 @@ const UsersTab = ({ client, customStyles, isEditing, onUpdateClient }) => {
|
|
|
295
303
|
syncUsers(updatedUsers);
|
|
296
304
|
showSuccess('Usuário removido com sucesso!');
|
|
297
305
|
} catch (error) {
|
|
298
|
-
showError(extractErrorMessage(error) || 'Falha ao remover usuário. Tente novamente.');
|
|
306
|
+
showError(mapPasswordErrorMessage(extractErrorMessage(error) || 'Falha ao remover usuário. Tente novamente.'));
|
|
299
307
|
}
|
|
300
308
|
},
|
|
301
309
|
});
|
|
@@ -335,7 +343,10 @@ const UsersTab = ({ client, customStyles, isEditing, onUpdateClient }) => {
|
|
|
335
343
|
{isEditing && (
|
|
336
344
|
<TouchableOpacity
|
|
337
345
|
onPress={() => openModal()}
|
|
338
|
-
style={customStyles.iconButtonPrimary}
|
|
346
|
+
style={customStyles.iconButtonPrimary}
|
|
347
|
+
accessibilityLabel="Adicionar usuário"
|
|
348
|
+
accessibilityRole="button"
|
|
349
|
+
testID="users-tab-add-user">
|
|
339
350
|
<FeatherIcon
|
|
340
351
|
name="plus"
|
|
341
352
|
size={16}
|