@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,221 @@
1
+ const extractId = value => String(value || '').replace(/\D/g, '');
2
+
3
+ /** API Platform expects people as IRI (e.g. /people/106218), not a bare id. */
4
+ const toPeopleIri = value => {
5
+ const id = extractId(value);
6
+ return id ? `/people/${id}` : '';
7
+ };
8
+
9
+ /** Query for GET /users when opening the Users tab on client-details. */
10
+ const buildUsersListQuery = client => {
11
+ const peopleIri = toPeopleIri(client?.id || client?.['@id']);
12
+ if (!peopleIri) {
13
+ return null;
14
+ }
15
+ return {
16
+ people: peopleIri,
17
+ itemsPerPage: 100,
18
+ __storeMeta: {
19
+ dedupeKey: `client-details-users-${peopleIri}`,
20
+ skipSystemError: true,
21
+ },
22
+ };
23
+ };
24
+
25
+
26
+ const normalizeUserItem = entry => {
27
+ if (!entry) {
28
+ return null;
29
+ }
30
+
31
+ const id = extractId(entry?.id || entry?.['@id'] || entry?.user?.id || entry?.user?.['@id']);
32
+ const username = String(
33
+ entry?.username || entry?.name || entry?.user?.username || '',
34
+ ).trim();
35
+ const apiKey = String(
36
+ entry?.apiKey || entry?.api_key || entry?.user?.apiKey || entry?.user?.api_key || '',
37
+ ).trim();
38
+ const role = String(entry?.role || 'Usuario').trim() || 'Usuario';
39
+ const timezoneId = extractId(
40
+ entry?.timezone?.id ||
41
+ entry?.timezone?.['@id'] ||
42
+ entry?.timezone_id ||
43
+ entry?.timezoneId ||
44
+ entry?.timezone,
45
+ );
46
+
47
+ if (!id && !username && !apiKey) {
48
+ return null;
49
+ }
50
+
51
+ return {
52
+ id: id || `temp-${Date.now()}`,
53
+ username,
54
+ name: username,
55
+ role,
56
+ apiKey,
57
+ timezoneId: timezoneId || '',
58
+ };
59
+ };
60
+
61
+ const embeddedUsersFromClient = client => {
62
+ const sourceUsers = Array.isArray(client?.user)
63
+ ? client.user
64
+ : client?.user
65
+ ? [client.user]
66
+ : [];
67
+ return sourceUsers.map(normalizeUserItem).filter(Boolean);
68
+ };
69
+
70
+ const mapUsersForClient = users =>
71
+ users.map(user => ({
72
+ id: extractId(user?.id) || user?.id,
73
+ '@id': extractId(user?.id) || user?.id,
74
+ username: user?.username || user?.name || '',
75
+ role: user?.role || 'Usuario',
76
+ apiKey: user?.apiKey || '',
77
+ timezoneId: user?.timezoneId || '',
78
+ }));
79
+
80
+ const formatApiKeyPreview = value => {
81
+ const apiKey = String(value || '').trim();
82
+ if (!apiKey) {
83
+ return 'Chave de API indisponivel';
84
+ }
85
+
86
+ if (apiKey.length <= 16) {
87
+ return apiKey;
88
+ }
89
+
90
+ return `${apiKey.slice(0, 8)}...${apiKey.slice(-6)}`;
91
+ };
92
+
93
+ const copyTextToClipboard = async text => {
94
+ const normalizedText = String(text ?? '').trim();
95
+ if (!normalizedText) {
96
+ return false;
97
+ }
98
+
99
+ if (
100
+ typeof navigator !== 'undefined' &&
101
+ navigator?.clipboard &&
102
+ typeof navigator.clipboard.writeText === 'function'
103
+ ) {
104
+ await navigator.clipboard.writeText(normalizedText);
105
+ return true;
106
+ }
107
+
108
+ return false;
109
+ };
110
+
111
+ const extractErrorMessage = error => {
112
+ if (Array.isArray(error?.violations) && error.violations.length) {
113
+ return error.violations
114
+ .map(item => item?.message || item)
115
+ .filter(Boolean)
116
+ .join('\n');
117
+ }
118
+
119
+ if (Array.isArray(error?.message)) {
120
+ return error.message
121
+ .map(item => item?.message || item)
122
+ .filter(Boolean)
123
+ .join(', ');
124
+ }
125
+
126
+ const status = error?.response?.status || error?.status;
127
+ if (status === 401) {
128
+ return 'Autenticação necessária. Faça login novamente e tente criar o usuário.';
129
+ }
130
+
131
+ if (error?.response?.data?.message) {
132
+ return String(error.response.data.message);
133
+ }
134
+
135
+ const msg = error?.message || error?.error || (typeof error === 'string' ? error : '');
136
+ if (/authentication required/i.test(String(msg))) {
137
+ return 'Autenticação necessária. Faça login novamente e tente criar o usuário.';
138
+ }
139
+
140
+ return msg;
141
+ };
142
+
143
+ const apiKeyModalStyles = {
144
+ buttonRow: {
145
+ flexDirection: 'row',
146
+ flexWrap: 'wrap',
147
+ gap: 12,
148
+ },
149
+ secondaryButton: {
150
+ flex: 1,
151
+ minWidth: 132,
152
+ paddingVertical: 14,
153
+ borderRadius: 12,
154
+ borderWidth: 1,
155
+ borderColor: '#64748B',
156
+ alignItems: 'center',
157
+ justifyContent: 'center',
158
+ },
159
+ secondaryText: {
160
+ fontSize: 16,
161
+ fontWeight: '600',
162
+ color: '#64748B',
163
+ },
164
+ };
165
+
166
+
167
+ const toTimezoneItem = entry => {
168
+ if (!entry) {
169
+ return null;
170
+ }
171
+ const id = extractId(entry?.id || entry?.['@id']);
172
+ if (!id) {
173
+ return null;
174
+ }
175
+ const name = String(entry?.name || entry?.timezone || '').trim();
176
+ const displayName = String(
177
+ entry?.displayName || entry?.label || entry?.offset || name,
178
+ ).trim();
179
+ return {
180
+ id,
181
+ name,
182
+ displayName: displayName || name || id,
183
+ };
184
+ };
185
+
186
+ const extractCollectionItems = response => {
187
+ if (Array.isArray(response)) {
188
+ return response;
189
+ }
190
+ if (Array.isArray(response?.member)) {
191
+ return response.member;
192
+ }
193
+ if (Array.isArray(response?.['hydra:member'])) {
194
+ return response['hydra:member'];
195
+ }
196
+ if (Array.isArray(response?.items)) {
197
+ return response.items;
198
+ }
199
+ return [];
200
+ };
201
+
202
+ const toTimezoneIri = timezoneId => {
203
+ const id = extractId(timezoneId);
204
+ return id ? `/timezones/${id}` : null;
205
+ };
206
+
207
+ module.exports = {
208
+ apiKeyModalStyles,
209
+ copyTextToClipboard,
210
+ extractCollectionItems,
211
+ extractErrorMessage,
212
+ extractId,
213
+ toPeopleIri,
214
+ buildUsersListQuery,
215
+ embeddedUsersFromClient,
216
+ formatApiKeyPreview,
217
+ mapUsersForClient,
218
+ normalizeUserItem,
219
+ toTimezoneIri,
220
+ toTimezoneItem,
221
+ };
@@ -0,0 +1,37 @@
1
+ export const extractId = value => {
2
+ if (value === null || value === undefined || value === '') return '';
3
+ if (typeof value === 'number') return String(value);
4
+ if (typeof value === 'string') {
5
+ const match = value.match(/(\d+)$/);
6
+ return match ? match[1] : value;
7
+ }
8
+ if (typeof value === 'object') {
9
+ return extractId(value.id || value['@id'] || '');
10
+ }
11
+ return '';
12
+ };
13
+
14
+ export const buildInitialProviderPayload = client => {
15
+ const providerId = extractId(client?.id || client?.['@id']);
16
+
17
+ return {
18
+ id: providerId,
19
+ '@id': client?.['@id'] || (providerId ? `/people/${providerId}` : ''),
20
+ alias: String(client?.alias || '').trim(),
21
+ name: String(client?.name || '').trim(),
22
+ peopleType: client?.peopleType || '',
23
+ };
24
+ };
25
+
26
+ export const buildCreateProductParams = client => {
27
+ const initialProvider = buildInitialProviderPayload(client);
28
+
29
+ if (!initialProvider.id) {
30
+ return null;
31
+ }
32
+
33
+ return {
34
+ context: 'products',
35
+ initialProvider,
36
+ };
37
+ };
@@ -0,0 +1,4 @@
1
+ // React entrypoint kept after Vue removal.
2
+ const moduleEntry = {};
3
+
4
+ export default moduleEntry;
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import { SafeAreaView } from 'react-native-safe-area-context';
4
+ import styles from './details.page.styles';
5
+
6
+ /**
7
+ * Loading skeleton for ClientDetails — extracted to keep details.js under 500 lines.
8
+ */
9
+ const ClientDetailsSkeleton = ({ tabs = [] }) => (
10
+ <SafeAreaView style={styles.container} edges={['bottom']}>
11
+ <View style={styles.headerProfile}>
12
+ <View
13
+ style={[
14
+ styles.skeletonCircle,
15
+ { width: 64, height: 64, borderRadius: 32, marginBottom: 12 },
16
+ ]}
17
+ />
18
+ <View
19
+ style={[
20
+ styles.skeletonLine,
21
+ { width: 180, height: 22, marginBottom: 8 },
22
+ ]}
23
+ />
24
+ <View style={[styles.skeletonLine, { width: 90, height: 12 }]} />
25
+ </View>
26
+
27
+ <View style={styles.tabsHeader}>
28
+ {tabs.map(tab => (
29
+ <View key={`skeleton-${tab.key}`} style={styles.skeletonTab} />
30
+ ))}
31
+ </View>
32
+
33
+ <View style={styles.skeletonContent}>
34
+ <View style={styles.skeletonCard}>
35
+ <View
36
+ style={[
37
+ styles.skeletonLine,
38
+ { width: '48%', height: 18, marginBottom: 14 },
39
+ ]}
40
+ />
41
+ <View
42
+ style={[
43
+ styles.skeletonLine,
44
+ { width: '100%', height: 14, marginBottom: 10 },
45
+ ]}
46
+ />
47
+ <View
48
+ style={[
49
+ styles.skeletonLine,
50
+ { width: '90%', height: 14, marginBottom: 10 },
51
+ ]}
52
+ />
53
+ <View style={[styles.skeletonLine, { width: '82%', height: 14 }]} />
54
+ </View>
55
+ <View style={styles.skeletonCard}>
56
+ <View
57
+ style={[
58
+ styles.skeletonLine,
59
+ { width: '52%', height: 18, marginBottom: 14 },
60
+ ]}
61
+ />
62
+ <View
63
+ style={[
64
+ styles.skeletonLine,
65
+ {
66
+ width: '100%',
67
+ height: 46,
68
+ borderRadius: 10,
69
+ marginBottom: 10,
70
+ },
71
+ ]}
72
+ />
73
+ <View
74
+ style={[
75
+ styles.skeletonLine,
76
+ { width: '100%', height: 46, borderRadius: 10 },
77
+ ]}
78
+ />
79
+ </View>
80
+ </View>
81
+ </SafeAreaView>
82
+ );
83
+
84
+ export default ClientDetailsSkeleton;
@@ -0,0 +1,17 @@
1
+ /** Compatibility exports for the shared people detail contract. */
2
+
3
+ export {
4
+ resolveContextKey,
5
+ normalizeCollection,
6
+ PERSON_PHOTO_MEDIA_TYPES,
7
+ COMPANY_ICON_MEDIA_TYPES,
8
+ extractId,
9
+ resolveRouteClientSeed,
10
+ resolveRouteClientId,
11
+ mergeLinkedContactIntoClient,
12
+ } from '@controleonline/ui-people/src/react/utils/peopleDetailsHelpers';
13
+
14
+ export {
15
+ resolvePeopleDetailTabIndex as resolveInitialTabIndex,
16
+ buildPeopleDetailTabDefs as buildClientTabDefs,
17
+ } from '@controleonline/ui-people/src/react/utils/peopleDetailsHelpers';
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import People from '@controleonline/ui-people/src/react/pages/People';
3
+
4
+ const Clients = () => {
5
+ return (
6
+ <People
7
+ context={{
8
+ context: ['client', 'prospect'],
9
+ defaultContext: 'client',
10
+ // Keep the index independent from optional remote media. Broken/stale
11
+ // avatar files must not generate 404 requests while browsing clients.
12
+ usePeopleImage: false,
13
+ title: global.t?.t('people', 'title', 'clients'),
14
+ searchPlaceholder: global.t?.t('people', 'searchPlaceholder', 'searchClient'),
15
+ modalTitleByType: {
16
+ client: global.t?.t('people', 'title', 'registerClient'),
17
+ prospect: global.t?.t('people', 'title', 'registerProspect'),
18
+ },
19
+ emptyTitle: global.t?.t('people', 'title', 'emptyClient'),
20
+ emptySearchTitle: global.t?.t('people', 'title', 'emptySearchClient'),
21
+ emptySubtitle: global.t?.t('people', 'title', 'addFirstClient'),
22
+ }}
23
+ />
24
+ );
25
+ };
26
+
27
+ export default Clients;