@controleonline/ui-customers 1.0.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/agents/developer.agent.md +30 -0
- package/.github/agents/devops.agent.md +30 -0
- package/.github/agents/qa.agent.md +30 -0
- package/.github/agents/security.agent.md +30 -0
- package/.scrutinizer.yml +20 -0
- package/AGENTS.md +50 -0
- package/FUNDING.yml +1 -0
- package/README.md +23 -0
- package/docs/technical/Client-Details-Franquia-Filial.md +18 -0
- package/docs/technical/Client-Details-People-Name-Case.md +103 -0
- package/docs/technical/Cliente-Vendedor-Vinculo-e-Permissoes.md +152 -0
- package/docs/wiki.md +1 -0
- package/package.json +32 -0
- package/scripts/validate-wiki-pointer.cjs +13 -0
- package/src/react/components/tabs/AddressesTab.js +494 -0
- package/src/react/components/tabs/CategoriesTab.js +196 -0
- package/src/react/components/tabs/ClassificationChips.js +494 -0
- package/src/react/components/tabs/ContactTab.js +621 -0
- package/src/react/components/tabs/ContactTab.styles.js +119 -0
- package/src/react/components/tabs/ContractsTab.js +13 -0
- package/src/react/components/tabs/DocumentsTab.js +463 -0
- package/src/react/components/tabs/DocumentsTab.styles.js +133 -0
- package/src/react/components/tabs/EmployeeContactRow.js +83 -0
- package/src/react/components/tabs/EmployeesTab.js +454 -0
- package/src/react/components/tabs/EmployeesTab.styles.js +172 -0
- package/src/react/components/tabs/FiscalTab.js +6 -0
- package/src/react/components/tabs/FranchiseCommissionSection.js +307 -0
- package/src/react/components/tabs/FranchiseLinksManageModal.js +215 -0
- package/src/react/components/tabs/FranchiseLinksTab.js +282 -0
- package/src/react/components/tabs/GeneralTab.js +466 -0
- package/src/react/components/tabs/GeneralTab.styles.js +101 -0
- package/src/react/components/tabs/MediaTab.js +356 -0
- package/src/react/components/tabs/PeopleCategoriesPanel.js +601 -0
- package/src/react/components/tabs/ProductsTab.js +236 -0
- package/src/react/components/tabs/ProductsTab.styles.js +143 -0
- package/src/react/components/tabs/SalesmanCommissionBlock.js +120 -0
- package/src/react/components/tabs/SalesmanManageModal.js +208 -0
- package/src/react/components/tabs/SalesmanTab.js +436 -0
- package/src/react/components/tabs/SalesmanTab.styles.js +4 -0
- package/src/react/components/tabs/UserApiKeyModal.js +90 -0
- package/src/react/components/tabs/UserFormModal.js +301 -0
- package/src/react/components/tabs/UsersTab.js +426 -0
- package/src/react/components/tabs/UsersTab.styles.js +283 -0
- package/src/react/components/tabs/UsersTab.styles.test.js +42 -0
- package/src/react/components/tabs/contactMutationMessage.js +18 -0
- package/src/react/components/tabs/employeeContacts.js +214 -0
- package/src/react/components/tabs/employeeUnlink.js +68 -0
- package/src/react/components/tabs/employeesTabHelpers.js +227 -0
- package/src/react/components/tabs/franchiseCommissionHelpers.js +134 -0
- package/src/react/components/tabs/franchiseLinksTab.helpers.js +247 -0
- package/src/react/components/tabs/generalTabHelpers.js +120 -0
- package/src/react/components/tabs/salesmanTab.helpers.js +114 -0
- package/src/react/components/tabs/salesmanTabHelpers.js +173 -0
- package/src/react/components/tabs/salesmanTabMedia.js +55 -0
- package/src/react/components/tabs/salesmanTabSession.js +39 -0
- package/src/react/components/tabs/useEmployeeUnlink.js +58 -0
- package/src/react/components/tabs/useFranchiseLinksManage.js +215 -0
- package/src/react/components/tabs/useSalesmanManage.js +222 -0
- package/src/react/components/tabs/usersTabHelpers.js +221 -0
- package/src/react/domain/productSupplierFlow.js +37 -0
- package/src/react/index.js +4 -0
- package/src/react/pages/ClientDetailsSkeleton.js +84 -0
- package/src/react/pages/clientDetailsHelpers.js +17 -0
- package/src/react/pages/clients.js +27 -0
- package/src/react/pages/details.js +529 -0
- package/src/react/pages/details.page.styles.js +105 -0
- package/src/react/pages/details.styles.js +15 -0
- package/src/react/pages/employees.js +69 -0
- package/src/react/pages/franchisees.js +30 -0
- package/src/react/pages/prospects.js +21 -0
- package/src/react/pages/provider.js +21 -0
- package/src/react/router/routes.js +104 -0
- package/src/react/styles/details.js +571 -0
- package/src/store/people_categories/index.js +72 -0
- package/src/tests/browser/manager/client-details-create-user.spec.js +321 -0
- package/src/tests/browser/manager/client-details-employee-unlink.spec.js +215 -0
- package/src/tests/browser/manager/client-details-employees-refresh.spec.js +271 -0
- package/src/tests/browser/manager/client-details-enable.spec.js +241 -0
- package/src/tests/browser/manager/client-details-fiscal-tabs.spec.js +410 -0
- package/src/tests/browser/manager/client-details-franchise-commission.spec.js +318 -0
- package/src/tests/browser/manager/client-details-franchise-links.spec.js +365 -0
- package/src/tests/browser/manager/client-details-media.spec.js +405 -0
- package/src/tests/browser/manager/client-details-relationships.spec.js +313 -0
- package/src/tests/browser/manager/client-details-salesman-commission.spec.js +291 -0
- package/src/tests/browser/manager/client-details-salesman-manage.spec.js +448 -0
- package/src/tests/react/components/tabs/contactMutationMessage.test.js +25 -0
- package/src/tests/react/components/tabs/employeeContacts.test.js +266 -0
- package/src/tests/react/components/tabs/employeeUnlink.test.js +67 -0
- package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +78 -0
- package/src/tests/react/components/tabs/employeesTabHelpers.test.js +64 -0
- package/src/tests/react/components/tabs/fiscalTabContract.test.mjs +29 -0
- package/src/tests/react/components/tabs/franchiseCommissionHelpers.test.js +128 -0
- package/src/tests/react/components/tabs/franchiseLinksTab.helpers.test.mjs +166 -0
- package/src/tests/react/components/tabs/generalTabHelpers.test.js +130 -0
- package/src/tests/react/components/tabs/peopleMediaTabs.test.js +322 -0
- package/src/tests/react/components/tabs/salesmanTab.helpers.test.mjs +75 -0
- package/src/tests/react/components/tabs/salesmanTabHelpers.test.js +142 -0
- package/src/tests/react/components/tabs/salesmanTabHelpers.test.mjs +92 -0
- package/src/tests/react/components/tabs/usersTabHelpers.test.js +59 -0
- package/src/tests/react/domain/productSupplierFlow.test.mjs +55 -0
- package/src/tests/react/pages/clientDetailsHelpers.test.js +98 -0
- package/src/tests/react/pages/employees.test.js +44 -0
|
@@ -0,0 +1,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;
|