@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,266 @@
1
+ const {describe, expect, it} = global
2
+
3
+ const {
4
+ buildEmployeeContactsFromPeopleLinks,
5
+ buildPeopleLinkReadParams,
6
+ buildSalesmanLinksFromPeopleLinks,
7
+ extractPeopleLinkItems,
8
+ filterPeopleLinksByScope,
9
+ normalizeEmployeeLinkType,
10
+ } = require('../../../../react/components/tabs/employeeContacts')
11
+
12
+ describe('employeeContacts', () => {
13
+ it('reads collection members from hydra payloads', () => {
14
+ expect(
15
+ extractPeopleLinkItems({
16
+ 'hydra:member': [{id: 1}],
17
+ }),
18
+ ).toEqual([{id: 1}])
19
+ })
20
+
21
+ it('normalizes supported link types and falls back to employee', () => {
22
+ expect(normalizeEmployeeLinkType('OWNER')).toBe('owner')
23
+ expect(normalizeEmployeeLinkType('courier')).toBe('courier')
24
+ expect(normalizeEmployeeLinkType('unknown')).toBe('employee')
25
+ })
26
+
27
+ it('builds numeric relationship filters for collection reads', () => {
28
+ expect(
29
+ buildPeopleLinkReadParams({
30
+ companyId: '/people/29',
31
+ peopleId: '/people/31',
32
+ linkType: 'sellers-client',
33
+ }),
34
+ ).toEqual({
35
+ company: '29',
36
+ people: '31',
37
+ linkType: ['sellers-client'],
38
+ })
39
+ })
40
+
41
+ it('accepts linkTypes allowlist and itemsPerPage for contact reads (#636)', () => {
42
+ expect(
43
+ buildPeopleLinkReadParams({
44
+ companyId: 3,
45
+ linkTypes: ['employee', 'owner', 'director', 'manager', 'courier'],
46
+ itemsPerPage: 100,
47
+ }),
48
+ ).toEqual({
49
+ company: '3',
50
+ linkType: ['employee', 'owner', 'director', 'manager', 'courier'],
51
+ itemsPerPage: 100,
52
+ })
53
+ })
54
+
55
+ it('maps employee contacts when people is a plain IRI string (#636)', () => {
56
+ expect(
57
+ buildEmployeeContactsFromPeopleLinks(
58
+ {
59
+ member: [
60
+ {
61
+ id: 3229,
62
+ linkType: 'employee',
63
+ company: '/people/3',
64
+ people: '/people/105789',
65
+ },
66
+ {
67
+ id: 7,
68
+ linkType: 'owner',
69
+ company: {id: 3},
70
+ people: {
71
+ id: 7,
72
+ name: 'Owner',
73
+ peopleType: 'F',
74
+ },
75
+ },
76
+ ],
77
+ },
78
+ {parentPeopleId: '3'},
79
+ ),
80
+ ).toEqual([
81
+ expect.objectContaining({
82
+ id: '105789',
83
+ linkType: 'employee',
84
+ peopleLink: expect.objectContaining({id: 3229}),
85
+ }),
86
+ expect.objectContaining({
87
+ id: '7',
88
+ linkType: 'owner',
89
+ name: 'Owner',
90
+ }),
91
+ ])
92
+ })
93
+
94
+ it('rejects links returned outside the requested relationship scope', () => {
95
+ const requestedLink = {
96
+ id: 11,
97
+ company: {id: 29},
98
+ people: {id: 31},
99
+ linkType: 'employee',
100
+ }
101
+ const unrelatedLink = {
102
+ id: 12,
103
+ company: {id: 2},
104
+ people: {id: 31},
105
+ linkType: 'employee',
106
+ }
107
+
108
+ expect(
109
+ filterPeopleLinksByScope([requestedLink, unrelatedLink], {
110
+ companyId: 29,
111
+ peopleId: 31,
112
+ linkTypes: ['employee'],
113
+ }),
114
+ ).toEqual([requestedLink])
115
+ })
116
+
117
+ it('keeps only salesman links whose client matches the open details page', () => {
118
+ const requestedLink = {
119
+ id: 21,
120
+ company: {id: 6, name: 'Alexandre'},
121
+ people: {id: 29},
122
+ linkType: 'sellers-client',
123
+ }
124
+ const unrelatedLink = {
125
+ id: 22,
126
+ company: {id: 6, name: 'Alexandre'},
127
+ people: {id: 11},
128
+ linkType: 'sellers-client',
129
+ }
130
+
131
+ expect(
132
+ buildSalesmanLinksFromPeopleLinks([requestedLink, unrelatedLink], {
133
+ clientId: 29,
134
+ }),
135
+ ).toEqual([requestedLink])
136
+ })
137
+
138
+ it('maps people links to physical contacts for the customer details tab', () => {
139
+ expect(
140
+ buildEmployeeContactsFromPeopleLinks(
141
+ {
142
+ member: [
143
+ {
144
+ id: 31,
145
+ linkType: 'owner',
146
+ company: {'@id': '/people/31'},
147
+ people: {
148
+ id: 30,
149
+ '@id': '/people/30',
150
+ name: 'Claudia',
151
+ alias: 'Claudia',
152
+ peopleType: 'F',
153
+ },
154
+ },
155
+ {
156
+ id: 32,
157
+ linkType: 'employee',
158
+ company: {'@id': '/people/31'},
159
+ people: {
160
+ id: 33,
161
+ '@id': '/people/33',
162
+ name: 'Filial',
163
+ alias: 'Filial',
164
+ peopleType: 'J',
165
+ },
166
+ },
167
+ {
168
+ id: 34,
169
+ linkType: 'courier',
170
+ company: {'@id': '/people/31'},
171
+ people: {
172
+ id: 35,
173
+ '@id': '/people/35',
174
+ name: 'Rafael',
175
+ alias: 'Rafa',
176
+ peopleType: 'F',
177
+ },
178
+ },
179
+ {
180
+ id: 36,
181
+ linkType: 'employee',
182
+ company: {'@id': '/people/99'},
183
+ people: {
184
+ id: 37,
185
+ '@id': '/people/37',
186
+ name: 'Contato de outra empresa',
187
+ alias: 'Outra',
188
+ peopleType: 'F',
189
+ },
190
+ },
191
+ ],
192
+ },
193
+ {parentPeopleId: '31'},
194
+ ),
195
+ ).toEqual([
196
+ expect.objectContaining({
197
+ id: 30,
198
+ '@id': '/people/30',
199
+ name: 'Claudia',
200
+ alias: 'Claudia',
201
+ peopleType: 'F',
202
+ linkType: 'owner',
203
+ peopleLink: expect.objectContaining({
204
+ id: 31,
205
+ linkType: 'owner',
206
+ }),
207
+ }),
208
+ expect.objectContaining({
209
+ id: 35,
210
+ '@id': '/people/35',
211
+ name: 'Rafael',
212
+ alias: 'Rafa',
213
+ peopleType: 'F',
214
+ linkType: 'courier',
215
+ peopleLink: expect.objectContaining({
216
+ id: 34,
217
+ linkType: 'courier',
218
+ }),
219
+ }),
220
+ ])
221
+ })
222
+
223
+ it('filters unsupported link types when an allowlist is provided', () => {
224
+ expect(
225
+ buildEmployeeContactsFromPeopleLinks(
226
+ {
227
+ member: [
228
+ {
229
+ id: 40,
230
+ linkType: 'employee',
231
+ company: {'@id': '/people/31'},
232
+ people: {
233
+ id: 41,
234
+ '@id': '/people/41',
235
+ name: 'Marina',
236
+ alias: 'Mari',
237
+ peopleType: 'F',
238
+ },
239
+ },
240
+ {
241
+ id: 42,
242
+ linkType: 'client',
243
+ company: {'@id': '/people/31'},
244
+ people: {
245
+ id: 43,
246
+ '@id': '/people/43',
247
+ name: 'Visitante',
248
+ alias: 'Visit',
249
+ peopleType: 'F',
250
+ },
251
+ },
252
+ ],
253
+ },
254
+ {
255
+ parentPeopleId: '31',
256
+ allowedLinkTypes: ['employee', 'owner'],
257
+ },
258
+ ),
259
+ ).toEqual([
260
+ expect.objectContaining({
261
+ id: 41,
262
+ linkType: 'employee',
263
+ }),
264
+ ])
265
+ })
266
+ })
@@ -0,0 +1,67 @@
1
+ import {
2
+ extractEmployeeUnlinkIds,
3
+ filterEmployeesAfterUnlink,
4
+ removeEmployeeFromCompany,
5
+ confirmEmployeeRemoval,
6
+ EMPLOYEE_UNLINK_MESSAGE,
7
+ } from '../../../../react/components/tabs/employeeUnlink';
8
+
9
+ describe('employeeUnlink', () => {
10
+ it('extracts people and people_link ids', () => {
11
+ expect(
12
+ extractEmployeeUnlinkIds({
13
+ id: '/people/80',
14
+ peopleLinkId: '/people_links/201',
15
+ }),
16
+ ).toEqual({peopleId: '80', linkId: '201'});
17
+ });
18
+
19
+ it('prefers people.remove and falls back to people_link.remove', async () => {
20
+ const removePeople = jest.fn().mockResolvedValue(null);
21
+ const removePeopleLink = jest.fn();
22
+ await expect(
23
+ removeEmployeeFromCompany({
24
+ peopleId: '80',
25
+ linkId: '201',
26
+ removePeople,
27
+ removePeopleLink,
28
+ }),
29
+ ).resolves.toEqual({mode: 'people', peopleId: '80', linkId: '201'});
30
+ expect(removePeople).toHaveBeenCalledWith('80');
31
+ expect(removePeopleLink).not.toHaveBeenCalled();
32
+
33
+ await expect(
34
+ removeEmployeeFromCompany({
35
+ peopleId: '',
36
+ linkId: '201',
37
+ removePeople: undefined,
38
+ removePeopleLink: jest.fn().mockResolvedValue(null),
39
+ }),
40
+ ).resolves.toMatchObject({mode: 'link', linkId: '201'});
41
+ });
42
+
43
+ it('filters the local list after unlink', () => {
44
+ const next = filterEmployeesAfterUnlink(
45
+ [
46
+ {id: 80, peopleLink: {id: 201}},
47
+ {id: 81, peopleLink: {id: 202}},
48
+ ],
49
+ {peopleId: '80', linkId: '201'},
50
+ );
51
+ expect(next.map(item => item.id)).toEqual([81]);
52
+ });
53
+
54
+ it('uses MessageService dialog when available', () => {
55
+ const showDialog = jest.fn();
56
+ const onConfirm = jest.fn();
57
+ confirmEmployeeRemoval({showDialog, onConfirm});
58
+ expect(showDialog).toHaveBeenCalledWith(
59
+ expect.objectContaining({
60
+ title: 'Remover colaborador',
61
+ message: EMPLOYEE_UNLINK_MESSAGE,
62
+ confirmLabel: 'Remover',
63
+ onConfirm,
64
+ }),
65
+ );
66
+ });
67
+ });
@@ -0,0 +1,78 @@
1
+ import {
2
+ extractPeopleMediaUrl,
3
+ fetchPeopleMediaUrls,
4
+ } from '../../../../react/components/tabs/employeesTabHelpers';
5
+
6
+ jest.mock('@controleonline/ui-common/src/react/utils/fileUrl', () => ({
7
+ resolveFileImageUrl: file => (file?.url ? file.url : file?.['@id'] || ''),
8
+ }), { virtual: true });
9
+
10
+ jest.mock('@controleonline/ui-common/src/react/utils/entityDisplay', () => ({
11
+ formatDisplayUppercase: value => String(value || '').toUpperCase(),
12
+ }), { virtual: true });
13
+
14
+ describe('fetchPeopleMediaUrls batch', () => {
15
+ it('issues a single getPeopleMedia call for multiple people ids', async () => {
16
+ const getPeopleMedia = jest.fn().mockResolvedValue([
17
+ { people: { '@id': '/people/10' }, file: { url: 'https://cdn/a.png' } },
18
+ { people: { id: 20 }, file: { url: 'https://cdn/b.png' } },
19
+ ]);
20
+
21
+ const result = await fetchPeopleMediaUrls({
22
+ peopleActions: { getPeopleMedia },
23
+ mediaType: 'avatar',
24
+ peopleIds: [10, '20', '/people/10'],
25
+ });
26
+
27
+ expect(getPeopleMedia).toHaveBeenCalledTimes(1);
28
+ expect(getPeopleMedia).toHaveBeenCalledWith({
29
+ people: ['/people/10', '/people/20'],
30
+ 'mediaType.type': 'avatar',
31
+ itemsPerPage: 2,
32
+ });
33
+ expect(result).toEqual({
34
+ '10': 'https://cdn/a.png',
35
+ '20': 'https://cdn/b.png',
36
+ });
37
+ });
38
+
39
+ it('falls back to per-id calls when batch rejects', async () => {
40
+ const getPeopleMedia = jest
41
+ .fn()
42
+ .mockRejectedValueOnce(new Error('batch unsupported'))
43
+ .mockResolvedValueOnce([{ people: { id: 1 }, file: { url: 'https://cdn/1.png' } }])
44
+ .mockResolvedValueOnce([{ people: { id: 2 }, file: { url: 'https://cdn/2.png' } }]);
45
+
46
+ const result = await fetchPeopleMediaUrls({
47
+ peopleActions: { getPeopleMedia },
48
+ mediaType: 'avatar',
49
+ peopleIds: [1, 2],
50
+ });
51
+
52
+ expect(getPeopleMedia).toHaveBeenCalledTimes(3);
53
+ expect(result).toEqual({
54
+ '1': 'https://cdn/1.png',
55
+ '2': 'https://cdn/2.png',
56
+ });
57
+ });
58
+ });
59
+
60
+ describe('extractPeopleMediaUrl from people_links payload', () => {
61
+ it('reads avatar file from embedded peopleMedia', () => {
62
+ const url = extractPeopleMediaUrl({
63
+ id: 10,
64
+ peopleMedia: [
65
+ {
66
+ mediaType: { type: 'avatar' },
67
+ file: { id: 99, url: 'https://cdn/avatar.png' },
68
+ },
69
+ ],
70
+ });
71
+ expect(url).toBe('https://cdn/avatar.png');
72
+ });
73
+
74
+ it('returns empty string when peopleMedia is missing', () => {
75
+ expect(extractPeopleMediaUrl({ id: 10 })).toBe('');
76
+ expect(extractPeopleMediaUrl(null)).toBe('');
77
+ });
78
+ });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Unit tests for EmployeesTab pure helpers (app-community#297 modularization).
3
+ * Includes navigation contract for employee detail (app-community#9).
4
+ */
5
+ import {
6
+ formatDateInput,
7
+ parseBrDateToYmd,
8
+ buildEmployeeCreatePayload,
9
+ extractId,
10
+ LINK_TYPE_OPTIONS,
11
+ buildEmployeeDetailNavParams,
12
+ } from '../../../../react/components/tabs/employeesTabHelpers';
13
+
14
+ describe('employeesTabHelpers', () => {
15
+ it('formatDateInput masks BR date', () => {
16
+ expect(formatDateInput('01022020')).toBe('01/02/2020');
17
+ expect(formatDateInput('01')).toBe('01');
18
+ });
19
+
20
+ it('parseBrDateToYmd validates and converts', () => {
21
+ expect(parseBrDateToYmd('01/02/2020')).toBe('2020-02-01');
22
+ expect(parseBrDateToYmd('31/02/2020')).toBeNull();
23
+ expect(parseBrDateToYmd('1/2/20')).toBeNull();
24
+ });
25
+
26
+ it('buildEmployeeCreatePayload sets company and optional foundationDate', () => {
27
+ const payload = buildEmployeeCreatePayload({
28
+ name: 'ANA',
29
+ alias: 'A',
30
+ foundationDate: '2020-02-01',
31
+ linkType: 'manager',
32
+ parentPeopleId: '15',
33
+ });
34
+ expect(payload.company).toBe('/people/15');
35
+ expect(payload.foundationDate).toBe('2020-02-01');
36
+ expect(payload.linkType).toBe('manager');
37
+ expect(payload.peopleType).toBe('F');
38
+ });
39
+
40
+ it('extractId and LINK_TYPE_OPTIONS', () => {
41
+ expect(extractId('/people/3')).toBe('3');
42
+ expect(LINK_TYPE_OPTIONS.some(o => o.value === 'employee')).toBe(true);
43
+ });
44
+
45
+ it('buildEmployeeDetailNavParams opens employee detail on general tab', () => {
46
+ const params = buildEmployeeDetailNavParams({
47
+ employee: { id: '/people/42', linkType: 'employee' },
48
+ parentPeopleId: '15',
49
+ });
50
+ expect(params).toEqual({
51
+ clientId: '42',
52
+ contextKey: 'contacts',
53
+ initialTab: 'general',
54
+ parentCompanyId: '15',
55
+ linkType: 'employee',
56
+ });
57
+ });
58
+
59
+ it('buildEmployeeDetailNavParams returns null without employee id', () => {
60
+ expect(
61
+ buildEmployeeDetailNavParams({ employee: {}, parentPeopleId: '15' }),
62
+ ).toBeNull();
63
+ });
64
+ });
@@ -0,0 +1,29 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const fiscalTab = fs.readFileSync(
6
+ new URL('../../../../react/components/tabs/FiscalTab.js', import.meta.url),
7
+ 'utf8',
8
+ );
9
+ const details = fs.readFileSync(
10
+ new URL('../../../../react/pages/details.js', import.meta.url),
11
+ 'utf8',
12
+ );
13
+ const helpers = fs.readFileSync(
14
+ new URL('../../../../react/pages/clientDetailsHelpers.js', import.meta.url),
15
+ 'utf8',
16
+ );
17
+
18
+ test('company details exposes the fiscal tab for legal entities', () => {
19
+ assert.match(helpers, /\{ key: 'fiscal', label: label\('fiscal', 'Configurações Fiscais'\) \}/);
20
+ assert.match(details, /activeTabKey === 'fiscal'/);
21
+ assert.match(details, /<FiscalTab \{\.\.\.tabProps\} navigation=\{navigation\} \/>/);
22
+ });
23
+
24
+ test('company fiscal tab delegates the complete fiscal settings to ui-accounting', () => {
25
+ assert.match(fiscalTab, /@controleonline\/ui-accounting\/src\/react\/components\/fiscal\/FiscalCompanyConfig/);
26
+ assert.match(fiscalTab, /<FiscalCompanyConfig company=\{client\} navigation=\{navigation\} \/>/);
27
+ assert.doesNotMatch(fiscalTab, /IntegrationConfigPage/);
28
+ assert.doesNotMatch(fiscalTab, /providerKey: 'receita-federal'/);
29
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Unit tests for franchise commission helpers (ui-customers#10).
3
+ */
4
+ import {
5
+ isFranchiseeLink,
6
+ pickFranchiseeLink,
7
+ canEditFranchiseCommission,
8
+ buildFranchiseeLinkParams,
9
+ normalizeCommissionFields,
10
+ validateCommissionFields,
11
+ formatPercent,
12
+ formatMinimum,
13
+ } from '../../../../react/components/tabs/franchiseCommissionHelpers';
14
+
15
+ describe('franchiseCommissionHelpers', () => {
16
+ describe('isFranchiseeLink', () => {
17
+ it('accepts franchisee link matching people and company', () => {
18
+ const link = {
19
+ linkType: 'franchisee',
20
+ people: {id: 11},
21
+ company: {'@id': '/people/3'},
22
+ };
23
+ expect(isFranchiseeLink(link, {peopleId: '11', companyId: '3'})).toBe(true);
24
+ });
25
+
26
+ it('rejects non-franchisee link types', () => {
27
+ expect(
28
+ isFranchiseeLink(
29
+ {linkType: 'client', people: {id: 11}, company: {id: 3}},
30
+ {peopleId: '11', companyId: '3'},
31
+ ),
32
+ ).toBe(false);
33
+ });
34
+
35
+ it('rejects mismatched people id', () => {
36
+ expect(
37
+ isFranchiseeLink(
38
+ {linkType: 'franchisee', people: {id: 99}, company: {id: 3}},
39
+ {peopleId: '11', companyId: '3'},
40
+ ),
41
+ ).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('pickFranchiseeLink', () => {
46
+ it('picks from hydra member payload', () => {
47
+ const payload = {
48
+ 'hydra:member': [
49
+ {id: 1, linkType: 'client', people: {id: 11}, company: {id: 3}},
50
+ {
51
+ id: 2,
52
+ linkType: 'franchisee',
53
+ people: {id: 11},
54
+ company: {id: 3},
55
+ comission: 12.5,
56
+ },
57
+ ],
58
+ };
59
+ const link = pickFranchiseeLink(payload, {peopleId: 11, companyId: 3});
60
+ expect(link?.id).toBe(2);
61
+ expect(link?.comission).toBe(12.5);
62
+ });
63
+
64
+ it('returns null when no franchisee link', () => {
65
+ expect(pickFranchiseeLink([{linkType: 'employee'}], {})).toBe(null);
66
+ });
67
+ });
68
+
69
+ describe('canEditFranchiseCommission', () => {
70
+ it('allows ROLE_SUPER', () => {
71
+ expect(
72
+ canEditFranchiseCommission({
73
+ user: {roles: ['ROLE_SUPER']},
74
+ currentCompany: {user: {owner_enabled: false}},
75
+ }),
76
+ ).toBe(true);
77
+ });
78
+
79
+ it('allows franchisor owner', () => {
80
+ expect(
81
+ canEditFranchiseCommission({
82
+ user: {roles: ['ROLE_MANAGER']},
83
+ currentCompany: {user: {owner_enabled: true}},
84
+ }),
85
+ ).toBe(true);
86
+ });
87
+
88
+ it('denies other profiles', () => {
89
+ expect(
90
+ canEditFranchiseCommission({
91
+ user: {roles: ['ROLE_MANAGER']},
92
+ currentCompany: {user: {owner_enabled: false}},
93
+ }),
94
+ ).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe('buildFranchiseeLinkParams', () => {
99
+ it('includes linkType franchisee and ids', () => {
100
+ expect(buildFranchiseeLinkParams({peopleId: '/people/11', companyId: 3})).toEqual({
101
+ linkType: ['franchisee'],
102
+ people: '11',
103
+ company: '3',
104
+ });
105
+ });
106
+ });
107
+
108
+ describe('normalizeCommissionFields / validate / format', () => {
109
+ it('normalizes numeric fields', () => {
110
+ expect(normalizeCommissionFields({comission: '10', minimum_comission: 2000})).toEqual({
111
+ comission: 10,
112
+ minimum_comission: 2000,
113
+ });
114
+ });
115
+
116
+ it('validates percentage range and minimum >= 0', () => {
117
+ expect(validateCommissionFields({comission: 10, minimum_comission: 0}).ok).toBe(true);
118
+ expect(validateCommissionFields({comission: 101, minimum_comission: 0}).ok).toBe(false);
119
+ expect(validateCommissionFields({comission: 10, minimum_comission: -1}).ok).toBe(false);
120
+ });
121
+
122
+ it('formats display values', () => {
123
+ expect(formatPercent(12.5)).toBe('12.5%');
124
+ expect(formatMinimum(2000)).toBe('2000');
125
+ expect(formatPercent(null)).toBe('—');
126
+ });
127
+ });
128
+ });