@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,529 @@
1
+ /*
2
+ * Contract imported from AGENTS.md
3
+ * ## Escopo
4
+ * - `ui-customers` e o modulo React de cadastro e edicao de clientes.
5
+ * - Esta pagina concentra detalhes, enderecos e dados de exibicao de pessoas.
6
+ *
7
+ * ## Estado
8
+ *
9
+ * ## Limites
10
+ * - Nao duplicar validacoes de formato fora dos helpers compartilhados.
11
+ * - Manter aqui a orquestracao da tela e a edicao do cadastro de pessoas.
12
+ */
13
+ import React, { useCallback, useState, useLayoutEffect, useEffect, useMemo } from 'react';
14
+
15
+ import {
16
+ Text,
17
+ View,
18
+ ScrollView,
19
+ TouchableOpacity,
20
+ } from 'react-native';
21
+
22
+ import { SafeAreaView } from 'react-native-safe-area-context';
23
+ import { resolveFileImageUrl } from '@controleonline/ui-common/src/react/utils/fileUrl';
24
+ import PeopleAvatar from '@controleonline/ui-people/src/react/components/PeopleAvatar';
25
+ import { useStore, useStores } from '@store';
26
+ import { createDetailsStyles } from '../styles/details';
27
+ import GeneralTab from '../components/tabs/GeneralTab';
28
+ import UsersTab from '../components/tabs/UsersTab';
29
+ import SalesmanTab from '../components/tabs/SalesmanTab';
30
+ import EmployeesTab from '../components/tabs/EmployeesTab';
31
+ import ContractsTab from '../components/tabs/ContractsTab';
32
+ import ProductsTab from '../components/tabs/ProductsTab';
33
+ import MediaTab from '../components/tabs/MediaTab';
34
+ import FiscalTab from '../components/tabs/FiscalTab';
35
+ import FranchiseLinksTab from '../components/tabs/FranchiseLinksTab';
36
+ import {
37
+ buildEmployeeContactsFromPeopleLinks,
38
+ buildPeopleLinkReadParams,
39
+ } from '../components/tabs/employeeContacts';
40
+ import styles from './details.page.styles';
41
+
42
+ import {
43
+ inlineStyle_299_16,
44
+ inlineStyle_317_16,
45
+ inlineStyle_334_16,
46
+ inlineStyle_342_16,
47
+ } from './details.styles';
48
+
49
+ import ClientDetailsSkeleton from './ClientDetailsSkeleton';
50
+ import {
51
+ resolveContextKey,
52
+ normalizeCollection,
53
+ PERSON_PHOTO_MEDIA_TYPES,
54
+ COMPANY_ICON_MEDIA_TYPES,
55
+ extractId,
56
+ buildClientTabDefs,
57
+ resolveInitialTabIndex,
58
+ resolveRouteClientId,
59
+ resolveRouteClientSeed,
60
+ } from './clientDetailsHelpers';
61
+
62
+ const ClientDetails = ({ route, navigation }) => {
63
+ const routeParams = route.params || {};
64
+ const routeClientSeed = resolveRouteClientSeed(routeParams);
65
+ const clientId = resolveRouteClientId(routeParams);
66
+ const detailContext = resolveContextKey(routeParams?.contextKey);
67
+ const requestedInitialTab = String(route.params?.initialTab || '').trim();
68
+ const peopleStore = useStores(state => state.people) || {};
69
+ const peopleLinkStore = useStore('people_link');
70
+ const themeStore = useStore('theme');
71
+ const themeColors = themeStore.getters.colors;
72
+ const peopleActions = peopleStore?.actions || {};
73
+ const peopleGetters = peopleStore?.getters || {};
74
+ const getPeople = peopleActions?.get;
75
+ const getPeopleItems = peopleActions?.getItems;
76
+ const getPeopleLinks = peopleLinkStore?.actions?.getItems;
77
+ const savePeople = peopleActions?.save;
78
+ const detailsStyles = useMemo(() => createDetailsStyles(themeColors), [themeColors]);
79
+
80
+ const parentCompanyId = extractId(routeParams?.parentCompanyId);
81
+ const parentCompanyIri = parentCompanyId ? `/people/${parentCompanyId}` : '';
82
+ const initialContactLinkType = String(routeParams?.linkType || '')
83
+ .trim()
84
+ .toLowerCase();
85
+ const cachedClient = useMemo(() => {
86
+ if (!clientId) {
87
+ return routeClientSeed;
88
+ }
89
+
90
+ if (
91
+ routeClientSeed &&
92
+ extractId(routeClientSeed?.id || routeClientSeed?.['@id']) === clientId
93
+ ) {
94
+ return routeClientSeed;
95
+ }
96
+
97
+ const currentItem = peopleGetters?.item;
98
+ if (extractId(currentItem?.id || currentItem?.['@id']) === clientId) {
99
+ return currentItem;
100
+ }
101
+
102
+ const items = Array.isArray(peopleGetters?.items) ? peopleGetters.items : [];
103
+ return (
104
+ items.find(item => extractId(item?.id || item?.['@id']) === clientId) || null
105
+ );
106
+ }, [clientId, peopleGetters?.item, peopleGetters?.items, routeClientSeed]);
107
+ const [client, setClient] = useState(cachedClient);
108
+ const [isLoadingClient, setIsLoadingClient] = useState(
109
+ Boolean(clientId) && !cachedClient,
110
+ );
111
+ const [activeTab, setActiveTab] = useState(0);
112
+ const [clientAvatarImageUrl, setClientAvatarImageUrl] = useState('');
113
+ const [mediaRefreshKey, setMediaRefreshKey] = useState(0);
114
+
115
+ useLayoutEffect(() => {
116
+ navigation.setOptions({
117
+ headerTitle: '',
118
+ headerShadowVisible: false,
119
+ headerStyle: { backgroundColor: '#F8FAFC' },
120
+ headerRight: () => null,
121
+ });
122
+ }, [navigation]);
123
+
124
+ useEffect(() => {
125
+ let mounted = true;
126
+ const initialTabIndex = resolveInitialTabIndex({ requestedInitialTab, nextClient: cachedClient, detailContext });
127
+ setActiveTab(initialTabIndex);
128
+
129
+ if (!clientId || !getPeople) {
130
+ setIsLoadingClient(false);
131
+ return () => {
132
+ mounted = false;
133
+ };
134
+ }
135
+
136
+ if (!cachedClient) {
137
+ setIsLoadingClient(true);
138
+ }
139
+
140
+ const loadClientById = async () => {
141
+ const storeMeta = {
142
+ dedupeKey: `client-details-${clientId}`,
143
+ preserveItem: true,
144
+ skipSystemError: true,
145
+ };
146
+
147
+ try {
148
+ return await getPeople({
149
+ id: clientId,
150
+ __storeMeta: storeMeta,
151
+ });
152
+ } catch (error) {
153
+ if (typeof getPeopleItems !== 'function') {
154
+ throw error;
155
+ }
156
+
157
+ const response = await getPeopleItems({
158
+ id: clientId,
159
+ itemsPerPage: 1,
160
+ __storeMeta: storeMeta,
161
+ });
162
+
163
+ return (
164
+ normalizeCollection(response).find(
165
+ item => extractId(item?.id || item?.['@id']) === clientId,
166
+ ) ||
167
+ cachedClient ||
168
+ null
169
+ );
170
+ }
171
+ };
172
+
173
+ const loadClient = async () => {
174
+ if (detailContext !== 'contacts' || !parentCompanyIri) {
175
+ return loadClientById();
176
+ }
177
+
178
+ const [fullClient, peopleLinkResponse] = await Promise.all([
179
+ loadClientById(),
180
+ getPeopleLinks
181
+ ? getPeopleLinks(
182
+ buildPeopleLinkReadParams({
183
+ companyId: parentCompanyId,
184
+ peopleId: clientId,
185
+ }),
186
+ ).catch(() => null)
187
+ : Promise.resolve(null),
188
+ ]);
189
+
190
+ const linkedContact = buildEmployeeContactsFromPeopleLinks(
191
+ peopleLinkResponse,
192
+ {
193
+ parentPeopleId: parentCompanyId,
194
+ },
195
+ )[0];
196
+
197
+ if (!linkedContact) {
198
+ return fullClient;
199
+ }
200
+
201
+ return {
202
+ ...(fullClient || {}),
203
+ linkType: linkedContact.linkType || initialContactLinkType,
204
+ peopleLink: linkedContact.peopleLink,
205
+ };
206
+ };
207
+
208
+ loadClient()
209
+ .then(fullClient => {
210
+ if (!mounted || !fullClient) {
211
+ return;
212
+ }
213
+
214
+ setClient(previousClient => {
215
+ const nextClient = { ...(previousClient || cachedClient || {}), ...fullClient };
216
+ const nextTabIndex = resolveInitialTabIndex({ requestedInitialTab, nextClient, detailContext });
217
+ setActiveTab(nextTabIndex);
218
+ return nextClient;
219
+ });
220
+ })
221
+ .catch(() => {
222
+ if (!mounted) {
223
+ return;
224
+ }
225
+ setClient(cachedClient || null);
226
+ })
227
+ .finally(() => {
228
+ if (mounted) {
229
+ setIsLoadingClient(false);
230
+ }
231
+ });
232
+
233
+ return () => {
234
+ mounted = false;
235
+ };
236
+ }, [
237
+ clientId,
238
+ detailContext,
239
+ getPeople,
240
+ getPeopleItems,
241
+ getPeopleLinks,
242
+ initialContactLinkType,
243
+ parentCompanyId,
244
+ parentCompanyIri,
245
+ requestedInitialTab,
246
+ ]);
247
+
248
+ useEffect(() => {
249
+ let mounted = true;
250
+ const currentClientId = extractId(client?.id || client?.['@id']);
251
+ const isCurrentClientCompany =
252
+ String(client?.peopleType || '').toUpperCase() === 'J';
253
+ const mediaTypes = isCurrentClientCompany
254
+ ? COMPANY_ICON_MEDIA_TYPES
255
+ : PERSON_PHOTO_MEDIA_TYPES;
256
+
257
+ setClientAvatarImageUrl('');
258
+
259
+ if (!currentClientId) {
260
+ return () => {
261
+ mounted = false;
262
+ };
263
+ }
264
+
265
+ resolvePeopleMediaImageUrl({
266
+ peopleId: currentClientId,
267
+ mediaTypes,
268
+ })
269
+ .then(imageUrl => {
270
+ if (!mounted) {
271
+ return;
272
+ }
273
+
274
+ setClientAvatarImageUrl(imageUrl);
275
+ })
276
+ .catch(() => {
277
+ if (mounted) {
278
+ setClientAvatarImageUrl('');
279
+ }
280
+ });
281
+
282
+ return () => {
283
+ mounted = false;
284
+ };
285
+ }, [client?.id, client?.['@id'], client?.peopleType, mediaRefreshKey]);
286
+
287
+ const updateClientData = (field, data) => {
288
+ setClient(prevClient => ({ ...prevClient, [field]: data }));
289
+ };
290
+
291
+ const resolvePeopleMediaImageUrl = useCallback(
292
+ async ({peopleId, mediaTypes}) => {
293
+ for (const mediaType of mediaTypes) {
294
+ const response = await peopleActions.getPeopleMedia({
295
+ people: `/people/${peopleId}`,
296
+ 'mediaType.type': mediaType,
297
+ itemsPerPage: 1,
298
+ }).catch(() => null);
299
+ const media = Array.isArray(response) ? response[0] : null;
300
+ const imageUrl = resolveFileImageUrl(media?.file);
301
+
302
+ if (imageUrl) {
303
+ return imageUrl;
304
+ }
305
+ }
306
+
307
+ return '';
308
+ },
309
+ [peopleActions],
310
+ );
311
+
312
+ const handleClientMediaChanged = useCallback(() => {
313
+ setMediaRefreshKey(previousValue => previousValue + 1);
314
+ }, []);
315
+
316
+ const persistClientData = async partialData => {
317
+ const clientId = extractId(
318
+ client?.id || client?.['@id'],
319
+ );
320
+
321
+ if (!clientId || !savePeople) {
322
+ throw new Error('Nao foi possivel identificar o cliente para salvar.');
323
+ }
324
+
325
+ const payload = {
326
+ id: clientId,
327
+ ...partialData,
328
+ };
329
+
330
+ const saved = await savePeople(payload);
331
+ setClient(prev => ({ ...(prev || {}), ...(saved || {}), ...partialData }));
332
+
333
+ return saved;
334
+ };
335
+
336
+ const isPessoaJuridica = String(client?.peopleType || '').toUpperCase() === 'J';
337
+ const isProviderContext = ['provider', 'providers'].includes(detailContext);
338
+
339
+ const tabs = buildClientTabDefs({
340
+ isPessoaJuridica,
341
+ isProviderContext,
342
+ t: global.t,
343
+ });
344
+
345
+ useEffect(() => {
346
+ if (activeTab > tabs.length - 1) {
347
+ setActiveTab(0);
348
+ }
349
+ }, [activeTab, tabs.length]);
350
+
351
+ const handleTabPress = index => {
352
+ setActiveTab(index);
353
+ };
354
+
355
+ if (isLoadingClient || !client) {
356
+ return <ClientDetailsSkeleton tabs={tabs} />;
357
+ }
358
+
359
+ const tabProps = {
360
+ client,
361
+ customStyles: detailsStyles,
362
+ isEditing: true,
363
+ onUpdateClient: updateClientData,
364
+ onSaveClientData: persistClientData,
365
+ parentCompanyIri,
366
+ initialContactLinkType,
367
+ onChangeClientAvatar: null,
368
+ isSavingClientAvatar: false,
369
+ };
370
+ const activeTabKey = tabs[activeTab]?.key || 'general';
371
+ const activeTabContent = (() => {
372
+ if (activeTabKey === 'general') {
373
+ return <GeneralTab {...tabProps} />;
374
+ }
375
+
376
+ if (activeTabKey === 'fiscal') {
377
+ return (
378
+ <ScrollView
379
+ style={styles.tabScroll}
380
+ contentContainerStyle={styles.tabScrollContent}
381
+ showsVerticalScrollIndicator={false}>
382
+ <FiscalTab {...tabProps} navigation={navigation} />
383
+ </ScrollView>
384
+ );
385
+ }
386
+
387
+ if (activeTabKey === 'media') {
388
+ return (
389
+ <ScrollView
390
+ style={styles.tabScroll}
391
+ contentContainerStyle={inlineStyle_342_16}
392
+ nestedScrollEnabled
393
+ showsVerticalScrollIndicator={false}>
394
+ <MediaTab client={client} onChanged={handleClientMediaChanged} />
395
+ </ScrollView>
396
+ );
397
+ }
398
+
399
+
400
+ if (activeTabKey === 'sellers' || activeTabKey === 'users') {
401
+ return (
402
+ <ScrollView
403
+ style={styles.tabScroll}
404
+ contentContainerStyle={inlineStyle_299_16}
405
+ nestedScrollEnabled
406
+ showsVerticalScrollIndicator={false}>
407
+ {isPessoaJuridica ? (
408
+ <SalesmanTab
409
+ {...tabProps}
410
+ title="Vendedores"
411
+ linkType="sellers-client"
412
+ emptyText="Nenhum vendedor vinculado"
413
+ errorText="Nao foi possivel carregar os vendedores vinculados."
414
+ />
415
+ ) : (
416
+ <UsersTab {...tabProps} />
417
+ )}
418
+ </ScrollView>
419
+ );
420
+ }
421
+
422
+ if (activeTabKey === 'franchise') {
423
+ return (
424
+ <ScrollView
425
+ style={styles.tabScroll}
426
+ contentContainerStyle={inlineStyle_299_16}
427
+ nestedScrollEnabled
428
+ showsVerticalScrollIndicator={false}>
429
+ <FranchiseLinksTab
430
+ {...tabProps}
431
+ emptyText="Nenhuma franquia ou filial vinculada"
432
+ errorText="Não foi possível carregar os vínculos de franquia/filial."
433
+ />
434
+ </ScrollView>
435
+ );
436
+ }
437
+
438
+ if (activeTabKey === 'contacts') {
439
+ return (
440
+ <ScrollView
441
+ style={styles.tabScroll}
442
+ contentContainerStyle={inlineStyle_317_16}
443
+ nestedScrollEnabled
444
+ showsVerticalScrollIndicator={false}>
445
+ <EmployeesTab
446
+ {...tabProps}
447
+ title="Contatos"
448
+ emptyText="Nenhum contato vinculado"
449
+ errorText="Nao foi possivel carregar os contatos vinculados."
450
+ createTitle="Adicionar Contato"
451
+ requiredErrorText="Nome e apelido do contato sao obrigatorios."
452
+ createSuccessText="Contato cadastrado com sucesso."
453
+ createErrorText="Nao foi possivel cadastrar o contato."
454
+ />
455
+ </ScrollView>
456
+ );
457
+ }
458
+
459
+ if (activeTabKey === 'products') {
460
+ return (
461
+ <ScrollView
462
+ style={styles.tabScroll}
463
+ contentContainerStyle={inlineStyle_334_16}
464
+ nestedScrollEnabled
465
+ showsVerticalScrollIndicator={false}>
466
+ <ProductsTab {...tabProps} />
467
+ </ScrollView>
468
+ );
469
+ }
470
+
471
+ return (
472
+ <ScrollView
473
+ style={styles.tabScroll}
474
+ contentContainerStyle={inlineStyle_342_16}
475
+ nestedScrollEnabled
476
+ showsVerticalScrollIndicator={false}>
477
+ <ContractsTab {...tabProps} />
478
+ </ScrollView>
479
+ );
480
+ })();
481
+
482
+ return (
483
+ <SafeAreaView style={styles.container} edges={['bottom']}>
484
+ <View style={styles.headerProfile}>
485
+ <View style={styles.avatarContainer}>
486
+ <PeopleAvatar
487
+ people={client}
488
+ imageUrl={clientAvatarImageUrl}
489
+ name={String(client?.name || '').replace(/\s+/g, ' ').trim()}
490
+ size={64}
491
+ backgroundColor={themeColors.buttonBackground}
492
+ borderColor={themeColors.buttonText}
493
+ borderWidth={3}
494
+ textColor={themeColors.buttonText}
495
+ useGravatar={false}
496
+ usePeopleImage={false}
497
+ />
498
+ </View>
499
+ <Text style={styles.profileName} numberOfLines={1} ellipsizeMode="tail">
500
+ {String(client?.name || '').replace(/\s+/g, ' ').trim()}
501
+ </Text>
502
+
503
+ <Text style={styles.profileId}>{`ID: ${client.id}`}</Text>
504
+ </View>
505
+ <View style={styles.tabsHeader}>
506
+ {tabs.map(tab => (
507
+ <TouchableOpacity
508
+ key={tab.key}
509
+ style={[styles.tabButton, activeTab === tabs.findIndex(item => item.key === tab.key) && styles.tabButtonActive]}
510
+ onPress={() => handleTabPress(tabs.findIndex(item => item.key === tab.key))}>
511
+ <Text
512
+ style={[
513
+ styles.tabButtonText,
514
+ activeTab === tabs.findIndex(item => item.key === tab.key) && styles.tabButtonTextActive,
515
+ ]}>
516
+ {tab.label}
517
+ </Text>
518
+ {activeTab === tabs.findIndex(item => item.key === tab.key) && <View style={styles.activeIndicator} />}
519
+ </TouchableOpacity>
520
+ ))}
521
+ </View>
522
+ <View style={styles.contentContainer}>
523
+ {activeTabContent}
524
+ </View>
525
+ </SafeAreaView>
526
+ );
527
+ };
528
+
529
+ export default ClientDetails;
@@ -0,0 +1,105 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { colors } from '@controleonline/../../src/styles/colors';
3
+
4
+ const styles = StyleSheet.create({
5
+ container: {
6
+ flex: 1,
7
+ backgroundColor: '#F8FAFC',
8
+ },
9
+ headerProfile: {
10
+ alignItems: 'center',
11
+ paddingVertical: 20,
12
+ backgroundColor: '#F8FAFC',
13
+ },
14
+ avatarContainer: {
15
+ position: 'relative',
16
+ marginBottom: 12,
17
+ },
18
+ profileName: {
19
+ fontSize: 20,
20
+ fontWeight: '700',
21
+ color: '#0F172A',
22
+ marginBottom: 4,
23
+ textAlign: 'center',
24
+ maxWidth: '86%',
25
+ },
26
+ profileId: {
27
+ fontSize: 14,
28
+ color: '#64748B',
29
+ fontWeight: '500',
30
+ },
31
+ tabsHeader: {
32
+ flexDirection: 'row',
33
+ backgroundColor: '#fff',
34
+ borderBottomWidth: 1,
35
+ borderBottomColor: '#F1F5F9',
36
+ },
37
+ tabButton: {
38
+ flex: 1,
39
+ alignItems: 'center',
40
+ paddingVertical: 16,
41
+ position: 'relative',
42
+ },
43
+ tabButtonText: {
44
+ fontSize: 14,
45
+ fontWeight: '600',
46
+ color: '#64748B',
47
+ },
48
+ tabButtonTextActive: {
49
+ color: colors.primary,
50
+ fontWeight: '700',
51
+ },
52
+ activeIndicator: {
53
+ position: 'absolute',
54
+ bottom: 0,
55
+ width: '60%',
56
+ height: 3,
57
+ backgroundColor: colors.primary,
58
+ borderTopLeftRadius: 3,
59
+ borderTopRightRadius: 3,
60
+ },
61
+ contentContainer: {
62
+ flex: 1,
63
+ },
64
+ scrollContent: {
65
+ flexGrow: 1,
66
+ },
67
+ tabPane: {
68
+ height: '100%',
69
+ },
70
+ tabScroll: {
71
+ flex: 1,
72
+ },
73
+ skeletonContent: {
74
+ paddingHorizontal: 16,
75
+ paddingTop: 16,
76
+ },
77
+ skeletonCircle: {
78
+ backgroundColor: '#E2E8F0',
79
+ },
80
+ skeletonLine: {
81
+ backgroundColor: '#E2E8F0',
82
+ borderRadius: 8,
83
+ },
84
+ skeletonTab: {
85
+ flex: 1,
86
+ height: 36,
87
+ marginHorizontal: 8,
88
+ marginVertical: 10,
89
+ borderRadius: 10,
90
+ backgroundColor: '#E2E8F0',
91
+ },
92
+ skeletonCard: {
93
+ backgroundColor: '#FFFFFF',
94
+ borderRadius: 14,
95
+ padding: 16,
96
+ marginBottom: 12,
97
+ shadowColor: '#000',
98
+ shadowOffset: { width: 0, height: 2 },
99
+ shadowOpacity: 0.06,
100
+ shadowRadius: 6,
101
+ elevation: 2,
102
+ },
103
+ });
104
+
105
+ export default styles;
@@ -0,0 +1,15 @@
1
+ export const inlineStyle_299_16 = {
2
+ paddingBottom: 80,
3
+ };
4
+
5
+ export const inlineStyle_317_16 = {
6
+ paddingBottom: 80,
7
+ };
8
+
9
+ export const inlineStyle_334_16 = {
10
+ paddingBottom: 80,
11
+ };
12
+
13
+ export const inlineStyle_342_16 = {
14
+ paddingBottom: 80,
15
+ };
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import People from '@controleonline/ui-people/src/react/pages/People';
3
+ import {
4
+ normalizePeopleContextType,
5
+ } from '@controleonline/ui-people/src/react/utils/peopleContext';
6
+ import { normalizeEntityId } from '@controleonline/ui-people/src/react/utils/peopleLinkFilters';
7
+
8
+ const normalizePeopleType = value =>
9
+ String(value ?? '')
10
+ .trim()
11
+ .toUpperCase();
12
+
13
+ export const buildEmployeesContext = routeParams => {
14
+ const normalizedSelectedContext = normalizePeopleContextType(routeParams?.selectedContext);
15
+ const normalizedDefaultContext = normalizePeopleContextType(routeParams?.defaultContext);
16
+ const normalizedContext = Array.isArray(routeParams?.context)
17
+ ? routeParams.context
18
+ : routeParams?.context
19
+ ? [routeParams.context]
20
+ : normalizedSelectedContext
21
+ ? [normalizedSelectedContext]
22
+ : ['all', 'employee', 'owner', 'courier'];
23
+
24
+ const defaultContext =
25
+ normalizedDefaultContext ||
26
+ normalizedSelectedContext ||
27
+ normalizePeopleContextType(normalizedContext[0]) ||
28
+ 'all';
29
+
30
+ const modalTitleByType = {
31
+ employee: 'Cadastro de Funcionario',
32
+ owner: 'Cadastro de Proprietario',
33
+ courier: 'Cadastro de Entregador',
34
+ ...(routeParams?.modalTitleByType || {}),
35
+ };
36
+
37
+ return {
38
+ context: normalizedContext.map(type => normalizePeopleContextType(type)).filter(Boolean),
39
+ defaultContext,
40
+ selectedContext: normalizedSelectedContext || defaultContext,
41
+ defaultPeopleType:
42
+ normalizePeopleType(routeParams?.defaultPeopleType) ||
43
+ (defaultContext === 'courier' ? 'F' : 'J'),
44
+ title: routeParams?.title || global.t?.t('people', 'label', defaultContext),
45
+ searchPlaceholder:
46
+ routeParams?.searchPlaceholder || global.t?.t('people', 'label', defaultContext),
47
+ modalTitleByType,
48
+ detailsRouteName: 'EmployeeDetails',
49
+ useStoreExternalFilter: true,
50
+ detailsRouteParams: (person, selectedLinkType) => ({
51
+ employeeId: normalizeEntityId(person?.id ?? person?.['@id']),
52
+ contextKey: selectedLinkType === 'all' ? '' : String(selectedLinkType || ''),
53
+ }),
54
+ typeSelectorLabel:
55
+ routeParams?.typeSelectorLabel || global.t?.t('people', 'label', 'contactRole'),
56
+ };
57
+ };
58
+
59
+ const Employees = ({route}) => {
60
+ const context = buildEmployeesContext(route?.params || {});
61
+
62
+ return (
63
+ <People
64
+ context={context}
65
+ />
66
+ );
67
+ };
68
+
69
+ export default Employees;