@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,236 @@
1
+ import React, { useMemo, useCallback } from 'react';
2
+ import { Text, TouchableOpacity, View } from 'react-native';
3
+ import { useNavigation } from '@react-navigation/native';
4
+ import Icon from 'react-native-vector-icons/MaterialIcons';
5
+ import { colors } from '@controleonline/../../src/styles/colors';
6
+ import styles from './ProductsTab.styles';
7
+ import {
8
+ buildCreateProductParams,
9
+ buildInitialProviderPayload,
10
+ extractId,
11
+ } from '../../domain/productSupplierFlow';
12
+
13
+ const ROLE_LABELS = {
14
+ supplier: 'Fornecedor',
15
+ manufacturer: 'Fabricante',
16
+ distributor: 'Distribuidor',
17
+ };
18
+
19
+ const SUPPLY_TYPES = new Set(['component', 'feedstock', 'package']);
20
+
21
+ const formatMoney = value => {
22
+ if (value === null || value === undefined || value === '') return '';
23
+
24
+ const parsed = Number(String(value).replace(',', '.'));
25
+ if (!Number.isFinite(parsed)) {
26
+ return String(value);
27
+ }
28
+
29
+ try {
30
+ return parsed.toLocaleString('pt-BR', {
31
+ style: 'currency',
32
+ currency: 'BRL',
33
+ });
34
+ } catch (error) {
35
+ return `R$ ${parsed.toFixed(2)}`;
36
+ }
37
+ };
38
+
39
+ const resolveProductContext = product => {
40
+ return SUPPLY_TYPES.has(String(product?.type || '').toLowerCase()) ? 'supplies' : 'products';
41
+ };
42
+
43
+ const getProductName = product =>
44
+ String(product?.product || `Produto #${product?.id || ''}`).trim();
45
+
46
+ const getProductSubtitle = product => {
47
+ const parts = [];
48
+
49
+ if (product?.sku) {
50
+ parts.push(`SKU ${product.sku}`);
51
+ }
52
+
53
+ if (product?.type) {
54
+ parts.push(String(product.type).trim());
55
+ }
56
+
57
+ return parts.join(' | ');
58
+ };
59
+
60
+ const buildMeta = relation => {
61
+ const items = [];
62
+
63
+ if (relation?.costPrice) {
64
+ items.push({ label: 'Custo', value: formatMoney(relation.costPrice) });
65
+ }
66
+
67
+ if (relation?.supplierSku) {
68
+ items.push({ label: 'Cod. interno', value: relation.supplierSku });
69
+ }
70
+
71
+ if (relation?.leadTimeDays || relation?.leadTimeDays === 0) {
72
+ items.push({
73
+ label: 'Prazo',
74
+ value: `${relation.leadTimeDays} ${Number(relation.leadTimeDays) === 1 ? 'dia' : 'dias'}`,
75
+ });
76
+ }
77
+
78
+ if (relation?.priority || relation?.priority === 0) {
79
+ items.push({ label: 'Prioridade', value: String(relation.priority) });
80
+ }
81
+
82
+ return items;
83
+ };
84
+
85
+ const ProductsTab = ({ client, customStyles }) => {
86
+ const navigation = useNavigation();
87
+
88
+ const providerPayload = useMemo(
89
+ () => buildInitialProviderPayload(client),
90
+ [client?.id, client?.['@id'], client?.alias, client?.name, client?.peopleType],
91
+ );
92
+
93
+ const openCreateProduct = useCallback(() => {
94
+ const params = buildCreateProductParams(client);
95
+ if (!params) {
96
+ return;
97
+ }
98
+
99
+ navigation.push('ProductDetails', params);
100
+ }, [client, navigation]);
101
+
102
+ const relations = useMemo(() => {
103
+ const safeRelations = Array.isArray(client?.productPeople) ? [...client.productPeople] : [];
104
+
105
+ return safeRelations.sort((left, right) => {
106
+ const leftPriority = Number(left?.priority ?? 9999);
107
+ const rightPriority = Number(right?.priority ?? 9999);
108
+ if (leftPriority !== rightPriority) {
109
+ return leftPriority - rightPriority;
110
+ }
111
+
112
+ return getProductName(left?.product)
113
+ .toLowerCase()
114
+ .localeCompare(getProductName(right?.product).toLowerCase());
115
+ });
116
+ }, [client?.productPeople]);
117
+
118
+ const openProduct = relation => {
119
+ const productId = extractId(relation?.product?.id || relation?.product?.['@id']);
120
+ if (!productId) {
121
+ return;
122
+ }
123
+
124
+ navigation.push('ProductDetails', {
125
+ ProductId: productId,
126
+ context: resolveProductContext(relation?.product),
127
+ });
128
+ };
129
+
130
+ return (
131
+ <View style={customStyles.section}>
132
+ <View style={styles.sectionHeader}>
133
+ <View style={styles.sectionHeaderCopy}>
134
+ <Text style={customStyles.sectionTitle}>Produtos fornecidos</Text>
135
+ <Text style={styles.sectionSubtitle}>
136
+ {relations.length === 1
137
+ ? '1 produto vinculado a este fornecedor'
138
+ : `${relations.length} produtos vinculados a este fornecedor`}
139
+ </Text>
140
+ </View>
141
+
142
+ {providerPayload.id ? (
143
+ <TouchableOpacity
144
+ style={styles.addButton}
145
+ onPress={openCreateProduct}
146
+ activeOpacity={0.85}>
147
+ <Icon name="add" size={18} color="#FFFFFF" />
148
+ <Text style={styles.addButtonText}>Produto</Text>
149
+ </TouchableOpacity>
150
+ ) : null}
151
+ </View>
152
+
153
+ {relations.length === 0 ? (
154
+ <View style={styles.emptyState}>
155
+ <Text style={customStyles.emptyText}>
156
+ Nenhum produto vinculado a este fornecedor.
157
+ </Text>
158
+
159
+ {providerPayload.id ? (
160
+ <TouchableOpacity
161
+ style={styles.emptyActionButton}
162
+ onPress={openCreateProduct}
163
+ activeOpacity={0.85}>
164
+ <Icon name="add-box" size={18} color="#FFFFFF" />
165
+ <Text style={styles.emptyActionButtonText}>Adicionar produto</Text>
166
+ </TouchableOpacity>
167
+ ) : null}
168
+ </View>
169
+ ) : (
170
+ relations.map(relation => {
171
+ const product = relation?.product || {};
172
+ const metadata = buildMeta(relation);
173
+ const productId = extractId(product?.id || product?.['@id']);
174
+
175
+ return (
176
+ <TouchableOpacity
177
+ key={String(relation?.id || `${productId || 'product'}-${relation?.priority || 0}`)}
178
+ style={styles.card}
179
+ onPress={() => openProduct(relation)}
180
+ disabled={!productId}
181
+ activeOpacity={0.85}>
182
+ <View style={styles.cardHeader}>
183
+ <View style={styles.iconWrap}>
184
+ <Icon name="inventory-2" size={20} color={colors.primary} />
185
+ </View>
186
+
187
+ <View style={styles.cardBody}>
188
+ <Text style={styles.cardTitle} numberOfLines={1}>
189
+ {getProductName(product)}
190
+ </Text>
191
+
192
+ {getProductSubtitle(product) ? (
193
+ <Text style={styles.cardSubtitle} numberOfLines={1}>
194
+ {getProductSubtitle(product)}
195
+ </Text>
196
+ ) : null}
197
+ </View>
198
+
199
+ <Icon name="chevron-right" size={18} color="#94A3B8" />
200
+ </View>
201
+
202
+ <View style={styles.badgesRow}>
203
+ <View style={styles.primaryBadge}>
204
+ <Text style={styles.primaryBadgeText}>
205
+ {ROLE_LABELS[relation?.role] || relation?.role || 'Relacionamento'}
206
+ </Text>
207
+ </View>
208
+
209
+ {product?.price ? (
210
+ <View style={styles.secondaryBadge}>
211
+ <Text style={styles.secondaryBadgeText}>
212
+ {formatMoney(product.price)}
213
+ </Text>
214
+ </View>
215
+ ) : null}
216
+ </View>
217
+
218
+ {metadata.length > 0 ? (
219
+ <View style={styles.metaWrap}>
220
+ {metadata.map(item => (
221
+ <View key={`${relation?.id}-${item.label}`} style={styles.metaPill}>
222
+ <Text style={styles.metaLabel}>{item.label}</Text>
223
+ <Text style={styles.metaValue}>{item.value}</Text>
224
+ </View>
225
+ ))}
226
+ </View>
227
+ ) : null}
228
+ </TouchableOpacity>
229
+ );
230
+ })
231
+ )}
232
+ </View>
233
+ );
234
+ };
235
+
236
+ export default ProductsTab;
@@ -0,0 +1,143 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ const styles = StyleSheet.create({
4
+ sectionHeader: {
5
+ flexDirection: 'row',
6
+ alignItems: 'flex-start',
7
+ justifyContent: 'space-between',
8
+ marginBottom: 12,
9
+ },
10
+ sectionHeaderCopy: {
11
+ flex: 1,
12
+ minWidth: 0,
13
+ paddingRight: 12,
14
+ },
15
+ sectionSubtitle: {
16
+ marginTop: 4,
17
+ fontSize: 13,
18
+ color: '#64748B',
19
+ },
20
+ addButton: {
21
+ flexDirection: 'row',
22
+ alignItems: 'center',
23
+ gap: 6,
24
+ borderRadius: 999,
25
+ backgroundColor: '#2563EB',
26
+ paddingHorizontal: 12,
27
+ paddingVertical: 8,
28
+ },
29
+ addButtonText: {
30
+ fontSize: 13,
31
+ fontWeight: '700',
32
+ color: '#FFFFFF',
33
+ },
34
+ emptyState: {
35
+ marginTop: 4,
36
+ },
37
+ emptyActionButton: {
38
+ marginTop: 12,
39
+ alignSelf: 'flex-start',
40
+ flexDirection: 'row',
41
+ alignItems: 'center',
42
+ gap: 8,
43
+ borderRadius: 12,
44
+ backgroundColor: '#2563EB',
45
+ paddingHorizontal: 14,
46
+ paddingVertical: 10,
47
+ },
48
+ emptyActionButtonText: {
49
+ fontSize: 13,
50
+ fontWeight: '700',
51
+ color: '#FFFFFF',
52
+ },
53
+ card: {
54
+ borderWidth: 1,
55
+ borderColor: '#E2E8F0',
56
+ borderRadius: 14,
57
+ padding: 14,
58
+ backgroundColor: '#FFFFFF',
59
+ marginTop: 12,
60
+ },
61
+ cardHeader: {
62
+ flexDirection: 'row',
63
+ alignItems: 'center',
64
+ },
65
+ iconWrap: {
66
+ width: 44,
67
+ height: 44,
68
+ borderRadius: 14,
69
+ backgroundColor: '#EFF6FF',
70
+ alignItems: 'center',
71
+ justifyContent: 'center',
72
+ marginRight: 12,
73
+ },
74
+ cardBody: {
75
+ flex: 1,
76
+ minWidth: 0,
77
+ },
78
+ cardTitle: {
79
+ fontSize: 15,
80
+ fontWeight: '700',
81
+ color: '#0F172A',
82
+ },
83
+ cardSubtitle: {
84
+ marginTop: 2,
85
+ fontSize: 13,
86
+ color: '#64748B',
87
+ },
88
+ badgesRow: {
89
+ flexDirection: 'row',
90
+ flexWrap: 'wrap',
91
+ gap: 8,
92
+ marginTop: 12,
93
+ },
94
+ primaryBadge: {
95
+ paddingHorizontal: 10,
96
+ paddingVertical: 6,
97
+ borderRadius: 999,
98
+ backgroundColor: '#DBEAFE',
99
+ },
100
+ primaryBadgeText: {
101
+ fontSize: 12,
102
+ fontWeight: '700',
103
+ color: '#1D4ED8',
104
+ },
105
+ secondaryBadge: {
106
+ paddingHorizontal: 10,
107
+ paddingVertical: 6,
108
+ borderRadius: 999,
109
+ backgroundColor: '#F1F5F9',
110
+ },
111
+ secondaryBadgeText: {
112
+ fontSize: 12,
113
+ fontWeight: '600',
114
+ color: '#475569',
115
+ },
116
+ metaWrap: {
117
+ flexDirection: 'row',
118
+ flexWrap: 'wrap',
119
+ gap: 8,
120
+ marginTop: 12,
121
+ },
122
+ metaPill: {
123
+ minWidth: 112,
124
+ paddingHorizontal: 10,
125
+ paddingVertical: 8,
126
+ borderRadius: 12,
127
+ backgroundColor: '#F8FAFC',
128
+ },
129
+ metaLabel: {
130
+ fontSize: 11,
131
+ fontWeight: '600',
132
+ color: '#94A3B8',
133
+ textTransform: 'uppercase',
134
+ },
135
+ metaValue: {
136
+ marginTop: 3,
137
+ fontSize: 13,
138
+ fontWeight: '700',
139
+ color: '#0F172A',
140
+ },
141
+ });
142
+
143
+ export default styles;
@@ -0,0 +1,120 @@
1
+ import React from 'react';
2
+ import { Text, TextInput, TouchableOpacity, View } from 'react-native';
3
+ import FeatherIcon from 'react-native-vector-icons/Feather';
4
+ import {
5
+ extractId,
6
+ formatCommissionSubtitle,
7
+ resolveEffectiveCommission,
8
+ } from './salesmanTabHelpers';
9
+
10
+ const SalesmanCommissionBlock = ({
11
+ item,
12
+ displayCommission,
13
+ canEdit,
14
+ editingLinkId,
15
+ editForm,
16
+ setEditForm,
17
+ isSaving,
18
+ defaultLinksBySalesmanId,
19
+ openEdit,
20
+ cancelEdit,
21
+ saveEdit,
22
+ customStyles,
23
+ }) => {
24
+ if (!displayCommission) {
25
+ return null;
26
+ }
27
+
28
+ const salesmanId = extractId(item?.company?.id || item?.company?.['@id']);
29
+ const defaultLink = defaultLinksBySalesmanId[salesmanId] || null;
30
+ const effective = resolveEffectiveCommission(item, defaultLink);
31
+ const linkId = extractId(item?.id || item?.['@id']);
32
+ const isEditingThis = editingLinkId === linkId;
33
+
34
+ if (isEditingThis && canEdit) {
35
+ return (
36
+ <View style={{ marginTop: 6, gap: 6 }} testID={`salesman-commission-edit-${linkId}`}>
37
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
38
+ <Text style={customStyles.itemSubtext}>Comissão %</Text>
39
+ <TextInput
40
+ testID={`salesman-commission-input-${linkId}`}
41
+ value={editForm.comission}
42
+ onChangeText={text =>
43
+ setEditForm(prev => ({ ...prev, comission: text.replace(/[^\d.,]/g, '') }))
44
+ }
45
+ keyboardType="decimal-pad"
46
+ style={{
47
+ minWidth: 64,
48
+ borderWidth: 1,
49
+ borderColor: customStyles.itemSubtext?.color || '#D7E1EC',
50
+ borderRadius: 6,
51
+ paddingHorizontal: 8,
52
+ paddingVertical: 4,
53
+ color: customStyles.itemText?.color,
54
+ }}
55
+ />
56
+ <Text style={customStyles.itemSubtext}>Mín %</Text>
57
+ <TextInput
58
+ testID={`salesman-minimum-commission-input-${linkId}`}
59
+ value={editForm.minimumComission}
60
+ onChangeText={text =>
61
+ setEditForm(prev => ({
62
+ ...prev,
63
+ minimumComission: text.replace(/[^\d.,]/g, ''),
64
+ }))
65
+ }
66
+ keyboardType="decimal-pad"
67
+ style={{
68
+ minWidth: 64,
69
+ borderWidth: 1,
70
+ borderColor: customStyles.itemSubtext?.color || '#D7E1EC',
71
+ borderRadius: 6,
72
+ paddingHorizontal: 8,
73
+ paddingVertical: 4,
74
+ color: customStyles.itemText?.color,
75
+ }}
76
+ />
77
+ </View>
78
+ <View style={{ flexDirection: 'row', gap: 12 }}>
79
+ <TouchableOpacity
80
+ testID={`salesman-commission-save-${linkId}`}
81
+ onPress={() => saveEdit(item)}
82
+ disabled={isSaving}>
83
+ <Text style={[customStyles.itemSubtext, { fontWeight: '600' }]}>
84
+ {isSaving ? 'Salvando…' : 'Salvar'}
85
+ </Text>
86
+ </TouchableOpacity>
87
+ <TouchableOpacity testID={`salesman-commission-cancel-${linkId}`} onPress={cancelEdit}>
88
+ <Text style={customStyles.itemSubtext}>Cancelar</Text>
89
+ </TouchableOpacity>
90
+ </View>
91
+ </View>
92
+ );
93
+ }
94
+
95
+ return (
96
+ <View
97
+ style={{ marginTop: 4, flexDirection: 'row', alignItems: 'center', gap: 6 }}
98
+ testID={`salesman-commission-${linkId}`}>
99
+ <Text
100
+ style={customStyles.itemSubtext}
101
+ testID={`salesman-commission-label-${linkId}`}>
102
+ {formatCommissionSubtitle(effective)}
103
+ </Text>
104
+ {canEdit ? (
105
+ <TouchableOpacity
106
+ testID={`salesman-commission-edit-btn-${linkId}`}
107
+ onPress={() => openEdit(item)}
108
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
109
+ <FeatherIcon
110
+ name="edit-2"
111
+ size={14}
112
+ color={customStyles.iconButtonGhostIcon?.color || '#6C7787'}
113
+ />
114
+ </TouchableOpacity>
115
+ ) : null}
116
+ </View>
117
+ );
118
+ };
119
+
120
+ export default SalesmanCommissionBlock;
@@ -0,0 +1,208 @@
1
+ import React from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ ScrollView,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ View,
9
+ } from 'react-native';
10
+ import { Picker } from '@react-native-picker/picker';
11
+ import Icon from 'react-native-vector-icons/MaterialIcons';
12
+ import AnimatedModal from '@controleonline/ui-common/src/react/components/AnimatedModal';
13
+ import { colors } from '@controleonline/../../src/styles/colors';
14
+
15
+ const styles = {
16
+ modalCard: {
17
+ backgroundColor: '#FFFFFF',
18
+ borderRadius: 20,
19
+ padding: 20,
20
+ width: '100%',
21
+ maxWidth: 420,
22
+ alignSelf: 'center',
23
+ gap: 18,
24
+ },
25
+ modalHeader: {
26
+ flexDirection: 'row',
27
+ alignItems: 'center',
28
+ justifyContent: 'space-between',
29
+ },
30
+ modalTitle: {
31
+ fontSize: 18,
32
+ fontWeight: '700',
33
+ color: '#0F172A',
34
+ },
35
+ fieldGroup: {
36
+ gap: 8,
37
+ marginBottom: 12,
38
+ },
39
+ fieldLabel: {
40
+ fontSize: 14,
41
+ fontWeight: '600',
42
+ color: '#334155',
43
+ },
44
+ pickerContainer: {
45
+ borderWidth: 1,
46
+ borderColor: '#CBD5E1',
47
+ borderRadius: 12,
48
+ overflow: 'hidden',
49
+ backgroundColor: '#FFFFFF',
50
+ },
51
+ input: {
52
+ borderWidth: 1,
53
+ borderColor: '#CBD5E1',
54
+ borderRadius: 12,
55
+ paddingHorizontal: 14,
56
+ paddingVertical: 12,
57
+ fontSize: 15,
58
+ color: '#0F172A',
59
+ backgroundColor: '#FFFFFF',
60
+ },
61
+ modalActions: {
62
+ flexDirection: 'row',
63
+ gap: 12,
64
+ marginTop: 8,
65
+ },
66
+ secondaryButton: {
67
+ flex: 1,
68
+ paddingVertical: 14,
69
+ borderRadius: 12,
70
+ borderWidth: 1,
71
+ borderColor: '#94A3B8',
72
+ alignItems: 'center',
73
+ justifyContent: 'center',
74
+ },
75
+ secondaryButtonText: {
76
+ fontSize: 15,
77
+ fontWeight: '600',
78
+ color: '#475569',
79
+ },
80
+ primaryButton: {
81
+ flex: 1,
82
+ paddingVertical: 14,
83
+ borderRadius: 12,
84
+ backgroundColor: colors.primary,
85
+ alignItems: 'center',
86
+ justifyContent: 'center',
87
+ },
88
+ primaryButtonText: {
89
+ fontSize: 15,
90
+ fontWeight: '600',
91
+ color: '#FFFFFF',
92
+ },
93
+ };
94
+
95
+ /**
96
+ * Modal to link or edit a client↔salesman people_link (MANAGER only).
97
+ */
98
+ const SalesmanManageModal = ({
99
+ visible,
100
+ onClose,
101
+ editingLink,
102
+ formData,
103
+ setFormData,
104
+ salesmanOptions,
105
+ isSaving,
106
+ onSave,
107
+ }) => {
108
+ const title = editingLink ? 'Editar vendedor' : 'Vincular vendedor';
109
+
110
+ return (
111
+ <AnimatedModal
112
+ visible={visible}
113
+ onRequestClose={onClose}
114
+ style={{ paddingHorizontal: 20 }}>
115
+ <View style={styles.modalCard} testID="salesman-manage-modal">
116
+ <View style={styles.modalHeader}>
117
+ <Text style={styles.modalTitle} testID="salesman-manage-modal-title">
118
+ {title}
119
+ </Text>
120
+ <TouchableOpacity
121
+ onPress={onClose}
122
+ accessibilityLabel="Fechar"
123
+ testID="salesman-manage-modal-close">
124
+ <Icon name="close" size={20} color="#64748B" />
125
+ </TouchableOpacity>
126
+ </View>
127
+
128
+ <ScrollView keyboardShouldPersistTaps="handled">
129
+ <View style={styles.fieldGroup}>
130
+ <Text style={styles.fieldLabel}>Vendedor</Text>
131
+ <View style={styles.pickerContainer} testID="salesman-manage-picker">
132
+ <Picker
133
+ selectedValue={formData.sellerIri}
134
+ enabled={!editingLink}
135
+ testID="salesman-manage-seller-picker"
136
+ onValueChange={value =>
137
+ setFormData(previous => ({ ...previous, sellerIri: value }))
138
+ }>
139
+ <Picker.Item label="Selecione um vendedor" value="" />
140
+ {salesmanOptions.map(option => (
141
+ <Picker.Item
142
+ key={option.id}
143
+ label={option.alias ? `${option.name} (${option.alias})` : option.name}
144
+ value={option.iri}
145
+ />
146
+ ))}
147
+ </Picker>
148
+ </View>
149
+ </View>
150
+
151
+ <View style={styles.fieldGroup}>
152
+ <Text style={styles.fieldLabel}>Comissão (%)</Text>
153
+ <TextInput
154
+ style={styles.input}
155
+ keyboardType="decimal-pad"
156
+ value={String(formData.commission ?? '')}
157
+ onChangeText={value =>
158
+ setFormData(previous => ({ ...previous, commission: value }))
159
+ }
160
+ placeholder="0"
161
+ testID="salesman-manage-commission-input"
162
+ />
163
+ </View>
164
+
165
+ <View style={styles.fieldGroup}>
166
+ <Text style={styles.fieldLabel}>Comissão mínima (%)</Text>
167
+ <TextInput
168
+ style={styles.input}
169
+ keyboardType="decimal-pad"
170
+ value={String(formData.minimumCommission ?? '')}
171
+ onChangeText={value =>
172
+ setFormData(previous => ({
173
+ ...previous,
174
+ minimumCommission: value,
175
+ }))
176
+ }
177
+ placeholder="0"
178
+ testID="salesman-manage-minimum-input"
179
+ />
180
+ </View>
181
+ </ScrollView>
182
+
183
+ <View style={styles.modalActions}>
184
+ <TouchableOpacity
185
+ style={styles.secondaryButton}
186
+ onPress={onClose}
187
+ disabled={isSaving}
188
+ testID="salesman-manage-cancel-btn">
189
+ <Text style={styles.secondaryButtonText}>Cancelar</Text>
190
+ </TouchableOpacity>
191
+ <TouchableOpacity
192
+ style={styles.primaryButton}
193
+ onPress={onSave}
194
+ disabled={isSaving}
195
+ testID="salesman-manage-save-btn">
196
+ {isSaving ? (
197
+ <ActivityIndicator size="small" color="#FFFFFF" />
198
+ ) : (
199
+ <Text style={styles.primaryButtonText}>Salvar</Text>
200
+ )}
201
+ </TouchableOpacity>
202
+ </View>
203
+ </View>
204
+ </AnimatedModal>
205
+ );
206
+ };
207
+
208
+ export default SalesmanManageModal;