@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,166 @@
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ buildAvailableFranchiseOptions,
4
+ buildFranchiseLinkReadParams,
5
+ extractEntityId,
6
+ buildFranchiseLinksFromPeopleLinks,
7
+ buildFranchiseSavePayload,
8
+ canManageFranchiseLinks,
9
+ franchiseLinkTypeLabel,
10
+ normalizeFranchiseCandidate,
11
+ normalizeFranchiseLink,
12
+ normalizeFranchiseLinkType,
13
+ } from '../../../../react/components/tabs/franchiseLinksTab.helpers.js';
14
+
15
+ assert.deepEqual(buildFranchiseLinkReadParams('/people/11'), {
16
+ company: '11',
17
+ linkType: ['franchisee'],
18
+ itemsPerPage: 100,
19
+ });
20
+ assert.equal('enable' in buildFranchiseLinkReadParams(5), false);
21
+ assert.equal(extractEntityId({id: {'@id': '/people/5'}}), '5');
22
+ assert.equal(extractEntityId('[object Object]'), '');
23
+
24
+ assert.equal(canManageFranchiseLinks('MANAGER'), true);
25
+ assert.equal(canManageFranchiseLinks('manager'), true);
26
+ assert.equal(canManageFranchiseLinks('SHOP'), false);
27
+
28
+ assert.equal(normalizeFranchiseLinkType('franchisee'), 'franchisee');
29
+ assert.equal(normalizeFranchiseLinkType('FILIAL'), 'filial');
30
+ assert.equal(normalizeFranchiseLinkType('other'), '');
31
+
32
+ assert.equal(franchiseLinkTypeLabel('franchisee'), 'Franquia');
33
+ assert.equal(franchiseLinkTypeLabel('filial'), 'Filial');
34
+
35
+ assert.equal(
36
+ normalizeFranchiseCandidate({ id: 1, name: 'ACME', peopleType: 'F' }),
37
+ null,
38
+ );
39
+ assert.deepEqual(
40
+ normalizeFranchiseCandidate({
41
+ id: 10,
42
+ name: 'ACME LTDA',
43
+ alias: 'acme',
44
+ peopleType: 'J',
45
+ }),
46
+ {
47
+ id: '10',
48
+ iri: '/people/10',
49
+ name: 'ACME LTDA',
50
+ alias: 'acme',
51
+ peopleType: 'J',
52
+ },
53
+ );
54
+
55
+ const link = normalizeFranchiseLink({
56
+ id: 5,
57
+ linkType: 'franchisee',
58
+ people: { id: 20, name: 'Franquia X', peopleType: 'J' },
59
+ company: { id: 1 },
60
+ });
61
+ assert.equal(link.linkedId, '20');
62
+ assert.equal(link.linkType, 'franchisee');
63
+ assert.equal(link.linkedName, 'Franquia X');
64
+
65
+ const payload = buildFranchiseSavePayload({
66
+ editingLink: null,
67
+ formData: {
68
+ linkedIri: '/people/20',
69
+ linkType: 'filial',
70
+ comission: '12,5',
71
+ minimumComission: '250',
72
+ },
73
+ companyIri: '/people/1',
74
+ });
75
+ assert.deepEqual(payload, {
76
+ company: '/people/1',
77
+ people: '/people/20',
78
+ linkType: 'filial',
79
+ comission: 12.5,
80
+ minimum_comission: 250,
81
+ enable: true,
82
+ });
83
+
84
+ assert.equal(
85
+ buildFranchiseSavePayload({
86
+ formData: { linkedIri: '', linkType: 'franchisee' },
87
+ companyIri: '/people/1',
88
+ }),
89
+ null,
90
+ );
91
+
92
+ const fromApi = buildFranchiseLinksFromPeopleLinks(
93
+ {
94
+ 'hydra:member': [
95
+ {
96
+ id: 1,
97
+ linkType: 'franchisee',
98
+ company: { id: 100 },
99
+ people: { id: 200, name: 'F1', peopleType: 'J' },
100
+ },
101
+ {
102
+ id: 2,
103
+ linkType: 'employee',
104
+ company: { id: 100 },
105
+ people: { id: 201, name: 'PF', peopleType: 'F' },
106
+ },
107
+ {
108
+ id: 3,
109
+ linkType: 'filial',
110
+ company: { id: 999 },
111
+ people: { id: 300, name: 'Other', peopleType: 'J' },
112
+ },
113
+ ],
114
+ },
115
+ { companyId: 100 },
116
+ );
117
+ assert.equal(fromApi.length, 1);
118
+ assert.equal(fromApi[0].id, 1);
119
+
120
+ const options = buildAvailableFranchiseOptions({
121
+ candidates: [
122
+ { id: 1, name: 'A', peopleType: 'J' },
123
+ { id: 2, name: 'B', peopleType: 'J' },
124
+ ],
125
+ linkedItems: [{ linkedId: '1' }],
126
+ editingLink: null,
127
+ });
128
+ assert.equal(options.length, 1);
129
+ assert.equal(options[0].id, '2');
130
+
131
+ console.log('franchiseLinksTab.helpers.test.mjs OK');
132
+
133
+
134
+ // task-521 regression: API rows with company_id=5 + link_type franchisee must survive
135
+ // build when people is only an id/IRI (staging payload shape).
136
+ const stagingLike = buildFranchiseLinksFromPeopleLinks(
137
+ {
138
+ member: [
139
+ {
140
+ id: 51,
141
+ linkType: 'franchisee',
142
+ company: 5,
143
+ people: 51,
144
+ enable: true,
145
+ },
146
+ {
147
+ id: 52,
148
+ linkType: 'franchisee',
149
+ company: '/people/5',
150
+ people: '/people/52',
151
+ enable: true,
152
+ },
153
+ {
154
+ id: 99,
155
+ linkType: 'employee',
156
+ company: 5,
157
+ people: 7,
158
+ enable: true,
159
+ },
160
+ ],
161
+ },
162
+ { companyId: 5 },
163
+ );
164
+ assert.equal(stagingLike.length, 2);
165
+ assert.equal(extractEntityId(stagingLike[0].people), '51');
166
+ assert.equal(extractEntityId(stagingLike[1].people), '52');
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Unit tests for GeneralTab pure helpers and people_link persistence condition.
3
+ */
4
+ import {
5
+ normalizeEnable,
6
+ normalizeLinkType,
7
+ formatYmdToBr,
8
+ parseBrDateToYmd,
9
+ formatDateInput,
10
+ toPeopleIri,
11
+ extractId,
12
+ LINK_TYPE_OPTIONS,
13
+ } from '../../../components/tabs/generalTabHelpers';
14
+
15
+ describe('generalTabHelpers', () => {
16
+ describe('normalizeEnable', () => {
17
+ it('accepts boolean true/false', () => {
18
+ expect(normalizeEnable(true)).toBe(true);
19
+ expect(normalizeEnable(false)).toBe(false);
20
+ });
21
+ it('accepts numeric 1/0', () => {
22
+ expect(normalizeEnable(1)).toBe(true);
23
+ expect(normalizeEnable(0)).toBe(false);
24
+ });
25
+ it('accepts string "1"/"true"', () => {
26
+ expect(normalizeEnable('1')).toBe(true);
27
+ expect(normalizeEnable('true')).toBe(true);
28
+ expect(normalizeEnable('0')).toBe(false);
29
+ expect(normalizeEnable('false')).toBe(false);
30
+ });
31
+ it('defaults unknown to false', () => {
32
+ expect(normalizeEnable(null)).toBe(false);
33
+ expect(normalizeEnable(undefined)).toBe(false);
34
+ expect(normalizeEnable('')).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('normalizeLinkType', () => {
39
+ it('keeps known link types', () => {
40
+ for (const opt of LINK_TYPE_OPTIONS) {
41
+ expect(normalizeLinkType(opt.value)).toBe(opt.value);
42
+ }
43
+ });
44
+ it('defaults unknown to employee', () => {
45
+ expect(normalizeLinkType('')).toBe('employee');
46
+ expect(normalizeLinkType('unknown')).toBe('employee');
47
+ expect(normalizeLinkType(null)).toBe('employee');
48
+ });
49
+ it('is case-insensitive', () => {
50
+ expect(normalizeLinkType('OWNER')).toBe('owner');
51
+ expect(normalizeLinkType(' Manager ')).toBe('manager');
52
+ });
53
+ });
54
+
55
+ describe('date helpers', () => {
56
+ it('formatYmdToBr converts ISO date', () => {
57
+ expect(formatYmdToBr('2024-03-15')).toBe('15/03/2024');
58
+ expect(formatYmdToBr('2024-03-15T10:00:00Z')).toBe('15/03/2024');
59
+ });
60
+ it('formatDateInput masks progressive input', () => {
61
+ expect(formatDateInput('15')).toBe('15');
62
+ expect(formatDateInput('1503')).toBe('15/03');
63
+ expect(formatDateInput('15032024')).toBe('15/03/2024');
64
+ });
65
+ it('parseBrDateToYmd round-trips valid date', () => {
66
+ expect(parseBrDateToYmd('15/03/2024')).toBe('2024-03-15');
67
+ });
68
+ it('parseBrDateToYmd rejects invalid date', () => {
69
+ expect(parseBrDateToYmd('32/13/2024')).toBeNull();
70
+ expect(parseBrDateToYmd('incomplete')).toBeNull();
71
+ });
72
+ });
73
+
74
+ describe('toPeopleIri / extractId', () => {
75
+ it('builds IRI from id', () => {
76
+ expect(toPeopleIri({ id: 42 })).toBe('/people/42');
77
+ });
78
+ it('keeps existing @id IRI', () => {
79
+ expect(toPeopleIri({ '@id': '/people/7' })).toBe('/people/7');
80
+ });
81
+ it('extractId strips non-digits', () => {
82
+ expect(extractId('/people/99')).toBe('99');
83
+ expect(extractId(null)).toBe('');
84
+ });
85
+ });
86
+
87
+ describe('people_link persistence condition (task-330)', () => {
88
+ /**
89
+ * Mirrors the condition used in GeneralTab handleSaveRegistration:
90
+ * only call people_link.save when canEditLinkType AND linkType actually changed.
91
+ * This avoids Method Not Allowed on the GetCollection-only PeopleLink API
92
+ * when the user only toggles enable/status.
93
+ */
94
+ const shouldPersistPeopleLink = ({ canEditLinkType, currentLinkType, originalLinkType }) =>
95
+ Boolean(
96
+ canEditLinkType &&
97
+ normalizeLinkType(currentLinkType) !== normalizeLinkType(originalLinkType),
98
+ );
99
+
100
+ it('does NOT persist when only enable changes (linkType same)', () => {
101
+ expect(
102
+ shouldPersistPeopleLink({
103
+ canEditLinkType: true,
104
+ currentLinkType: 'employee',
105
+ originalLinkType: 'employee',
106
+ }),
107
+ ).toBe(false);
108
+ });
109
+
110
+ it('persists when linkType actually changes', () => {
111
+ expect(
112
+ shouldPersistPeopleLink({
113
+ canEditLinkType: true,
114
+ currentLinkType: 'manager',
115
+ originalLinkType: 'employee',
116
+ }),
117
+ ).toBe(true);
118
+ });
119
+
120
+ it('does NOT persist when canEditLinkType is false', () => {
121
+ expect(
122
+ shouldPersistPeopleLink({
123
+ canEditLinkType: false,
124
+ currentLinkType: 'owner',
125
+ originalLinkType: 'employee',
126
+ }),
127
+ ).toBe(false);
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,322 @@
1
+ const React = require('react');
2
+ const renderer = require('react-test-renderer');
3
+ const {jest} = require('@jest/globals');
4
+
5
+ const {describe, expect, it, beforeEach} = global;
6
+
7
+ global.IS_REACT_ACT_ENVIRONMENT = true;
8
+
9
+ const peopleActions = {
10
+ getMediaTypes: jest.fn(),
11
+ getPeopleMedia: jest.fn(),
12
+ savePeopleMedia: jest.fn(),
13
+ uploadPeopleMedia: jest.fn(),
14
+ deletePeopleMedia: jest.fn(),
15
+ setItem: jest.fn(),
16
+ };
17
+
18
+ const peopleLinkActions = {
19
+ getItems: jest.fn(),
20
+ };
21
+
22
+ const messageActions = {
23
+ showError: jest.fn(),
24
+ showSuccess: jest.fn(),
25
+ };
26
+
27
+ jest.mock('@store', () => ({
28
+ useStore: jest.fn(name => {
29
+ if (name === 'people') {
30
+ return {
31
+ actions: peopleActions,
32
+ getters: {
33
+ currentCompany: {id: 12},
34
+ },
35
+ };
36
+ }
37
+
38
+ if (name === 'people_link') {
39
+ return {
40
+ actions: peopleLinkActions,
41
+ getters: {},
42
+ };
43
+ }
44
+
45
+ if (name === 'theme') {
46
+ return {
47
+ getters: {
48
+ colors: {},
49
+ },
50
+ };
51
+ }
52
+
53
+ return {actions: {}, getters: {}};
54
+ }),
55
+ useStores: jest.fn(() => ({
56
+ actions: peopleActions,
57
+ getters: {
58
+ currentCompany: {id: 12},
59
+ },
60
+ })),
61
+ }));
62
+
63
+ jest.mock('@react-navigation/native', () => ({
64
+ useNavigation: () => ({
65
+ push: jest.fn(),
66
+ }),
67
+ }));
68
+
69
+ jest.mock('react-native', () => ({
70
+ ActivityIndicator: props => React.createElement('activityindicator', props, props.children),
71
+ Alert: {
72
+ alert: jest.fn(),
73
+ },
74
+ Keyboard: {
75
+ dismiss: jest.fn(),
76
+ },
77
+ Platform: {OS: 'web'},
78
+ ScrollView: props => React.createElement('scrollview', props, props.children),
79
+ StyleSheet: {
80
+ create: styles => styles,
81
+ },
82
+ Text: props => React.createElement('text', props, props.children),
83
+ TextInput: props => React.createElement('textinput', props, props.children),
84
+ TouchableOpacity: props => React.createElement('touchableopacity', props, props.children),
85
+ View: props => React.createElement('view', props, props.children),
86
+ }));
87
+
88
+ jest.mock('react-native-vector-icons/Feather', () => props =>
89
+ React.createElement('icon', props, props.children),
90
+ {virtual: true},
91
+ );
92
+
93
+ jest.mock('react-native-vector-icons/FontAwesome', () => props =>
94
+ React.createElement('icon', props, props.children),
95
+ {virtual: true},
96
+ );
97
+
98
+ jest.mock('react-native-vector-icons/MaterialIcons', () => props =>
99
+ React.createElement('icon', props, props.children),
100
+ {virtual: true},
101
+ );
102
+
103
+ jest.mock('@react-native-picker/picker', () => ({
104
+ Picker: props => React.createElement('picker', props, props.children),
105
+ }), {virtual: true});
106
+
107
+ jest.mock('@controleonline/ui-common/src/react/components/AnimatedModal', () => props =>
108
+ (props.visible ? React.createElement('modal', props, props.children) : null),
109
+ );
110
+
111
+ jest.mock('@controleonline/ui-common/src/react/components/MessageService', () => ({
112
+ useMessage: () => messageActions,
113
+ }));
114
+
115
+ jest.mock('@controleonline/ui-common/src/react/components/UserAvatar', () => props =>
116
+ React.createElement('avatar', props, props.children),
117
+ );
118
+
119
+ jest.mock('@controleonline/ui-default/src/react/components/upload/DefaultUpload', () => props =>
120
+ React.createElement('defaultupload', props, props.children),
121
+ );
122
+
123
+ jest.mock('@controleonline/ui-default/src/react/components/upload/fileUpload', () => ({
124
+ extractFileId: value => {
125
+ if (!value && value !== 0) return null;
126
+ const raw = typeof value === 'string' ? value : value?.id || value?.['@id'];
127
+ const match = String(raw || '').match(/(\d+)$/);
128
+ return match ? parseInt(match[1], 10) : null;
129
+ },
130
+ }));
131
+
132
+ jest.mock('@controleonline/ui-common/src/react/utils/fileUrl', () => ({
133
+ resolveFileImageUrl: () => 'https://example.test/file.png',
134
+ }));
135
+
136
+ const SalesmanTab =
137
+ require('@controleonline/ui-customers/src/react/components/tabs/SalesmanTab').default;
138
+ const MediaTab =
139
+ require('@controleonline/ui-customers/src/react/components/tabs/MediaTab').default;
140
+ const employeesTabModule =
141
+ require('@controleonline/ui-customers/src/react/components/tabs/EmployeesTab');
142
+ const EmployeesTab = employeesTabModule.default;
143
+ const {
144
+ formatEmployeeContactMeta,
145
+ formatEmployeeContactTitle,
146
+ } = employeesTabModule;
147
+
148
+ const flush = () => new Promise(resolve => setImmediate(resolve));
149
+
150
+ describe('people media tabs', () => {
151
+ beforeEach(() => {
152
+ jest.clearAllMocks();
153
+ });
154
+
155
+ it('loads company icons through the people store on SalesmanTab', async () => {
156
+ peopleLinkActions.getItems.mockResolvedValue([
157
+ {
158
+ id: 1,
159
+ linkType: 'sellers-client',
160
+ people: {id: '/people/12'},
161
+ company: {id: '/people/55', name: 'Companhia 55'},
162
+ },
163
+ ]);
164
+ peopleActions.getPeopleMedia.mockResolvedValue([
165
+ {
166
+ id: 9,
167
+ file: {id: 91, fileName: 'icon.png'},
168
+ },
169
+ ]);
170
+
171
+ await renderer.act(async () => {
172
+ renderer.create(
173
+ React.createElement(SalesmanTab, {
174
+ client: {id: 12},
175
+ customStyles: {
176
+ tabContent: {},
177
+ section: {},
178
+ loadingIndicator: {color: '#000'},
179
+ emptyText: {},
180
+ listItem: {},
181
+ listItemWithEndAction: {},
182
+ itemContent: {},
183
+ listAvatarBrand: {backgroundColor: '#fff', borderColor: '#ddd'},
184
+ listAvatarText: {color: '#000'},
185
+ listAvatar: {},
186
+ itemText: {},
187
+ itemSubtext: {},
188
+ iconButtonGhost: {},
189
+ iconButtonGhostIcon: {color: '#000'},
190
+ },
191
+ linkType: 'sellers-client',
192
+ emptyText: 'empty',
193
+ errorText: 'error',
194
+ }),
195
+ );
196
+ await flush();
197
+ });
198
+
199
+ expect(peopleActions.getPeopleMedia).toHaveBeenCalledWith({
200
+ people: '/people/55',
201
+ 'mediaType.type': 'icon',
202
+ itemsPerPage: 1,
203
+ });
204
+ });
205
+
206
+ it('loads avatars through the people store on EmployeesTab', async () => {
207
+ peopleLinkActions.getItems.mockResolvedValue([
208
+ {
209
+ id: 2,
210
+ linkType: 'employee',
211
+ people: {id: '/people/77', peopleType: 'F'},
212
+ company: {id: '/people/12'},
213
+ },
214
+ ]);
215
+ peopleActions.getPeopleMedia.mockResolvedValue([
216
+ {
217
+ id: 10,
218
+ file: {id: 101, fileName: 'avatar.png'},
219
+ },
220
+ ]);
221
+
222
+ await renderer.act(async () => {
223
+ renderer.create(
224
+ React.createElement(EmployeesTab, {
225
+ client: {id: 12},
226
+ customStyles: {
227
+ tabContent: {},
228
+ section: {},
229
+ sectionHeader: {},
230
+ sectionTitle: {},
231
+ iconButtonPrimary: {},
232
+ iconButtonPrimaryIcon: {color: '#000'},
233
+ loadingIndicator: {color: '#000'},
234
+ emptyText: {},
235
+ txt_title_emptyText: {},
236
+ listItem: {},
237
+ listItemWithEndAction: {},
238
+ itemContent: {},
239
+ listAvatarBrand: {backgroundColor: '#fff', borderColor: '#ddd'},
240
+ listAvatarText: {color: '#000'},
241
+ listAvatar: {},
242
+ itemText: {},
243
+ itemSubtext: {},
244
+ iconButtonGhost: {},
245
+ iconButtonGhostIcon: {color: '#000'},
246
+ saveButton: {},
247
+ saveButtonDisabled: {},
248
+ saveButtonText: {color: '#000'},
249
+ },
250
+ }),
251
+ );
252
+ await flush();
253
+ });
254
+
255
+ expect(peopleActions.getPeopleMedia).toHaveBeenCalledWith({
256
+ people: '/people/77',
257
+ 'mediaType.type': 'avatar',
258
+ itemsPerPage: 1,
259
+ });
260
+ });
261
+
262
+ it('formats contact rows as name alias and id link type', () => {
263
+ const contact = {
264
+ id: '/people/77',
265
+ name: 'Maria Silva',
266
+ alias: 'Mari',
267
+ linkType: 'employee',
268
+ peopleLink: {
269
+ linkType: 'owner',
270
+ },
271
+ };
272
+
273
+ expect(formatEmployeeContactTitle(contact)).toBe('MARIA SILVA / MARI');
274
+ expect(formatEmployeeContactMeta(contact)).toBe('ID: 77 / owner');
275
+ });
276
+
277
+ it('uses the generic upload flow before saving client JPG/PNG media', async () => {
278
+ peopleActions.getMediaTypes.mockResolvedValue([
279
+ {
280
+ id: 12,
281
+ '@id': '/media_types/12',
282
+ type: 'icon',
283
+ peopleType: 'J',
284
+ },
285
+ ]);
286
+ peopleActions.getPeopleMedia.mockResolvedValue([]);
287
+ peopleActions.savePeopleMedia.mockResolvedValue({id: 130});
288
+
289
+ let tree;
290
+
291
+ await renderer.act(async () => {
292
+ tree = renderer.create(
293
+ React.createElement(MediaTab, {
294
+ client: {
295
+ id: 11,
296
+ '@id': '/people/11',
297
+ peopleType: 'J',
298
+ },
299
+ }),
300
+ );
301
+ await flush();
302
+ });
303
+
304
+ const upload = tree.root.findByType('defaultupload');
305
+
306
+ expect(upload.props.acceptedTypes).toBe('image/png,image/jpeg,.png,.jpg,.jpeg');
307
+ expect(upload.props.onUploadFile).toBeUndefined();
308
+ expect(upload.props.uploadResultAlreadyAttached).toBeUndefined();
309
+
310
+ await renderer.act(async () => {
311
+ await upload.props.onAttachFile({id: 77, name: 'cliente.jpg'});
312
+ });
313
+
314
+ expect(peopleActions.savePeopleMedia).toHaveBeenCalledWith({
315
+ id: undefined,
316
+ people: '/people/11',
317
+ mediaType: '/media_types/12',
318
+ file: '/files/77',
319
+ });
320
+ expect(peopleActions.uploadPeopleMedia).not.toHaveBeenCalled();
321
+ });
322
+ });
@@ -0,0 +1,75 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import {
4
+ buildAvailableSalesmanOptions,
5
+ buildSalesmanSavePayload,
6
+ canManageSalesmen,
7
+ formatCommissionLabel,
8
+ normalizeCommissionValue,
9
+ normalizeSalesmanLink,
10
+ toPeopleIri,
11
+ } from '../../../../react/components/tabs/salesmanTab.helpers.js';
12
+
13
+ describe('salesmanTab.helpers', () => {
14
+ it('gates management to MANAGER app type only', () => {
15
+ assert.equal(canManageSalesmen('MANAGER'), true);
16
+ assert.equal(canManageSalesmen('manager'), true);
17
+ assert.equal(canManageSalesmen('CRM'), false);
18
+ assert.equal(canManageSalesmen(''), false);
19
+ });
20
+
21
+ it('normalizes commission values and labels', () => {
22
+ assert.equal(normalizeCommissionValue('12,5'), 12.5);
23
+ assert.equal(normalizeCommissionValue('abc'), 0);
24
+ assert.equal(formatCommissionLabel(10), '10,00%');
25
+ });
26
+
27
+ it('builds people IRI and normalizes links', () => {
28
+ assert.equal(toPeopleIri({ id: 7 }), '/people/7');
29
+ assert.equal(toPeopleIri('/people/9'), '/people/9');
30
+ const link = normalizeSalesmanLink({
31
+ id: 3,
32
+ company: { id: 11, name: 'Ana' },
33
+ comission: '5',
34
+ minimum_comission: '1',
35
+ });
36
+ assert.equal(link.sellerId, '11');
37
+ assert.equal(link.commission, 5);
38
+ assert.equal(link.minimumCommission, 1);
39
+ });
40
+
41
+ it('filters already linked salesmen from options', () => {
42
+ const options = buildAvailableSalesmanOptions({
43
+ salesmen: [
44
+ { id: 1, name: 'A' },
45
+ { id: 2, name: 'B' },
46
+ ],
47
+ linkedSalesmen: [{ sellerId: '1' }],
48
+ editingLink: null,
49
+ });
50
+ assert.equal(options.length, 1);
51
+ assert.equal(options[0].id, '2');
52
+ });
53
+
54
+ it('builds save payload for create and update', () => {
55
+ const created = buildSalesmanSavePayload({
56
+ editingLink: null,
57
+ formData: { sellerIri: '/people/5', commission: '8', minimumCommission: '2' },
58
+ clientIri: '/people/20',
59
+ linkType: 'sellers-client',
60
+ });
61
+ assert.equal(created.company, '/people/5');
62
+ assert.equal(created.people, '/people/20');
63
+ assert.equal(created.comission, 8);
64
+ assert.equal(created.minimum_comission, 2);
65
+ assert.equal(created.id, undefined);
66
+
67
+ const updated = buildSalesmanSavePayload({
68
+ editingLink: { id: '99' },
69
+ formData: { sellerIri: '/people/5', commission: '8', minimumCommission: '2' },
70
+ clientIri: '/people/20',
71
+ linkType: 'sellers-client',
72
+ });
73
+ assert.equal(updated.id, '99');
74
+ });
75
+ });