@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,356 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ } from 'react-native';
8
+ import { useStore } from '@store';
9
+ import DefaultUpload from '@controleonline/ui-default/src/react/components/upload/DefaultUpload';
10
+ import { extractFileId } from '@controleonline/ui-default/src/react/components/upload/fileUpload';
11
+ import { useMessage } from '@controleonline/ui-common/src/react/components/MessageService';
12
+ import { resolveThemePalette, withOpacity } from '@controleonline/../../src/styles/branding';
13
+ import { colors } from '@controleonline/../../src/styles/colors';
14
+
15
+ const COMPANY_MEDIA_ACCEPT_ATTRIBUTE = 'image/png,image/jpeg,.png,.jpg,.jpeg';
16
+
17
+ const normalizeCollection = payload => {
18
+ if (Array.isArray(payload)) return payload;
19
+ if (!payload || typeof payload !== 'object') return [];
20
+ if (Array.isArray(payload.member)) return payload.member;
21
+ if (Array.isArray(payload['hydra:member'])) return payload['hydra:member'];
22
+ if (Array.isArray(payload.items)) return payload.items;
23
+ return [];
24
+ };
25
+
26
+ const extractId = value => {
27
+ if (!value && value !== 0) return null;
28
+ if (typeof value === 'number') return value;
29
+ const raw = typeof value === 'string' ? value : value?.id || value?.['@id'];
30
+ if (!raw) return null;
31
+ const match = String(raw).match(/(\d+)$/);
32
+ return match ? parseInt(match[1], 10) : null;
33
+ };
34
+
35
+ const MediaTab = ({ client, onChanged = null }) => {
36
+ const peopleStore = useStore('people');
37
+ const themeStore = useStore('theme');
38
+ const { currentCompany } = peopleStore.getters;
39
+ const peopleActions = peopleStore.actions || {};
40
+ const { colors: themeColors } = themeStore.getters;
41
+ const { showError } = useMessage();
42
+
43
+ const clientId = extractId(client?.id || client?.['@id']);
44
+ const peopleType = String(client?.peopleType || 'J').trim().toUpperCase() || 'J';
45
+
46
+ const [mediaTypes, setMediaTypes] = useState([]);
47
+ const [peopleMedia, setPeopleMedia] = useState([]);
48
+ const [mediaTypesLoading, setMediaTypesLoading] = useState(false);
49
+ const [peopleMediaLoading, setPeopleMediaLoading] = useState(false);
50
+
51
+ const palette = useMemo(
52
+ () =>
53
+ resolveThemePalette(
54
+ { ...themeColors, ...(currentCompany?.theme?.colors || {}) },
55
+ colors,
56
+ ),
57
+ [currentCompany?.id, currentCompany?.theme?.colors, themeColors],
58
+ );
59
+
60
+ const mediaByTypeId = useMemo(
61
+ () =>
62
+ normalizeCollection(peopleMedia).reduce((accumulator, item) => {
63
+ const mediaTypeId = extractId(item?.mediaType);
64
+ if (mediaTypeId) {
65
+ accumulator[String(mediaTypeId)] = item;
66
+ }
67
+
68
+ return accumulator;
69
+ }, {}),
70
+ [peopleMedia],
71
+ );
72
+
73
+ const loadMediaTypes = useCallback(async () => {
74
+ setMediaTypesLoading(true);
75
+
76
+ try {
77
+ const response = await peopleActions.getMediaTypes({
78
+ peopleType,
79
+ itemsPerPage: 100,
80
+ });
81
+
82
+ setMediaTypes(normalizeCollection(response));
83
+ } catch (error) {
84
+ setMediaTypes([]);
85
+ showError(error?.message || 'Nao foi possivel carregar os tipos de midia.');
86
+ } finally {
87
+ setMediaTypesLoading(false);
88
+ }
89
+ }, [peopleActions, peopleType, showError]);
90
+
91
+ const loadPeopleMedia = useCallback(async () => {
92
+ if (!clientId) {
93
+ setPeopleMedia([]);
94
+ return;
95
+ }
96
+
97
+ setPeopleMediaLoading(true);
98
+
99
+ try {
100
+ const response = await peopleActions.getPeopleMedia({
101
+ people: `/people/${clientId}`,
102
+ 'mediaType.peopleType': peopleType,
103
+ itemsPerPage: 100,
104
+ });
105
+
106
+ const rows = normalizeCollection(response).map(item => {
107
+ const fileId = extractFileId(item?.file);
108
+ if (!fileId) return item;
109
+ const file =
110
+ item?.file && typeof item.file === 'object' && !Array.isArray(item.file)
111
+ ? {...item.file, id: item.file.id || fileId, '@id': item.file['@id'] || `/files/${fileId}`}
112
+ : {id: fileId, '@id': `/files/${fileId}`};
113
+ return {
114
+ ...item,
115
+ id: extractId(item) || item?.id,
116
+ file,
117
+ };
118
+ });
119
+ setPeopleMedia(rows);
120
+ } catch (error) {
121
+ setPeopleMedia([]);
122
+ showError(error?.message || 'Nao foi possivel carregar as midias.');
123
+ } finally {
124
+ setPeopleMediaLoading(false);
125
+ }
126
+ }, [clientId, peopleActions, peopleType, showError]);
127
+
128
+ useEffect(() => {
129
+ loadMediaTypes();
130
+ }, [loadMediaTypes]);
131
+
132
+ useEffect(() => {
133
+ loadPeopleMedia();
134
+ }, [loadPeopleMedia]);
135
+
136
+ const buildMediaUploadHandlers = useCallback(
137
+ mediaType => {
138
+ const mediaTypeId = extractId(mediaType);
139
+ const currentMedia = mediaByTypeId[String(mediaTypeId)] || null;
140
+
141
+ return {
142
+ onAttachFile: async file => {
143
+ const fileId = extractFileId(file);
144
+
145
+ if (!fileId) {
146
+ throw new Error('Arquivo sem identificador.');
147
+ }
148
+
149
+ return peopleActions.savePeopleMedia({
150
+ id: currentMedia?.id || currentMedia?.['@id'],
151
+ people: `/people/${clientId}`,
152
+ mediaType: mediaType?.['@id'] || `/media_types/${mediaTypeId}`,
153
+ file: `/files/${fileId}`,
154
+ });
155
+ },
156
+ onRemoveAttachment: async relation => {
157
+ await peopleActions.deletePeopleMedia({mediaId: relation?.id || relation?.['@id']});
158
+ },
159
+ };
160
+ },
161
+ [clientId, mediaByTypeId, peopleActions],
162
+ );
163
+
164
+ if (!clientId) {
165
+ return (
166
+ <View style={styles.container}>
167
+ <View style={styles.sectionCard}>
168
+ <Text style={styles.sectionTitle}>Midias</Text>
169
+ <Text style={styles.sectionDescription}>Nao foi possivel identificar este cadastro.</Text>
170
+ </View>
171
+ </View>
172
+ );
173
+ }
174
+
175
+ return (
176
+ <View style={styles.container}>
177
+ <View
178
+ style={[
179
+ styles.sectionCard,
180
+ {
181
+ backgroundColor: palette.panelBackground || colors.white,
182
+ borderColor: withOpacity(palette.text || colors.text, 0.08),
183
+ },
184
+ ]}
185
+ >
186
+ <Text style={[styles.sectionTitle, { color: palette.text || colors.text }]}>
187
+ Midias
188
+ </Text>
189
+
190
+ <Text style={[styles.sectionDescription, { color: palette.textSecondary || '#64748B' }]}>
191
+ Envie arquivos PNG ou JPG para cada tipo de midia do cadastro. A prévia abaixo sempre reflete o que está salvo no banco.
192
+ </Text>
193
+
194
+ {mediaTypesLoading || peopleMediaLoading ? (
195
+ <View style={styles.mediaLoadingState}>
196
+ <ActivityIndicator size="small" color={palette.primary || '#2563EB'} />
197
+ </View>
198
+ ) : (
199
+ <View style={styles.mediaGrid}>
200
+ {mediaTypes.map(mediaType => {
201
+ const mediaTypeId = extractId(mediaType);
202
+ const currentMedia = mediaByTypeId[String(mediaTypeId)] || null;
203
+ const mediaTypeLabel = String(mediaType?.type || '').trim() || 'Midia';
204
+ const handlers = buildMediaUploadHandlers(mediaType);
205
+
206
+ return (
207
+ <View
208
+ key={mediaTypeId || mediaType?.type}
209
+ style={[
210
+ styles.mediaCard,
211
+ {
212
+ backgroundColor: withOpacity(palette.primary || '#2563EB', 0.04),
213
+ borderColor: withOpacity(palette.primary || '#2563EB', 0.14),
214
+ },
215
+ ]}
216
+ >
217
+ <DefaultUpload
218
+ relationStoreName="people"
219
+ relationField="people"
220
+ relationResource="people"
221
+ entityId={clientId}
222
+ companyId={clientId}
223
+ company={client}
224
+ showAttachmentActions={false}
225
+ context="people_media"
226
+ libraryContexts={['people_media']}
227
+ attachments={currentMedia ? [currentMedia] : []}
228
+ acceptedTypes={COMPANY_MEDIA_ACCEPT_ATTRIBUTE}
229
+ fileType="image"
230
+ fileTypeLabel="imagem"
231
+ title={mediaTypeLabel}
232
+ triggerLabel={`Gerenciar ${mediaTypeLabel}`}
233
+ managerTitle={`Gerenciador de ${mediaTypeLabel}`}
234
+ searchPlaceholder="Buscar imagem"
235
+ uploadButtonLabel="Enviar nova"
236
+ emptyAttachmentLabel="Nenhuma imagem vinculada."
237
+ emptyLibraryLabel="Nenhuma imagem encontrada."
238
+ uploadSuccessMessage={`${mediaTypeLabel} atualizada com sucesso.`}
239
+ attachSuccessMessage={`${mediaTypeLabel} vinculada com sucesso.`}
240
+ removeSuccessMessage={`${mediaTypeLabel} removida.`}
241
+ onChanged={async () => {
242
+ await loadPeopleMedia();
243
+ onChanged?.();
244
+ }}
245
+ {...handlers}
246
+ />
247
+ </View>
248
+ );
249
+ })}
250
+ </View>
251
+ )}
252
+ </View>
253
+ </View>
254
+ );
255
+ };
256
+
257
+ const styles = StyleSheet.create({
258
+ container: {
259
+ flex: 1,
260
+ paddingHorizontal: 16,
261
+ paddingBottom: 24,
262
+ },
263
+ sectionCard: {
264
+ borderRadius: 18,
265
+ borderWidth: 1,
266
+ padding: 16,
267
+ gap: 12,
268
+ },
269
+ sectionTitle: {
270
+ fontSize: 18,
271
+ fontWeight: '700',
272
+ },
273
+ sectionDescription: {
274
+ fontSize: 14,
275
+ lineHeight: 20,
276
+ },
277
+ mediaLoadingState: {
278
+ paddingVertical: 28,
279
+ alignItems: 'center',
280
+ justifyContent: 'center',
281
+ },
282
+ mediaGrid: {
283
+ gap: 16,
284
+ },
285
+ mediaCard: {
286
+ borderWidth: 1,
287
+ borderRadius: 16,
288
+ padding: 14,
289
+ gap: 12,
290
+ },
291
+ mediaCardHeader: {
292
+ flexDirection: 'row',
293
+ alignItems: 'center',
294
+ justifyContent: 'space-between',
295
+ gap: 12,
296
+ },
297
+ mediaHeaderActions: {
298
+ flexDirection: 'row',
299
+ alignItems: 'center',
300
+ gap: 8,
301
+ },
302
+ mediaCardTitle: {
303
+ flex: 1,
304
+ fontSize: 15,
305
+ fontWeight: '700',
306
+ },
307
+ mediaDeleteButton: {
308
+ borderWidth: 1,
309
+ borderRadius: 999,
310
+ paddingVertical: 8,
311
+ paddingHorizontal: 12,
312
+ },
313
+ mediaBadge: {
314
+ borderRadius: 999,
315
+ paddingVertical: 4,
316
+ paddingHorizontal: 8,
317
+ },
318
+ mediaBadgeText: {
319
+ fontSize: 12,
320
+ fontWeight: '700',
321
+ },
322
+ mediaPreviewFrame: {
323
+ minHeight: 180,
324
+ borderRadius: 14,
325
+ borderWidth: 1,
326
+ overflow: 'hidden',
327
+ alignItems: 'center',
328
+ justifyContent: 'center',
329
+ },
330
+ mediaPreviewTransparencyGrid: {
331
+ // RN StyleSheet (web) rejects multi-value backgroundPosition / CSS backgroundImage gradients.
332
+ // Keep solid preview background; checkerboard was cosmetic only.
333
+ backgroundColor: '#F8FAFC',
334
+ },
335
+ mediaPreviewImage: {
336
+ width: '100%',
337
+ height: '100%',
338
+ minHeight: 180,
339
+ },
340
+ mediaEmptyState: {
341
+ alignItems: 'center',
342
+ justifyContent: 'center',
343
+ gap: 8,
344
+ minHeight: 180,
345
+ },
346
+ mediaEmptyText: {
347
+ fontSize: 13,
348
+ fontWeight: '500',
349
+ },
350
+ mediaHelpText: {
351
+ fontSize: 12,
352
+ lineHeight: 18,
353
+ },
354
+ });
355
+
356
+ export default MediaTab;