@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.
Files changed (102) hide show
  1. package/.github/agents/developer.agent.md +30 -0
  2. package/.github/agents/devops.agent.md +30 -0
  3. package/.github/agents/qa.agent.md +30 -0
  4. package/.github/agents/security.agent.md +30 -0
  5. package/.scrutinizer.yml +20 -0
  6. package/AGENTS.md +50 -0
  7. package/FUNDING.yml +1 -0
  8. package/README.md +23 -0
  9. package/docs/technical/Client-Details-Franquia-Filial.md +18 -0
  10. package/docs/technical/Client-Details-People-Name-Case.md +103 -0
  11. package/docs/technical/Cliente-Vendedor-Vinculo-e-Permissoes.md +152 -0
  12. package/docs/wiki.md +1 -0
  13. package/package.json +32 -0
  14. package/scripts/validate-wiki-pointer.cjs +13 -0
  15. package/src/react/components/tabs/AddressesTab.js +494 -0
  16. package/src/react/components/tabs/CategoriesTab.js +196 -0
  17. package/src/react/components/tabs/ClassificationChips.js +494 -0
  18. package/src/react/components/tabs/ContactTab.js +621 -0
  19. package/src/react/components/tabs/ContactTab.styles.js +119 -0
  20. package/src/react/components/tabs/ContractsTab.js +13 -0
  21. package/src/react/components/tabs/DocumentsTab.js +463 -0
  22. package/src/react/components/tabs/DocumentsTab.styles.js +133 -0
  23. package/src/react/components/tabs/EmployeeContactRow.js +83 -0
  24. package/src/react/components/tabs/EmployeesTab.js +454 -0
  25. package/src/react/components/tabs/EmployeesTab.styles.js +172 -0
  26. package/src/react/components/tabs/FiscalTab.js +6 -0
  27. package/src/react/components/tabs/FranchiseCommissionSection.js +307 -0
  28. package/src/react/components/tabs/FranchiseLinksManageModal.js +215 -0
  29. package/src/react/components/tabs/FranchiseLinksTab.js +282 -0
  30. package/src/react/components/tabs/GeneralTab.js +466 -0
  31. package/src/react/components/tabs/GeneralTab.styles.js +101 -0
  32. package/src/react/components/tabs/MediaTab.js +356 -0
  33. package/src/react/components/tabs/PeopleCategoriesPanel.js +601 -0
  34. package/src/react/components/tabs/ProductsTab.js +236 -0
  35. package/src/react/components/tabs/ProductsTab.styles.js +143 -0
  36. package/src/react/components/tabs/SalesmanCommissionBlock.js +120 -0
  37. package/src/react/components/tabs/SalesmanManageModal.js +208 -0
  38. package/src/react/components/tabs/SalesmanTab.js +436 -0
  39. package/src/react/components/tabs/SalesmanTab.styles.js +4 -0
  40. package/src/react/components/tabs/UserApiKeyModal.js +90 -0
  41. package/src/react/components/tabs/UserFormModal.js +301 -0
  42. package/src/react/components/tabs/UsersTab.js +426 -0
  43. package/src/react/components/tabs/UsersTab.styles.js +283 -0
  44. package/src/react/components/tabs/UsersTab.styles.test.js +42 -0
  45. package/src/react/components/tabs/contactMutationMessage.js +18 -0
  46. package/src/react/components/tabs/employeeContacts.js +214 -0
  47. package/src/react/components/tabs/employeeUnlink.js +68 -0
  48. package/src/react/components/tabs/employeesTabHelpers.js +227 -0
  49. package/src/react/components/tabs/franchiseCommissionHelpers.js +134 -0
  50. package/src/react/components/tabs/franchiseLinksTab.helpers.js +247 -0
  51. package/src/react/components/tabs/generalTabHelpers.js +120 -0
  52. package/src/react/components/tabs/salesmanTab.helpers.js +114 -0
  53. package/src/react/components/tabs/salesmanTabHelpers.js +173 -0
  54. package/src/react/components/tabs/salesmanTabMedia.js +55 -0
  55. package/src/react/components/tabs/salesmanTabSession.js +39 -0
  56. package/src/react/components/tabs/useEmployeeUnlink.js +58 -0
  57. package/src/react/components/tabs/useFranchiseLinksManage.js +215 -0
  58. package/src/react/components/tabs/useSalesmanManage.js +222 -0
  59. package/src/react/components/tabs/usersTabHelpers.js +221 -0
  60. package/src/react/domain/productSupplierFlow.js +37 -0
  61. package/src/react/index.js +4 -0
  62. package/src/react/pages/ClientDetailsSkeleton.js +84 -0
  63. package/src/react/pages/clientDetailsHelpers.js +17 -0
  64. package/src/react/pages/clients.js +27 -0
  65. package/src/react/pages/details.js +529 -0
  66. package/src/react/pages/details.page.styles.js +105 -0
  67. package/src/react/pages/details.styles.js +15 -0
  68. package/src/react/pages/employees.js +69 -0
  69. package/src/react/pages/franchisees.js +30 -0
  70. package/src/react/pages/prospects.js +21 -0
  71. package/src/react/pages/provider.js +21 -0
  72. package/src/react/router/routes.js +104 -0
  73. package/src/react/styles/details.js +571 -0
  74. package/src/store/people_categories/index.js +72 -0
  75. package/src/tests/browser/manager/client-details-create-user.spec.js +321 -0
  76. package/src/tests/browser/manager/client-details-employee-unlink.spec.js +215 -0
  77. package/src/tests/browser/manager/client-details-employees-refresh.spec.js +271 -0
  78. package/src/tests/browser/manager/client-details-enable.spec.js +241 -0
  79. package/src/tests/browser/manager/client-details-fiscal-tabs.spec.js +410 -0
  80. package/src/tests/browser/manager/client-details-franchise-commission.spec.js +318 -0
  81. package/src/tests/browser/manager/client-details-franchise-links.spec.js +365 -0
  82. package/src/tests/browser/manager/client-details-media.spec.js +405 -0
  83. package/src/tests/browser/manager/client-details-relationships.spec.js +313 -0
  84. package/src/tests/browser/manager/client-details-salesman-commission.spec.js +291 -0
  85. package/src/tests/browser/manager/client-details-salesman-manage.spec.js +448 -0
  86. package/src/tests/react/components/tabs/contactMutationMessage.test.js +25 -0
  87. package/src/tests/react/components/tabs/employeeContacts.test.js +266 -0
  88. package/src/tests/react/components/tabs/employeeUnlink.test.js +67 -0
  89. package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +78 -0
  90. package/src/tests/react/components/tabs/employeesTabHelpers.test.js +64 -0
  91. package/src/tests/react/components/tabs/fiscalTabContract.test.mjs +29 -0
  92. package/src/tests/react/components/tabs/franchiseCommissionHelpers.test.js +128 -0
  93. package/src/tests/react/components/tabs/franchiseLinksTab.helpers.test.mjs +166 -0
  94. package/src/tests/react/components/tabs/generalTabHelpers.test.js +130 -0
  95. package/src/tests/react/components/tabs/peopleMediaTabs.test.js +322 -0
  96. package/src/tests/react/components/tabs/salesmanTab.helpers.test.mjs +75 -0
  97. package/src/tests/react/components/tabs/salesmanTabHelpers.test.js +142 -0
  98. package/src/tests/react/components/tabs/salesmanTabHelpers.test.mjs +92 -0
  99. package/src/tests/react/components/tabs/usersTabHelpers.test.js +59 -0
  100. package/src/tests/react/domain/productSupplierFlow.test.mjs +55 -0
  101. package/src/tests/react/pages/clientDetailsHelpers.test.js +98 -0
  102. package/src/tests/react/pages/employees.test.js +44 -0
