@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
@@ -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 || !peopleCategoriesStore?.actions?.getItems) return;
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 peopleCategoriesStore.actions.getItems({
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'] || result?.items || peopleCategoriesStore.getters?.items || [];
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
- setLoading(false);
191
+ if (gen === itemsLoadGenRef.current) {
192
+ setLoading(false);
193
+ }
157
194
  }
158
- }, [peopleId, peopleIri, peopleCategoriesStore]);
195
+ }, [peopleId, peopleIri]);
159
196
 
160
197
  useEffect(() => {
161
198
  loadItems();
162
199
  }, [loadItems]);
163
200
 
164
- const loadCategoryOptions = useCallback(
165
- async context => {
166
- if (!context || !categoriesStore?.actions?.getItems) {
167
- setCategoryOptions([]);
168
- return;
169
- }
170
- setLoadingCategories(true);
171
- try {
172
- const result = await categoriesStore.actions.getItems({
173
- context,
174
- itemsPerPage: 200,
175
- });
176
- const list = Array.isArray(result)
177
- ? result
178
- : result?.['hydra:member'] || result?.items || [];
179
- setCategoryOptions(Array.isArray(list) ? list : []);
180
- } catch (err) {
181
- console.warn('[PeopleCategoriesPanel] categories load failed', err);
182
- setCategoryOptions([]);
183
- } finally {
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
- [categoriesStore],
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 && categoriesStore?.actions?.save) {
254
- const created = await categoriesStore.actions.save({
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
- showError?.('Categoria obrigatória');
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 (editingItem?.id && peopleCategoriesStore?.actions?.save) {
284
- await peopleCategoriesStore.actions.save({
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
- showSuccess?.('Categoria atualizada');
289
- } else if (peopleCategoriesStore?.actions?.save) {
290
- await peopleCategoriesStore.actions.save(payload);
291
- showSuccess?.('Categoria adicionada');
366
+ showOk?.('Categoria atualizada');
292
367
  } else {
293
- showError?.('Store people_categories indisponível');
294
- setSaving(false);
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
- if (!item?.id || !peopleCategoriesStore?.actions?.remove) return;
383
+ const remove = actionsRef.current.removePeopleCategory;
384
+ if (!item?.id || !remove) return;
310
385
  setSaving(true);
311
386
  try {
312
- await peopleCategoriesStore.actions.remove(item.id);
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: themeColors?.text || '#1E293B',
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: themeColors?.textSecondary || '#64748B',
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: themeColors?.primary || '#3B82F6',
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: themeColors?.surface || '#fff',
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: themeColors?.text || '#1E293B',
452
+ color: themeText,
378
453
  marginBottom: 12,
379
454
  },
380
455
  fieldLabel: {
381
456
  fontSize: 12,
382
457
  fontWeight: '600',
383
- color: themeColors?.textSecondary || '#64748B',
458
+ color: themeTextSecondary,
384
459
  marginBottom: 4,
385
460
  marginTop: 10,
386
461
  },
387
462
  input: {
388
463
  borderWidth: 1,
389
- borderColor: themeColors?.border || '#E2E8F0',
464
+ borderColor: themeBorder,
390
465
  borderRadius: 8,
391
466
  paddingHorizontal: 10,
392
467
  paddingVertical: Platform.OS === 'ios' ? 10 : 6,
393
- color: themeColors?.text || '#1E293B',
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: themeColors?.border || '#E2E8F0',
482
+ borderColor: themeBorder,
408
483
  },
409
484
  btnPrimary: {
410
485
  paddingHorizontal: 14,
411
486
  paddingVertical: 10,
412
487
  borderRadius: 8,
413
- backgroundColor: themeColors?.primary || '#3B82F6',
488
+ backgroundColor: themePrimary,
414
489
  },
415
490
  btnText: { fontSize: 13, fontWeight: '600' },
416
491
  }),
417
- [themeColors],
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={themeColors?.primary} />
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
- setForm(prev => ({
483
- ...prev,
484
- context: value,
485
- categoryId: '',
486
- categoryName: '',
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
- ...prev,
509
- categoryId: value,
510
- categoryName: opt?.name || prev.categoryName,
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={themeColors?.textSecondary}
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={themeColors?.textSecondary}
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={themeColors?.textSecondary}
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={themeColors?.textSecondary}
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: themeColors?.text }]}>Cancelar</Text>
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?.defaultCompany?.id,
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 onPress={onClose} style={inlineStyle_341_14}>
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 onPress={onSave} style={inlineStyle_348_20}>
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.password !== formData.confirmPassword) {
120
- showError('As senhas não coincidem.');
123
+ if (!formData.username) {
124
+ showError('Nome de usuário e senha são obrigatórios.');
121
125
  return;
122
126
  }
123
127
 
124
- if (!formData.username || !formData.password) {
125
- showError('Nome de usuário e senha são obrigatórios.');
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(extractErrorMessage(error) || 'Falha ao criar usuário. Tente novamente.');
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
- if (!String(formData.password || '').trim()) {
167
- showError('Nova senha é obrigatória.');
168
- return;
169
- }
170
-
171
- if (
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}