@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,172 @@
1
+ export const inlineStyle_243_18 = {
2
+ paddingVertical: 20,
3
+ alignItems: 'center',
4
+ };
5
+
6
+ export const inlineStyle_279_8 = {
7
+ justifyContent: 'flex-end',
8
+ };
9
+
10
+ export const inlineStyle_281_10 = {
11
+ backgroundColor: '#fff',
12
+ borderTopLeftRadius: 24,
13
+ borderTopRightRadius: 24,
14
+ maxHeight: '85%',
15
+ width: '100%',
16
+ shadowColor: '#000',
17
+
18
+ shadowOffset: {
19
+ width: 0,
20
+ height: -4,
21
+ },
22
+
23
+ shadowOpacity: 0.1,
24
+ shadowRadius: 12,
25
+ elevation: 10,
26
+ };
27
+
28
+ export const inlineStyle_294_12 = {
29
+ flexDirection: 'row',
30
+ alignItems: 'center',
31
+ justifyContent: 'space-between',
32
+ paddingHorizontal: 24,
33
+ paddingVertical: 20,
34
+ borderBottomWidth: 1,
35
+ borderBottomColor: '#F1F5F9',
36
+ };
37
+
38
+ export const inlineStyle_303_18 = {
39
+ fontSize: 20,
40
+ fontWeight: '700',
41
+ color: '#0F172A',
42
+ };
43
+
44
+ export const inlineStyle_308_14 = {
45
+ width: 32,
46
+ height: 32,
47
+ borderRadius: 16,
48
+ backgroundColor: '#F1F5F9',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ };
52
+
53
+ export const inlineStyle_321_12 = {
54
+ padding: 24,
55
+ };
56
+
57
+ export const inlineStyle_324_18 = {
58
+ marginBottom: 20,
59
+ };
60
+
61
+ export const inlineStyle_326_16 = {
62
+ fontSize: 16,
63
+ fontWeight: '600',
64
+ color: '#212529',
65
+ marginBottom: 8,
66
+ };
67
+
68
+ export const inlineStyle_338_16 = {
69
+ borderWidth: 1,
70
+ borderColor: '#e9ecef',
71
+ borderRadius: 12,
72
+ paddingHorizontal: 16,
73
+ paddingVertical: 12,
74
+ fontSize: 16,
75
+ backgroundColor: '#f8f9fa',
76
+ };
77
+
78
+ export const inlineStyle_351_18 = {
79
+ marginBottom: 20,
80
+ };
81
+
82
+ export const inlineStyle_353_16 = {
83
+ fontSize: 16,
84
+ fontWeight: '600',
85
+ color: '#212529',
86
+ marginBottom: 8,
87
+ };
88
+
89
+ export const inlineStyle_365_16 = {
90
+ borderWidth: 1,
91
+ borderColor: '#e9ecef',
92
+ borderRadius: 12,
93
+ paddingHorizontal: 16,
94
+ paddingVertical: 12,
95
+ fontSize: 16,
96
+ backgroundColor: '#f8f9fa',
97
+ };
98
+
99
+ export const inlineStyle_378_18 = {
100
+ marginBottom: 8,
101
+ };
102
+
103
+ export const inlineStyle_380_16 = {
104
+ fontSize: 16,
105
+ fontWeight: '600',
106
+ color: '#212529',
107
+ marginBottom: 8,
108
+ };
109
+
110
+ export const inlineStyle_389_16 = {
111
+ flexDirection: 'row',
112
+ alignItems: 'center',
113
+ borderWidth: 1,
114
+ borderColor: '#e9ecef',
115
+ borderRadius: 12,
116
+ paddingHorizontal: 16,
117
+ backgroundColor: '#f8f9fa',
118
+ };
119
+
120
+ export const inlineStyle_408_18 = {
121
+ flex: 1,
122
+ paddingVertical: 12,
123
+ paddingHorizontal: 12,
124
+ fontSize: 16,
125
+ color: '#212529',
126
+ };
127
+
128
+ export const inlineStyle_422_18 = {
129
+ marginBottom: 20,
130
+ };
131
+
132
+ export const inlineStyle_424_16 = {
133
+ fontSize: 16,
134
+ fontWeight: '600',
135
+ color: '#212529',
136
+ marginBottom: 8,
137
+ };
138
+
139
+ export const inlineStyle_433_16 = {
140
+ borderWidth: 1,
141
+ borderColor: '#e9ecef',
142
+ borderRadius: 12,
143
+ backgroundColor: '#f8f9fa',
144
+ overflow: 'hidden',
145
+ };
146
+
147
+ export const inlineStyle_446_18 = {
148
+ color: '#212529',
149
+ };
150
+
151
+ export const inlineStyle_460_12 = {
152
+ flexDirection: 'row',
153
+ padding: 20,
154
+ gap: 12,
155
+ borderTopWidth: 1,
156
+ borderTopColor: '#e9ecef',
157
+ };
158
+
159
+ export const inlineStyle_472_14 = {
160
+ flex: 1,
161
+ paddingVertical: 14,
162
+ borderRadius: 12,
163
+ borderWidth: 1,
164
+ borderColor: '#6c757d',
165
+ alignItems: 'center',
166
+ };
167
+
168
+ export const inlineStyle_480_20 = {
169
+ fontSize: 16,
170
+ fontWeight: '600',
171
+ color: '#6c757d',
172
+ };
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import FiscalCompanyConfig from '@controleonline/ui-accounting/src/react/components/fiscal/FiscalCompanyConfig';
3
+
4
+ export default function FiscalTab({ client, navigation }) {
5
+ return <FiscalCompanyConfig company={client} navigation={navigation} />;
6
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Franchise commission fields for client-details GeneralTab.
3
+ * Shown only when the viewed client is a franchisee of the main company
4
+ * (people_link linkType=franchisee). Editable for ROLE_SUPER / franchisor owner.
5
+ */
6
+ import React, {useEffect, useMemo, useState} from 'react';
7
+
8
+ import {
9
+ ActivityIndicator,
10
+ Modal,
11
+ Text,
12
+ TextInput,
13
+ TouchableOpacity,
14
+ View,
15
+ } from 'react-native';
16
+
17
+ import FeatherIcon from 'react-native-vector-icons/Feather';
18
+ import {useStore} from '@store';
19
+ import {useMessage} from '@controleonline/ui-common/src/react/components/MessageService';
20
+ import {
21
+ buildFranchiseeLinkParams,
22
+ canEditFranchiseCommission,
23
+ extractId,
24
+ formatMinimum,
25
+ formatPercent,
26
+ normalizeCommissionFields,
27
+ pickFranchiseeLink,
28
+ validateCommissionFields,
29
+ } from './franchiseCommissionHelpers';
30
+ import {createGeneralTabStyles} from './GeneralTab.styles';
31
+
32
+ const FranchiseCommissionSection = ({
33
+ client,
34
+ parentCompanyIri = '',
35
+ isEditing = false,
36
+ }) => {
37
+ const {showError, showSuccess} = useMessage();
38
+ const peopleLinkStore = useStore('people_link');
39
+ const peopleStore = useStore('people');
40
+ const authStore = useStore('auth');
41
+ const themeStore = useStore('theme');
42
+ const themeColors = themeStore.getters.colors;
43
+ const styles = useMemo(() => createGeneralTabStyles(themeColors), [themeColors]);
44
+
45
+ const peopleLinkActions = peopleLinkStore?.actions || {};
46
+ const currentCompany = peopleStore?.getters?.currentCompany || null;
47
+ const authUser = authStore?.getters?.user || null;
48
+
49
+ const clientId = extractId(client?.id || client?.['@id']);
50
+ const franchisorId =
51
+ extractId(parentCompanyIri) ||
52
+ extractId(currentCompany?.id || currentCompany?.['@id']);
53
+
54
+ const [link, setLink] = useState(null);
55
+ const [isLoading, setIsLoading] = useState(false);
56
+ const [isSaving, setIsSaving] = useState(false);
57
+ const [modalVisible, setModalVisible] = useState(false);
58
+ const [form, setForm] = useState({comission: '', minimum_comission: ''});
59
+
60
+ const editable = canEditFranchiseCommission({
61
+ user: authUser,
62
+ currentCompany,
63
+ });
64
+
65
+ useEffect(() => {
66
+ if (!clientId || !franchisorId || !peopleLinkActions?.getItems) {
67
+ setLink(null);
68
+ return undefined;
69
+ }
70
+
71
+ let cancelled = false;
72
+ setIsLoading(true);
73
+
74
+ peopleLinkActions
75
+ .getItems(
76
+ buildFranchiseeLinkParams({
77
+ peopleId: clientId,
78
+ companyId: franchisorId,
79
+ }),
80
+ )
81
+ .then(items => {
82
+ if (cancelled) {
83
+ return;
84
+ }
85
+ const next = pickFranchiseeLink(items, {
86
+ peopleId: clientId,
87
+ companyId: franchisorId,
88
+ });
89
+ setLink(next);
90
+ })
91
+ .catch(() => {
92
+ if (!cancelled) {
93
+ setLink(null);
94
+ }
95
+ })
96
+ .finally(() => {
97
+ if (!cancelled) {
98
+ setIsLoading(false);
99
+ }
100
+ });
101
+
102
+ return () => {
103
+ cancelled = true;
104
+ };
105
+ }, [clientId, franchisorId, peopleLinkActions]);
106
+
107
+ if (isLoading) {
108
+ return (
109
+ <View style={styles.fieldGroupLarge}>
110
+ <ActivityIndicator size="small" color={themeColors.primary} />
111
+ </View>
112
+ );
113
+ }
114
+
115
+ if (!link) {
116
+ return null;
117
+ }
118
+
119
+ const fields = normalizeCommissionFields(link);
120
+ const linkId = extractId(link?.id || link?.['@id']);
121
+
122
+ const openEdit = () => {
123
+ setForm({
124
+ comission: String(fields.comission ?? ''),
125
+ minimum_comission: String(fields.minimum_comission ?? ''),
126
+ });
127
+ setModalVisible(true);
128
+ };
129
+
130
+ const save = async () => {
131
+ const validated = validateCommissionFields({
132
+ comission: form.comission,
133
+ minimum_comission: form.minimum_comission,
134
+ });
135
+ if (!validated.ok) {
136
+ showError?.(
137
+ global.t?.t('people', 'error', validated.error) ||
138
+ 'Valores de comissão inválidos',
139
+ );
140
+ return;
141
+ }
142
+ if (!linkId || !peopleLinkActions?.save) {
143
+ showError?.(
144
+ global.t?.t('users', 'error', 'saveUnavailable') || 'Salvar indisponível',
145
+ );
146
+ return;
147
+ }
148
+
149
+ setIsSaving(true);
150
+ try {
151
+ const saved = await peopleLinkActions.save({
152
+ id: linkId,
153
+ comission: validated.comission,
154
+ minimum_comission: validated.minimum_comission,
155
+ });
156
+ setLink(prev => ({
157
+ ...(prev || {}),
158
+ ...(saved || {}),
159
+ comission: validated.comission,
160
+ minimum_comission: validated.minimum_comission,
161
+ }));
162
+ setModalVisible(false);
163
+ showSuccess?.(
164
+ global.t?.t('people', 'success', 'commissionUpdated') ||
165
+ 'Comissão atualizada',
166
+ );
167
+ } catch {
168
+ showError?.(
169
+ global.t?.t('people', 'error', 'commissionUpdateFailed') ||
170
+ 'Falha ao atualizar comissão',
171
+ );
172
+ } finally {
173
+ setIsSaving(false);
174
+ }
175
+ };
176
+
177
+ return (
178
+ <View style={styles.fieldGroupLarge}>
179
+ <Text style={styles.fieldLabel}>
180
+ {global.t?.t('people', 'label', 'franchiseCommission') ||
181
+ 'Comissão da franquia'}
182
+ </Text>
183
+
184
+ <View style={styles.switchRow}>
185
+ <View style={{flex: 1}}>
186
+ <Text style={styles.switchLabel}>
187
+ {global.t?.t('people', 'label', 'comission') || 'Comissão'}:{' '}
188
+ {formatPercent(fields.comission)}
189
+ </Text>
190
+ <Text style={[styles.switchLabel, {marginTop: 4}]}>
191
+ {global.t?.t('people', 'label', 'minimumComission') ||
192
+ 'Comissão mínima'}
193
+ : {formatMinimum(fields.minimum_comission)}
194
+ </Text>
195
+ </View>
196
+ {isEditing && editable ? (
197
+ <TouchableOpacity
198
+ onPress={openEdit}
199
+ accessibilityLabel="edit-franchise-commission"
200
+ testID="edit-franchise-commission"
201
+ style={{padding: 8}}>
202
+ <FeatherIcon
203
+ name="edit-2"
204
+ size={16}
205
+ color={themeColors.inputIcon || themeColors.textSecondary}
206
+ />
207
+ </TouchableOpacity>
208
+ ) : null}
209
+ </View>
210
+
211
+ <Modal
212
+ visible={modalVisible}
213
+ transparent
214
+ animationType="fade"
215
+ onRequestClose={() => !isSaving && setModalVisible(false)}>
216
+ <View
217
+ style={{
218
+ flex: 1,
219
+ backgroundColor: 'rgba(0,0,0,0.4)',
220
+ justifyContent: 'center',
221
+ padding: 24,
222
+ }}>
223
+ <View
224
+ style={{
225
+ backgroundColor: themeColors.pageBackground || '#fff',
226
+ borderRadius: 12,
227
+ padding: 16,
228
+ }}>
229
+ <Text style={[styles.fieldLabel, {marginBottom: 12}]}>
230
+ {global.t?.t('people', 'title', 'editFranchiseCommission') ||
231
+ 'Editar comissão da franquia'}
232
+ </Text>
233
+
234
+ <View style={styles.fieldGroup}>
235
+ <Text style={styles.fieldLabel}>
236
+ {global.t?.t('people', 'label', 'comission') ||
237
+ 'Comissão (%)'}
238
+ </Text>
239
+ <TextInput
240
+ value={form.comission}
241
+ onChangeText={text =>
242
+ setForm(prev => ({...prev, comission: text.replace(',', '.')}))
243
+ }
244
+ keyboardType="decimal-pad"
245
+ style={styles.input}
246
+ testID="franchise-comission-input"
247
+ placeholderTextColor={themeColors.inputPlaceholderText}
248
+ />
249
+ </View>
250
+
251
+ <View style={styles.fieldGroup}>
252
+ <Text style={styles.fieldLabel}>
253
+ {global.t?.t('people', 'label', 'minimumComission') ||
254
+ 'Comissão mínima'}
255
+ </Text>
256
+ <TextInput
257
+ value={form.minimum_comission}
258
+ onChangeText={text =>
259
+ setForm(prev => ({
260
+ ...prev,
261
+ minimum_comission: text.replace(',', '.'),
262
+ }))
263
+ }
264
+ keyboardType="decimal-pad"
265
+ style={styles.input}
266
+ testID="franchise-minimum-comission-input"
267
+ placeholderTextColor={themeColors.inputPlaceholderText}
268
+ />
269
+ </View>
270
+
271
+ <TouchableOpacity
272
+ onPress={save}
273
+ disabled={isSaving}
274
+ testID="save-franchise-commission"
275
+ style={[
276
+ styles.saveButton,
277
+ isSaving ? styles.saveButtonDisabled : null,
278
+ {marginTop: 8},
279
+ ]}>
280
+ {isSaving ? (
281
+ <ActivityIndicator
282
+ size="small"
283
+ color={themeColors.buttonText}
284
+ />
285
+ ) : (
286
+ <Text style={styles.saveButtonText}>
287
+ {global.t?.t('users', 'button', 'saveChanges') || 'Salvar'}
288
+ </Text>
289
+ )}
290
+ </TouchableOpacity>
291
+
292
+ <TouchableOpacity
293
+ onPress={() => !isSaving && setModalVisible(false)}
294
+ disabled={isSaving}
295
+ style={{marginTop: 12, alignItems: 'center'}}>
296
+ <Text style={styles.switchLabel}>
297
+ {global.t?.t('common', 'button', 'cancel') || 'Cancelar'}
298
+ </Text>
299
+ </TouchableOpacity>
300
+ </View>
301
+ </View>
302
+ </Modal>
303
+ </View>
304
+ );
305
+ };
306
+
307
+ export default FranchiseCommissionSection;
@@ -0,0 +1,215 @@
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: 12,
56
+ paddingVertical: 10,
57
+ color: '#0F172A',
58
+ backgroundColor: '#FFFFFF',
59
+ },
60
+ modalActions: {
61
+ flexDirection: 'row',
62
+ gap: 12,
63
+ marginTop: 8,
64
+ },
65
+ secondaryButton: {
66
+ flex: 1,
67
+ paddingVertical: 14,
68
+ borderRadius: 12,
69
+ borderWidth: 1,
70
+ borderColor: '#94A3B8',
71
+ alignItems: 'center',
72
+ justifyContent: 'center',
73
+ },
74
+ secondaryButtonText: {
75
+ fontSize: 15,
76
+ fontWeight: '600',
77
+ color: '#475569',
78
+ },
79
+ primaryButton: {
80
+ flex: 1,
81
+ paddingVertical: 14,
82
+ borderRadius: 12,
83
+ backgroundColor: colors.primary,
84
+ alignItems: 'center',
85
+ justifyContent: 'center',
86
+ },
87
+ primaryButtonText: {
88
+ fontSize: 15,
89
+ fontWeight: '600',
90
+ color: '#FFFFFF',
91
+ },
92
+ };
93
+
94
+ /**
95
+ * Modal to link or edit a PJ↔PJ franchisee/filial people_link (MANAGER only).
96
+ */
97
+ const FranchiseLinksManageModal = ({
98
+ visible,
99
+ onClose,
100
+ editingLink,
101
+ formData,
102
+ setFormData,
103
+ franchiseOptions,
104
+ isSaving,
105
+ onSave,
106
+ }) => {
107
+ const title = editingLink ? 'Editar vínculo' : 'Vincular franquia/filial';
108
+
109
+ return (
110
+ <AnimatedModal
111
+ visible={visible}
112
+ onRequestClose={onClose}
113
+ style={{ paddingHorizontal: 20 }}>
114
+ <View style={styles.modalCard}>
115
+ <View style={styles.modalHeader}>
116
+ <Text style={styles.modalTitle}>{title}</Text>
117
+ <TouchableOpacity onPress={onClose} accessibilityLabel="Fechar">
118
+ <Icon name="close" size={20} color="#64748B" />
119
+ </TouchableOpacity>
120
+ </View>
121
+
122
+ <ScrollView keyboardShouldPersistTaps="handled">
123
+ <View style={styles.fieldGroup}>
124
+ <Text style={styles.fieldLabel}>Pessoa jurídica</Text>
125
+ <View style={styles.pickerContainer}>
126
+ <Picker
127
+ selectedValue={formData.linkedIri}
128
+ enabled={!editingLink}
129
+ onValueChange={value =>
130
+ setFormData(previous => ({ ...previous, linkedIri: value }))
131
+ }
132
+ testID="franchise-link-people-picker">
133
+ <Picker.Item label="Selecione uma PJ" value="" />
134
+ {franchiseOptions.map(option => (
135
+ <Picker.Item
136
+ key={option.id}
137
+ label={
138
+ option.alias
139
+ ? `${option.name} (${option.alias})`
140
+ : option.name
141
+ }
142
+ value={option.iri}
143
+ />
144
+ ))}
145
+ </Picker>
146
+ </View>
147
+ </View>
148
+
149
+ <View style={styles.fieldGroup}>
150
+ <Text style={styles.fieldLabel}>Comissão (%)</Text>
151
+ <TextInput
152
+ value={formData.comission}
153
+ onChangeText={value =>
154
+ setFormData(previous => ({ ...previous, comission: value }))
155
+ }
156
+ keyboardType="decimal-pad"
157
+ style={styles.input}
158
+ testID="franchise-link-comission-input"
159
+ />
160
+ </View>
161
+
162
+ <View style={styles.fieldGroup}>
163
+ <Text style={styles.fieldLabel}>Comissão mínima</Text>
164
+ <TextInput
165
+ value={formData.minimumComission}
166
+ onChangeText={value =>
167
+ setFormData(previous => ({ ...previous, minimumComission: value }))
168
+ }
169
+ keyboardType="decimal-pad"
170
+ style={styles.input}
171
+ testID="franchise-link-minimum-comission-input"
172
+ />
173
+ </View>
174
+
175
+ <View style={styles.fieldGroup}>
176
+ <Text style={styles.fieldLabel}>Tipo de vínculo</Text>
177
+ <View style={styles.pickerContainer}>
178
+ <Picker
179
+ selectedValue={formData.linkType}
180
+ onValueChange={value =>
181
+ setFormData(previous => ({ ...previous, linkType: value }))
182
+ }
183
+ testID="franchise-link-type-picker">
184
+ <Picker.Item label="Franquia" value="franchisee" />
185
+ <Picker.Item label="Filial" value="filial" />
186
+ </Picker>
187
+ </View>
188
+ </View>
189
+ </ScrollView>
190
+
191
+ <View style={styles.modalActions}>
192
+ <TouchableOpacity
193
+ style={styles.secondaryButton}
194
+ onPress={onClose}
195
+ disabled={isSaving}>
196
+ <Text style={styles.secondaryButtonText}>Cancelar</Text>
197
+ </TouchableOpacity>
198
+ <TouchableOpacity
199
+ style={styles.primaryButton}
200
+ onPress={onSave}
201
+ disabled={isSaving}
202
+ testID="franchise-link-save-btn">
203
+ {isSaving ? (
204
+ <ActivityIndicator size="small" color="#FFFFFF" />
205
+ ) : (
206
+ <Text style={styles.primaryButtonText}>Salvar</Text>
207
+ )}
208
+ </TouchableOpacity>
209
+ </View>
210
+ </View>
211
+ </AnimatedModal>
212
+ );
213
+ };
214
+
215
+ export default FranchiseLinksManageModal;