@@ -0,0 +1,601 @@
1
+ /**
2
+ * PeopleCategoriesPanel — badges + CRUD for person classification timeline.
3
+ * PF: profession, position (cargo requires people_company_id)
4
+ * PJ: sector, activity_branch
5
+ * Issue: ControleOnline/app-community#377
6
+ */
7
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
8
+ import {
9
+ ActivityIndicator,
10
+ Modal,
11
+ Platform,
12
+ ScrollView,
13
+ Text,
14
+ TextInput,
15
+ TouchableOpacity,
16
+ View,
17
+ } from 'react-native';
18
+ import { Picker } from '@react-native-picker/picker';
19
+ import Icon from 'react-native-vector-icons/MaterialIcons';
20
+ import { useStore } from '@store';
21
+ import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
22
+
23
+ const PF_CONTEXTS = [
24
+ { value: 'profession', label: 'Profissão' },
25
+ { value: 'position', label: 'Cargo' },
26
+ ];
27
+ const PJ_CONTEXTS = [
28
+ { value: 'sector', label: 'Setor' },
29
+ { value: 'activity_branch', label: 'Ramo de Atividade' },
30
+ ];
31
+
32
+ const CONTEXT_LABELS = {
33
+ profession: 'Profissão',
34
+ position: 'Cargo',
35
+ sector: 'Setor',
36
+ activity_branch: 'Ramo de Atividade',
37
+ };
38
+
39
+ function extractId(value) {
40
+ if (value == null) return null;
41
+ if (typeof value === 'number') return value;
42
+ if (typeof value === 'string') {
43
+ const m = value.match(/\/(\d+)\s*$/);
44
+ if (m) return Number(m[1]);
45
+ const n = Number(value);
46
+ return Number.isFinite(n) ? n : null;
47
+ }
48
+ if (typeof value === 'object') {
49
+ if (value.id != null) return extractId(value.id);
50
+ if (value['@id']) return extractId(value['@id']);
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function toPeopleIri(client) {
56
+ const id = extractId(client);
57
+ return id != null ? `/people/${id}` : null;
58
+ }
59
+
60
+ function formatDateBr(ymd) {
61
+ if (!ymd) return '';
62
+ const s = String(ymd).slice(0, 10);
63
+ const [y, m, d] = s.split('-');
64
+ if (!y || !m || !d) return s;
65
+ return `${d}/${m}/${y}`;
66
+ }
67
+
68
+ function parseBrToYmd(br) {
69
+ if (!br) return null;
70
+ const parts = String(br).trim().split(/[/\-.]/);
71
+ if (parts.length !== 3) return null;
72
+ let d, m, y;
73
+ if (parts[0].length === 4) {
74
+ [y, m, d] = parts;
75
+ } else {
76
+ [d, m, y] = parts;
77
+ }
78
+ if (!y || !m || !d) return null;
79
+ return `${y.padStart(4, '0')}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
80
+ }
81
+
82
+ function categoryName(item) {
83
+ const c = item?.category;
84
+ if (!c) return '—';
85
+ if (typeof c === 'string') return c;
86
+ return c.name || c.label || `#${c.id || ''}`;
87
+ }
88
+
89
+ function categoryContext(item) {
90
+ const c = item?.category;
91
+ if (!c || typeof c === 'string') return '';
92
+ return String(c.context || '').trim();
93
+ }
94
+
95
+ function badgeColor(context, themeColors) {
96
+ const map = {
97
+ profession: '#3B82F6',
98
+ position: '#8B5CF6',
99
+ sector: '#10B981',
100
+ activity_branch: '#F59E0B',
101
+ };
102
+ return map[context] || themeColors?.primary || '#64748B';
103
+ }
104
+
105
+ const emptyForm = () => ({
106
+ context: '',
107
+ categoryId: '',
108
+ categoryName: '',
109
+ startDateBr: '',
110
+ endDateBr: '',
111
+ peopleCompanyId: '',
112
+ });
113
+
114
+ const PeopleCategoriesPanel = ({
115
+ client,
116
+ isEditing = false,
117
+ parentCompanyIri = null,
118
+ }) => {
119
+ const { showError, showSuccess } = useMessage();
120
+ const themeStore = useStore('theme');
121
+ const themeColors = themeStore?.getters?.colors || {};
122
+ const categoriesStore = useStore('categories');
123
+ const peopleCategoriesStore = useStore('people_categories');
124
+
125
+ const peopleType = String(client?.peopleType || 'J').toUpperCase();
126
+ const isPF = peopleType === 'F';
127
+ const contexts = isPF ? PF_CONTEXTS : PJ_CONTEXTS;
128
+ const peopleIri = useMemo(() => toPeopleIri(client), [client]);
129
+ const peopleId = extractId(client);
130
+
131
+ const [items, setItems] = useState([]);
132
+ const [loading, setLoading] = useState(false);
133
+ const [modalVisible, setModalVisible] = useState(false);
134
+ const [editingItem, setEditingItem] = useState(null);
135
+ const [form, setForm] = useState(emptyForm());
136
+ const [saving, setSaving] = useState(false);
137
+ const [categoryOptions, setCategoryOptions] = useState([]);
138
+ const [loadingCategories, setLoadingCategories] = useState(false);
139
+
140
+ const loadItems = useCallback(async () => {
141
+ if (!peopleId || !peopleCategoriesStore?.actions?.getItems) return;
142
+ setLoading(true);
143
+ try {
144
+ const result = await peopleCategoriesStore.actions.getItems({
145
+ people: peopleIri,
146
+ itemsPerPage: 100,
147
+ });
148
+ const list = Array.isArray(result)
149
+ ? result
150
+ : result?.['hydra:member'] || result?.items || peopleCategoriesStore.getters?.items || [];
151
+ setItems(Array.isArray(list) ? list : []);
152
+ } catch (err) {
153
+ console.warn('[PeopleCategoriesPanel] load failed', err);
154
+ setItems([]);
155
+ } finally {
156
+ setLoading(false);
157
+ }
158
+ }, [peopleId, peopleIri, peopleCategoriesStore]);
159
+
160
+ useEffect(() => {
161
+ loadItems();
162
+ }, [loadItems]);
163
+
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 {
184
+ setLoadingCategories(false);
185
+ }
186
+ },
187
+ [categoriesStore],
188
+ );
189
+
190
+ useEffect(() => {
191
+ if (form.context) {
192
+ loadCategoryOptions(form.context);
193
+ } else {
194
+ setCategoryOptions([]);
195
+ }
196
+ }, [form.context, loadCategoryOptions]);
197
+
198
+ const openCreate = () => {
199
+ setEditingItem(null);
200
+ const defaultContext = contexts[0]?.value || '';
201
+ setForm({
202
+ ...emptyForm(),
203
+ context: defaultContext,
204
+ startDateBr: formatDateBr(new Date().toISOString().slice(0, 10)),
205
+ });
206
+ setModalVisible(true);
207
+ };
208
+
209
+ const openEdit = item => {
210
+ setEditingItem(item);
211
+ const ctx = categoryContext(item);
212
+ const catId = extractId(item.category);
213
+ setForm({
214
+ context: ctx,
215
+ categoryId: catId != null ? String(catId) : '',
216
+ categoryName: categoryName(item),
217
+ startDateBr: formatDateBr(item.startDate),
218
+ endDateBr: formatDateBr(item.endDate),
219
+ peopleCompanyId: extractId(item.peopleCompany) != null ? String(extractId(item.peopleCompany)) : '',
220
+ });
221
+ setModalVisible(true);
222
+ };
223
+
224
+ const closeModal = () => {
225
+ setModalVisible(false);
226
+ setEditingItem(null);
227
+ setForm(emptyForm());
228
+ };
229
+
230
+ const handleSave = async () => {
231
+ if (!peopleIri) {
232
+ showError?.('Pessoa inválida');
233
+ return;
234
+ }
235
+ const startYmd = parseBrToYmd(form.startDateBr);
236
+ if (!startYmd) {
237
+ showError?.('Data início obrigatória (DD/MM/AAAA)');
238
+ return;
239
+ }
240
+ if (!form.categoryId && !form.categoryName) {
241
+ showError?.('Selecione ou informe a categoria');
242
+ return;
243
+ }
244
+ if (form.context === 'position' && !form.peopleCompanyId && !parentCompanyIri) {
245
+ showError?.('Cargo exige empresa vinculada (people_company_id)');
246
+ return;
247
+ }
248
+
249
+ setSaving(true);
250
+ try {
251
+ let categoryIri = form.categoryId ? `/categories/${form.categoryId}` : null;
252
+
253
+ if (!categoryIri && form.categoryName && categoriesStore?.actions?.save) {
254
+ const created = await categoriesStore.actions.save({
255
+ name: form.categoryName.trim(),
256
+ context: form.context,
257
+ });
258
+ const newId = extractId(created);
259
+ if (newId) categoryIri = `/categories/${newId}`;
260
+ }
261
+
262
+ if (!categoryIri) {
263
+ showError?.('Categoria obrigatória');
264
+ setSaving(false);
265
+ return;
266
+ }
267
+
268
+ const payload = {
269
+ people: peopleIri,
270
+ category: categoryIri,
271
+ startDate: startYmd,
272
+ endDate: parseBrToYmd(form.endDateBr) || null,
273
+ active: true,
274
+ };
275
+
276
+ if (form.context === 'position') {
277
+ const companyId = form.peopleCompanyId || extractId(parentCompanyIri);
278
+ if (companyId) {
279
+ payload.peopleCompany = `/people/${companyId}`;
280
+ }
281
+ }
282
+
283
+ if (editingItem?.id && peopleCategoriesStore?.actions?.save) {
284
+ await peopleCategoriesStore.actions.save({
285
+ ...payload,
286
+ id: editingItem.id,
287
+ });
288
+ showSuccess?.('Categoria atualizada');
289
+ } else if (peopleCategoriesStore?.actions?.save) {
290
+ await peopleCategoriesStore.actions.save(payload);
291
+ showSuccess?.('Categoria adicionada');
292
+ } else {
293
+ showError?.('Store people_categories indisponível');
294
+ setSaving(false);
295
+ return;
296
+ }
297
+
298
+ closeModal();
299
+ await loadItems();
300
+ } catch (err) {
301
+ console.error('[PeopleCategoriesPanel] save failed', err);
302
+ showError?.(err?.message || 'Falha ao salvar categoria');
303
+ } finally {
304
+ setSaving(false);
305
+ }
306
+ };
307
+
308
+ const handleDelete = async item => {
309
+ if (!item?.id || !peopleCategoriesStore?.actions?.remove) return;
310
+ setSaving(true);
311
+ try {
312
+ await peopleCategoriesStore.actions.remove(item.id);
313
+ showSuccess?.('Categoria removida');
314
+ await loadItems();
315
+ } catch (err) {
316
+ showError?.(err?.message || 'Falha ao remover');
317
+ } finally {
318
+ setSaving(false);
319
+ }
320
+ };
321
+
322
+ const styles = useMemo(
323
+ () => ({
324
+ section: { marginTop: 16, marginBottom: 8, paddingHorizontal: 4 },
325
+ headerRow: {
326
+ flexDirection: 'row',
327
+ alignItems: 'center',
328
+ justifyContent: 'space-between',
329
+ marginBottom: 10,
330
+ },
331
+ title: {
332
+ fontSize: 15,
333
+ fontWeight: '600',
334
+ color: themeColors?.text || '#1E293B',
335
+ },
336
+ badgesWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
337
+ badge: {
338
+ flexDirection: 'row',
339
+ alignItems: 'center',
340
+ paddingHorizontal: 10,
341
+ paddingVertical: 6,
342
+ borderRadius: 16,
343
+ marginRight: 6,
344
+ marginBottom: 6,
345
+ },
346
+ badgeText: { color: '#fff', fontSize: 12, fontWeight: '600', maxWidth: 160 },
347
+ badgeMeta: { color: 'rgba(255,255,255,0.85)', fontSize: 10, marginLeft: 6 },
348
+ empty: {
349
+ color: themeColors?.textSecondary || '#64748B',
350
+ fontSize: 13,
351
+ fontStyle: 'italic',
352
+ },
353
+ addBtn: {
354
+ flexDirection: 'row',
355
+ alignItems: 'center',
356
+ paddingHorizontal: 10,
357
+ paddingVertical: 6,
358
+ borderRadius: 8,
359
+ backgroundColor: themeColors?.primary || '#3B82F6',
360
+ },
361
+ addBtnText: { color: '#fff', fontSize: 12, fontWeight: '600', marginLeft: 4 },
362
+ modalOverlay: {
363
+ flex: 1,
364
+ backgroundColor: 'rgba(0,0,0,0.45)',
365
+ justifyContent: 'center',
366
+ padding: 20,
367
+ },
368
+ modalCard: {
369
+ backgroundColor: themeColors?.surface || '#fff',
370
+ borderRadius: 12,
371
+ padding: 16,
372
+ maxHeight: '85%',
373
+ },
374
+ modalTitle: {
375
+ fontSize: 16,
376
+ fontWeight: '700',
377
+ color: themeColors?.text || '#1E293B',
378
+ marginBottom: 12,
379
+ },
380
+ fieldLabel: {
381
+ fontSize: 12,
382
+ fontWeight: '600',
383
+ color: themeColors?.textSecondary || '#64748B',
384
+ marginBottom: 4,
385
+ marginTop: 10,
386
+ },
387
+ input: {
388
+ borderWidth: 1,
389
+ borderColor: themeColors?.border || '#E2E8F0',
390
+ borderRadius: 8,
391
+ paddingHorizontal: 10,
392
+ paddingVertical: Platform.OS === 'ios' ? 10 : 6,
393
+ color: themeColors?.text || '#1E293B',
394
+ fontSize: 14,
395
+ },
396
+ actionsRow: {
397
+ flexDirection: 'row',
398
+ justifyContent: 'flex-end',
399
+ marginTop: 16,
400
+ gap: 10,
401
+ },
402
+ btnSecondary: {
403
+ paddingHorizontal: 14,
404
+ paddingVertical: 10,
405
+ borderRadius: 8,
406
+ borderWidth: 1,
407
+ borderColor: themeColors?.border || '#E2E8F0',
408
+ },
409
+ btnPrimary: {
410
+ paddingHorizontal: 14,
411
+ paddingVertical: 10,
412
+ borderRadius: 8,
413
+ backgroundColor: themeColors?.primary || '#3B82F6',
414
+ },
415
+ btnText: { fontSize: 13, fontWeight: '600' },
416
+ }),
417
+ [themeColors],
418
+ );
419
+
420
+ if (!peopleId) return null;
421
+
422
+ return (
423
+ <View style={styles.section}>
424
+ <View style={styles.headerRow}>
425
+ <Text style={styles.title}>Classificação / Categorias</Text>
426
+ {isEditing && (
427
+ <TouchableOpacity style={styles.addBtn} onPress={openCreate} activeOpacity={0.85}>
428
+ <Icon name="add" size={16} color="#fff" />
429
+ <Text style={styles.addBtnText}>Adicionar</Text>
430
+ </TouchableOpacity>
431
+ )}
432
+ </View>
433
+
434
+ {loading ? (
435
+ <ActivityIndicator size="small" color={themeColors?.primary} />
436
+ ) : items.length === 0 ? (
437
+ <Text style={styles.empty}>Nenhuma categoria associada</Text>
438
+ ) : (
439
+ <View style={styles.badgesWrap}>
440
+ {items.map(item => {
441
+ const ctx = categoryContext(item);
442
+ const bg = badgeColor(ctx, themeColors);
443
+ const label = categoryName(item);
444
+ const period = [
445
+ formatDateBr(item.startDate),
446
+ item.endDate ? formatDateBr(item.endDate) : 'atual',
447
+ ]
448
+ .filter(Boolean)
449
+ .join(' → ');
450
+ return (
451
+ <TouchableOpacity
452
+ key={item.id || `${ctx}-${label}`}
453
+ style={[styles.badge, { backgroundColor: bg }]}
454
+ onPress={() => isEditing && openEdit(item)}
455
+ onLongPress={() => isEditing && handleDelete(item)}
456
+ activeOpacity={isEditing ? 0.8 : 1}
457
+ >
458
+ <Text style={styles.badgeText} numberOfLines={1}>
459
+ {CONTEXT_LABELS[ctx] ? `${CONTEXT_LABELS[ctx]}: ` : ''}
460
+ {label}
461
+ </Text>
462
+ {period ? <Text style={styles.badgeMeta}>{period}</Text> : null}
463
+ </TouchableOpacity>
464
+ );
465
+ })}
466
+ </View>
467
+ )}
468
+
469
+ <Modal visible={modalVisible} transparent animationType="fade" onRequestClose={closeModal}>
470
+ <View style={styles.modalOverlay}>
471
+ <View style={styles.modalCard}>
472
+ <ScrollView keyboardShouldPersistTaps="handled">
473
+ <Text style={styles.modalTitle}>
474
+ {editingItem ? 'Editar categoria' : 'Nova categoria'}
475
+ </Text>
476
+
477
+ <Text style={styles.fieldLabel}>Tipo</Text>
478
+ <View style={styles.input}>
479
+ <Picker
480
+ selectedValue={form.context}
481
+ onValueChange={value =>
482
+ setForm(prev => ({
483
+ ...prev,
484
+ context: value,
485
+ categoryId: '',
486
+ categoryName: '',
487
+ }))
488
+ }
489
+ >
490
+ {contexts.map(c => (
491
+ <Picker.Item key={c.value} label={c.label} value={c.value} />
492
+ ))}
493
+ </Picker>
494
+ </View>
495
+
496
+ <Text style={styles.fieldLabel}>Categoria</Text>
497
+ {loadingCategories ? (
498
+ <ActivityIndicator size="small" />
499
+ ) : categoryOptions.length > 0 ? (
500
+ <View style={styles.input}>
501
+ <Picker
502
+ selectedValue={form.categoryId}
503
+ onValueChange={value => {
504
+ const opt = categoryOptions.find(
505
+ o => String(extractId(o)) === String(value),
506
+ );
507
+ setForm(prev => ({
508
+ ...prev,
509
+ categoryId: value,
510
+ categoryName: opt?.name || prev.categoryName,
511
+ }));
512
+ }}
513
+ >
514
+ <Picker.Item label="— selecione —" value="" />
515
+ {categoryOptions.map(opt => {
516
+ const id = extractId(opt);
517
+ return (
518
+ <Picker.Item
519
+ key={id}
520
+ label={opt.name || `#${id}`}
521
+ value={String(id)}
522
+ />
523
+ );
524
+ })}
525
+ </Picker>
526
+ </View>
527
+ ) : (
528
+ <TextInput
529
+ style={styles.input}
530
+ placeholder="Nome da nova categoria"
531
+ value={form.categoryName}
532
+ onChangeText={text =>
533
+ setForm(prev => ({ ...prev, categoryName: text, categoryId: '' }))
534
+ }
535
+ placeholderTextColor={themeColors?.textSecondary}
536
+ />
537
+ )}
538
+
539
+ <Text style={styles.fieldLabel}>Data início (DD/MM/AAAA)</Text>
540
+ <TextInput
541
+ style={styles.input}
542
+ value={form.startDateBr}
543
+ onChangeText={text => setForm(prev => ({ ...prev, startDateBr: text }))}
544
+ placeholder="01/01/2024"
545
+ placeholderTextColor={themeColors?.textSecondary}
546
+ />
547
+
548
+ <Text style={styles.fieldLabel}>Data fim (opcional)</Text>
549
+ <TextInput
550
+ style={styles.input}
551
+ value={form.endDateBr}
552
+ onChangeText={text => setForm(prev => ({ ...prev, endDateBr: text }))}
553
+ placeholder="vazio = atual"
554
+ placeholderTextColor={themeColors?.textSecondary}
555
+ />
556
+
557
+ {form.context === 'position' && (
558
+ <>
559
+ <Text style={styles.fieldLabel}>Empresa do cargo (people_company_id)</Text>
560
+ <TextInput
561
+ style={styles.input}
562
+ value={form.peopleCompanyId}
563
+ onChangeText={text =>
564
+ setForm(prev => ({ ...prev, peopleCompanyId: text }))
565
+ }
566
+ placeholder={
567
+ parentCompanyIri
568
+ ? `Padrão: ${extractId(parentCompanyIri)}`
569
+ : 'ID da empresa'
570
+ }
571
+ placeholderTextColor={themeColors?.textSecondary}
572
+ keyboardType="numeric"
573
+ />
574
+ </>
575
+ )}
576
+
577
+ <View style={styles.actionsRow}>
578
+ <TouchableOpacity style={styles.btnSecondary} onPress={closeModal}>
579
+ <Text style={[styles.btnText, { color: themeColors?.text }]}>Cancelar</Text>
580
+ </TouchableOpacity>
581
+ <TouchableOpacity
582
+ style={styles.btnPrimary}
583
+ onPress={handleSave}
584
+ disabled={saving}
585
+ >
586
+ {saving ? (
587
+ <ActivityIndicator size="small" color="#fff" />
588
+ ) : (
589
+ <Text style={[styles.btnText, { color: '#fff' }]}>Salvar</Text>
590
+ )}
591
+ </TouchableOpacity>
592
+ </View>
593
+ </ScrollView>
594
+ </View>
595
+ </View>
596
+ </Modal>
597
+ </View>
598
+ );
599
+ };
600
+
601
+ export default PeopleCategoriesPanel;