@controleonline/ui-customers 1.0.81 → 1.0.83

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 (64) hide show
  1. package/.github/workflows/integration-source-gate.yml +68 -0
  2. package/.github/workflows/publish-npm.yml +91 -0
  3. package/AGENTS.md +12 -6
  4. package/docs/technical/Client-Details-Criar-Usuario.md +113 -0
  5. package/package.json +5 -4
  6. package/src/react/components/tabs/AddressesTab.js +240 -335
  7. package/src/react/components/tabs/DocumentAttachments.js +270 -0
  8. package/src/react/components/tabs/DocumentsTab.js +355 -463
  9. package/src/react/components/tabs/DocumentsTab.styles.js +123 -0
  10. package/src/react/components/tabs/EmployeeCreateFormFields.js +203 -0
  11. package/src/react/components/tabs/EmployeesTab.js +17 -171
  12. package/src/react/components/tabs/EmployeesTab.styles.js +220 -172
  13. package/src/react/components/tabs/GeneralTab.js +84 -46
  14. package/src/react/components/tabs/GeneralTab.styles.js +81 -77
  15. package/src/react/components/tabs/MediaTab.js +50 -5
  16. package/src/react/components/tabs/PeopleCategoriesPanel.js +161 -78
  17. package/src/react/components/tabs/SalesmanTab.js +1 -1
  18. package/src/react/components/tabs/UserFormModal.js +26 -2
  19. package/src/react/components/tabs/UsersTab.js +30 -19
  20. package/src/react/components/tabs/UsersTab.styles.js +257 -257
  21. package/src/react/components/tabs/UsersTabApiKeyModal.js +115 -0
  22. package/src/react/components/tabs/UsersTabUserModal.js +222 -0
  23. package/src/react/components/tabs/addressesTabHelpers.js +190 -0
  24. package/src/react/components/tabs/documentsTabHelpers.js +142 -0
  25. package/src/react/components/tabs/employeeContacts.js +3 -23
  26. package/src/react/components/tabs/employeeUnlink.js +7 -4
  27. package/src/react/components/tabs/employeesTabHelpers.js +33 -9
  28. package/src/react/components/tabs/generalTabHelpers.js +30 -8
  29. package/src/react/components/tabs/useGeneralTabPeopleLink.js +68 -0
  30. package/src/react/components/tabs/usersTabHelpers.js +24 -23
  31. package/src/react/pages/details.page.styles.js +114 -100
  32. package/src/react/pages/employees.js +4 -0
  33. package/src/react/utils/humanCompanyLinkTypes.js +28 -0
  34. package/src/tests/browser/manager/client-details-add-user-modal.spec.js +281 -0
  35. package/src/tests/browser/manager/client-details-address-create-coords.spec.js +414 -0
  36. package/src/tests/browser/manager/client-details-address-edit.spec.js +331 -0
  37. package/src/tests/browser/manager/client-details-contact-edit.spec.js +198 -0
  38. package/src/tests/browser/manager/client-details-create-contact.spec.js +306 -0
  39. package/src/tests/browser/manager/client-details-documents.spec.js +346 -0
  40. package/src/tests/browser/manager/client-details-identity-case.spec.js +227 -0
  41. package/src/tests/browser/manager/client-details-people-type.manifest.md +18 -0
  42. package/src/tests/browser/manager/client-details-people-type.spec.js +345 -0
  43. package/src/tests/browser/manager/client-details-supplier-product-create.spec.js +334 -0
  44. package/src/tests/browser/manager/client-details-users-tab-load.spec.js +194 -0
  45. package/src/tests/browser/manager/employees-salesman-after-sales.spec.js +441 -0
  46. package/src/tests/react/components/SalesmanTab.company.test.js +40 -0
  47. package/src/tests/react/components/tabs/UsersTab.styles.test.js +39 -0
  48. package/src/tests/react/components/tabs/addressesTabHelpers.test.js +78 -0
  49. package/src/tests/react/components/tabs/documentsTabContract.test.mjs +37 -0
  50. package/src/tests/react/components/tabs/documentsTabHelpers.test.mjs +93 -0
  51. package/src/tests/react/components/tabs/employeeContacts.test.js +46 -4
  52. package/src/tests/react/components/tabs/employeeLinkTypes.contract.test.mjs +52 -0
  53. package/src/tests/react/components/tabs/employeeUnlink.test.js +11 -11
  54. package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +2 -23
  55. package/src/tests/react/components/tabs/employeesTabHelpers.test.js +59 -0
  56. package/src/tests/react/components/tabs/generalTabHelpers.test.js +35 -0
  57. package/src/tests/react/components/tabs/peopleMediaTabs.test.js +217 -217
  58. package/src/tests/react/components/tabs/peopleType.contract.test.mjs +47 -0
  59. package/src/tests/react/components/tabs/resolveSeededLinkType.test.mjs +20 -0
  60. package/src/tests/react/components/tabs/salesmanTabMedia.no-redeclare.test.mjs +33 -0
  61. package/src/tests/react/components/tabs/usersTabHelpers.test.js +20 -0
  62. package/src/tests/react/components/tabs/usersTabHelpers.test.mjs +63 -0
  63. package/src/tests/react/pages/employees.test.js +2 -0
  64. package/src/tests/react/utils/humanCompanyLinkTypes.test.js +50 -0
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Smoke: supplier (provider) ProductsTab → "+" create product with initialProvider
3
+ * Covers app-community#98: open supplier details → Produtos → Produto → ProductDetails
4
+ * with supplier context; first save links product_people role=supplier.
5
+ */
6
+ const {expect, test} = require('playwright/test');
7
+ const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
8
+ const {version: appVersion} = require('../../../../../../../package.json');
9
+
10
+ const CORS_HEADERS = {
11
+ 'access-control-allow-origin': '*',
12
+ 'access-control-allow-headers':
13
+ 'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
14
+ 'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
15
+ };
16
+
17
+ const jsonHeaders = () => ({
18
+ ...CORS_HEADERS,
19
+ 'content-type': 'application/ld+json; charset=utf-8',
20
+ });
21
+
22
+ const textHeaders = () => ({
23
+ ...CORS_HEADERS,
24
+ 'content-type': 'text/css; charset=utf-8',
25
+ });
26
+
27
+ const collection = (member = []) => ({
28
+ member,
29
+ 'hydra:member': member,
30
+ totalItems: member.length,
31
+ 'hydra:totalItems': member.length,
32
+ });
33
+
34
+ const company = {
35
+ '@id': '/people/2',
36
+ id: 2,
37
+ name: 'CONTROLE ONLINE',
38
+ alias: 'CONTROLE ONLINE',
39
+ panel_enabled: true,
40
+ enabled: true,
41
+ commercial_enabled: true,
42
+ theme: {
43
+ colors: {
44
+ primary: '#0EA5E9',
45
+ secondary: '#F97316',
46
+ },
47
+ },
48
+ configs: {},
49
+ };
50
+
51
+ const supplier = {
52
+ '@id': '/people/55001',
53
+ id: 55001,
54
+ name: 'FORNECEDOR SMOKE LTDA',
55
+ alias: 'FORNECEDOR SMOKE',
56
+ peopleType: 'J',
57
+ enable: true,
58
+ productPeople: [],
59
+ };
60
+
61
+ const mockSupplierProductCreateApi = async page => {
62
+ const productPosts = [];
63
+ const productPeoplePosts = [];
64
+ let createdProduct = null;
65
+ let linkedRelations = [];
66
+
67
+ await page.route(`${API_ORIGIN}/**`, async route => {
68
+ const request = route.request();
69
+ const url = new URL(request.url());
70
+ const pathname = url.pathname.replace(/^\/+/, '');
71
+ const method = request.method().toUpperCase();
72
+
73
+ if (method === 'OPTIONS') {
74
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
75
+ }
76
+
77
+ if (pathname === 'themes-colors.css') {
78
+ return route.fulfill({
79
+ status: 200,
80
+ headers: textHeaders(),
81
+ body: ':root { --primary: #0ea5e9; --secondary: #f97316; }',
82
+ });
83
+ }
84
+
85
+ if (pathname === 'runtime/ip') {
86
+ return route.fulfill({
87
+ status: 200,
88
+ headers: jsonHeaders(),
89
+ body: JSON.stringify({ip: '127.0.0.1'}),
90
+ });
91
+ }
92
+
93
+ if (pathname === 'people/company/default') {
94
+ return route.fulfill({
95
+ status: 200,
96
+ headers: jsonHeaders(),
97
+ body: JSON.stringify(company),
98
+ });
99
+ }
100
+
101
+ if (pathname === 'people/companies/my') {
102
+ return route.fulfill({
103
+ status: 200,
104
+ headers: jsonHeaders(),
105
+ body: JSON.stringify(collection([company])),
106
+ });
107
+ }
108
+
109
+ if (pathname === 'people/55001' && method === 'GET') {
110
+ return route.fulfill({
111
+ status: 200,
112
+ headers: jsonHeaders(),
113
+ body: JSON.stringify({
114
+ ...supplier,
115
+ productPeople: linkedRelations,
116
+ }),
117
+ });
118
+ }
119
+
120
+ if (pathname === 'products' && method === 'POST') {
121
+ let body = {};
122
+ try {
123
+ body = request.postDataJSON() || {};
124
+ } catch {
125
+ body = {};
126
+ }
127
+ productPosts.push(body);
128
+ createdProduct = {
129
+ '@id': '/products/88001',
130
+ id: 88001,
131
+ product: body.product || body.name || 'PRODUTO SMOKE 98',
132
+ name: body.product || body.name || 'PRODUTO SMOKE 98',
133
+ sku: body.sku || 'SMOKE-98',
134
+ price: body.price || 0,
135
+ type: body.type || 'product',
136
+ };
137
+ return route.fulfill({
138
+ status: 201,
139
+ headers: jsonHeaders(),
140
+ body: JSON.stringify(createdProduct),
141
+ });
142
+ }
143
+
144
+ if (
145
+ (pathname === 'product_people' || pathname === 'product-people') &&
146
+ method === 'POST'
147
+ ) {
148
+ let body = {};
149
+ try {
150
+ body = request.postDataJSON() || {};
151
+ } catch {
152
+ body = {};
153
+ }
154
+ productPeoplePosts.push(body);
155
+ const relation = {
156
+ '@id': '/product_people/1',
157
+ id: 1,
158
+ product: createdProduct || {'@id': body.product, id: 88001},
159
+ people: {'@id': '/people/55001', id: 55001, alias: supplier.alias},
160
+ role: body.role || 'supplier',
161
+ priority: body.priority ?? 1,
162
+ };
163
+ linkedRelations = [relation];
164
+ return route.fulfill({
165
+ status: 201,
166
+ headers: jsonHeaders(),
167
+ body: JSON.stringify(relation),
168
+ });
169
+ }
170
+
171
+ if (pathname === 'products/88001' && method === 'GET') {
172
+ return route.fulfill({
173
+ status: 200,
174
+ headers: jsonHeaders(),
175
+ body: JSON.stringify(createdProduct || {id: 88001, product: 'PRODUTO SMOKE 98'}),
176
+ });
177
+ }
178
+
179
+ if (pathname.startsWith('products/') && method === 'GET') {
180
+ return route.fulfill({
181
+ status: 200,
182
+ headers: jsonHeaders(),
183
+ body: JSON.stringify(createdProduct || {id: 0, product: ''}),
184
+ });
185
+ }
186
+
187
+ if (method === 'GET') {
188
+ return route.fulfill({
189
+ status: 200,
190
+ headers: jsonHeaders(),
191
+ body: JSON.stringify(collection([])),
192
+ });
193
+ }
194
+
195
+ return route.fulfill({
196
+ status: 200,
197
+ headers: jsonHeaders(),
198
+ body: JSON.stringify({}),
199
+ });
200
+ });
201
+
202
+ await page.addInitScript(
203
+ ({version}) => {
204
+ localStorage.setItem(
205
+ 'session',
206
+ JSON.stringify({
207
+ id: 19,
208
+ people: '/people/19',
209
+ api_key: 'test-api-key',
210
+ active: 1,
211
+ mycompany: 2,
212
+ roles: ['ROLE_SUPER'],
213
+ }),
214
+ );
215
+ localStorage.setItem('app-type', 'MANAGER');
216
+ localStorage.setItem('config', JSON.stringify({language: 'pt-br'}));
217
+ localStorage.setItem(
218
+ 'device',
219
+ JSON.stringify({
220
+ id: 'web-manager',
221
+ device: 'web-manager',
222
+ type: 'WEB',
223
+ appName: 'Browser Manager',
224
+ appVersion: version,
225
+ buildNumber: version,
226
+ systemName: 'web',
227
+ systemVersion: 'web',
228
+ deviceType: 'web',
229
+ metadata: {},
230
+ }),
231
+ );
232
+ },
233
+ {version: appVersion},
234
+ );
235
+
236
+ return {productPosts, productPeoplePosts};
237
+ };
238
+
239
+ test.describe('client-details supplier product create (#98)', () => {
240
+ test('opens ProductDetails from supplier ProductsTab with provider context', async ({
241
+ page,
242
+ }) => {
243
+ const {productPosts, productPeoplePosts} = await mockSupplierProductCreateApi(page);
244
+
245
+ await page.goto(
246
+ '/client-details?clientId=55001&contextKey=provider&initialTab=products',
247
+ );
248
+
249
+ await expect(
250
+ page.getByText('FORNECEDOR SMOKE', {exact: false}).first(),
251
+ ).toBeVisible({timeout: 15000});
252
+
253
+ await expect(
254
+ page.getByText(/Produtos fornecidos|produtos vinculados/i).first(),
255
+ ).toBeVisible({timeout: 10000});
256
+
257
+ const addProductSelectors = [
258
+ page.getByRole('button', {name: /produto/i}).first(),
259
+ page.getByText(/^Produto$/i).first(),
260
+ page.locator('text=Produto').first(),
261
+ ];
262
+
263
+ let clicked = false;
264
+ for (const locator of addProductSelectors) {
265
+ try {
266
+ if (await locator.isVisible({timeout: 2500})) {
267
+ await locator.click();
268
+ clicked = true;
269
+ break;
270
+ }
271
+ } catch {
272
+ // try next
273
+ }
274
+ }
275
+
276
+ expect(clicked).toBe(true);
277
+
278
+ // ProductDetails / form should open (name or product field, or save action)
279
+ const productFormHints = [
280
+ page.getByText(/produto|cadastrar|novo produto|dados/i).first(),
281
+ page
282
+ .locator(
283
+ 'input[name="product"], input[name="name"], input[placeholder*="produto" i], input[placeholder*="nome" i]',
284
+ )
285
+ .first(),
286
+ page.getByRole('button', {name: /salvar|save|gravar/i}).first(),
287
+ ];
288
+
289
+ let formVisible = false;
290
+ for (const locator of productFormHints) {
291
+ try {
292
+ if (await locator.isVisible({timeout: 8000})) {
293
+ formVisible = true;
294
+ break;
295
+ }
296
+ } catch {
297
+ // try next
298
+ }
299
+ }
300
+
301
+ expect(formVisible).toBe(true);
302
+
303
+ // Optional: fill minimal fields and save when form is interactive
304
+ const nameField = page
305
+ .locator(
306
+ 'input[name="product"], input[name="name"], input[placeholder*="produto" i], input[placeholder*="nome" i]',
307
+ )
308
+ .first();
309
+
310
+ if (await nameField.isVisible({timeout: 3000}).catch(() => false)) {
311
+ await nameField.fill('PRODUTO SMOKE 98');
312
+ const saveBtn = page
313
+ .getByRole('button', {name: /salvar|save|gravar|confirmar/i})
314
+ .first();
315
+ if (await saveBtn.isVisible().catch(() => false)) {
316
+ await saveBtn.click();
317
+ // Allow async save + optional product_people link
318
+ await page.waitForTimeout(1500);
319
+ }
320
+ }
321
+
322
+ await expect(page.getByText('Request failed', {exact: false})).toHaveCount(0, {
323
+ timeout: 5000,
324
+ });
325
+
326
+ // If product was created, supplier link should have been attempted
327
+ if (productPosts.length > 0) {
328
+ expect(productPeoplePosts.length).toBeGreaterThanOrEqual(1);
329
+ const link = productPeoplePosts[0];
330
+ expect(String(link.role || '')).toMatch(/supplier/i);
331
+ expect(String(link.people || link.provider || '')).toMatch(/55001/);
332
+ }
333
+ });
334
+ });
@@ -0,0 +1,194 @@
1
+ const {expect, test} = require('playwright/test');
2
+ const {API_ORIGIN} = require('../../../../../../../src/tests/browser/apiOrigin');
3
+ const {version: appVersion} = require('../../../../../../../package.json');
4
+
5
+ const CORS_HEADERS = {
6
+ 'access-control-allow-origin': '*',
7
+ 'access-control-allow-headers':
8
+ 'API-TOKEN, APP-DOMAIN, DEVICE, ACCEPT, CONTENT-TYPE, X-Requested-With',
9
+ 'access-control-allow-methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
10
+ };
11
+
12
+ const collection = (member = []) => ({
13
+ member,
14
+ 'hydra:member': member,
15
+ totalItems: member.length,
16
+ 'hydra:totalItems': member.length,
17
+ });
18
+
19
+ const jsonHeaders = () => ({
20
+ ...CORS_HEADERS,
21
+ 'content-type': 'application/ld+json; charset=utf-8',
22
+ });
23
+
24
+ /**
25
+ * Browser smoke for ControleOnline/app-community#759:
26
+ * opening Users tab must fire GET /users?people=/people/{id}.
27
+ */
28
+ const mockClientDetailsUsersTabApi = async page => {
29
+ const company = {
30
+ id: 3,
31
+ name: 'Controle Online',
32
+ alias: 'CONTROLE ONLINE',
33
+ panel_enabled: true,
34
+ enabled: true,
35
+ commercial_enabled: true,
36
+ theme: {colors: {primary: '#0EA5E9', secondary: '#F97316'}},
37
+ configs: {},
38
+ };
39
+ const getUsersRequests = [];
40
+ const listedUsers = [
41
+ {
42
+ '@id': '/users/501',
43
+ id: 501,
44
+ username: 'user.31468',
45
+ people: '/people/31468',
46
+ },
47
+ ];
48
+
49
+ const person = {
50
+ '@id': '/people/31468',
51
+ id: 31468,
52
+ name: 'Pessoa Users Tab',
53
+ alias: 'PESSOA USERS',
54
+ peopleType: 'F',
55
+ enabled: true,
56
+ user: [],
57
+ };
58
+
59
+ await page.route(`${API_ORIGIN}/**`, async route => {
60
+ const request = route.request();
61
+ const url = new URL(request.url());
62
+ const pathname = url.pathname.replace(/^\/+/, '');
63
+ const method = request.method().toUpperCase();
64
+
65
+ if (method === 'OPTIONS') {
66
+ return route.fulfill({status: 204, headers: CORS_HEADERS, body: ''});
67
+ }
68
+
69
+ if (pathname === 'themes-colors.css') {
70
+ return route.fulfill({
71
+ status: 200,
72
+ headers: {...CORS_HEADERS, 'content-type': 'text/css; charset=utf-8'},
73
+ body: ':root { --primary: #0ea5e9; }',
74
+ });
75
+ }
76
+
77
+ if (pathname === 'runtime/ip') {
78
+ return route.fulfill({
79
+ status: 200,
80
+ headers: jsonHeaders(),
81
+ body: JSON.stringify({ip: '127.0.0.1'}),
82
+ });
83
+ }
84
+
85
+ if (pathname === 'people/company/default') {
86
+ return route.fulfill({
87
+ status: 200,
88
+ headers: jsonHeaders(),
89
+ body: JSON.stringify(company),
90
+ });
91
+ }
92
+
93
+ if (pathname === 'people/companies/my') {
94
+ return route.fulfill({
95
+ status: 200,
96
+ headers: jsonHeaders(),
97
+ body: JSON.stringify(collection([company])),
98
+ });
99
+ }
100
+
101
+ if (pathname === 'people/31468') {
102
+ return route.fulfill({
103
+ status: 200,
104
+ headers: jsonHeaders(),
105
+ body: JSON.stringify(person),
106
+ });
107
+ }
108
+
109
+ if (pathname === 'users' && method === 'GET') {
110
+ getUsersRequests.push({
111
+ people: url.searchParams.get('people'),
112
+ itemsPerPage: url.searchParams.get('itemsPerPage'),
113
+ company: url.searchParams.get('company'),
114
+ });
115
+ return route.fulfill({
116
+ status: 200,
117
+ headers: jsonHeaders(),
118
+ body: JSON.stringify(collection(listedUsers)),
119
+ });
120
+ }
121
+
122
+ return route.fulfill({
123
+ status: 200,
124
+ headers: jsonHeaders(),
125
+ body: JSON.stringify(collection([])),
126
+ });
127
+ });
128
+
129
+ await page.addInitScript(
130
+ ({appVersion}) => {
131
+ try {
132
+ localStorage.setItem(
133
+ 'session',
134
+ JSON.stringify({
135
+ id: 7,
136
+ people: '/people/7',
137
+ api_key: 'test-api-key',
138
+ token: 'test-api-key',
139
+ active: 1,
140
+ mycompany: 3,
141
+ roles: ['ROLE_SUPER', 'ROLE_HUMAN'],
142
+ }),
143
+ );
144
+ localStorage.setItem('config', JSON.stringify({language: 'pt-br'}));
145
+ localStorage.setItem('app-type', 'MANAGER');
146
+ localStorage.setItem(
147
+ 'device',
148
+ JSON.stringify({
149
+ id: 'web-manager',
150
+ device: 'web-manager',
151
+ type: 'WEB',
152
+ appName: 'Browser Manager',
153
+ appVersion,
154
+ buildNumber: appVersion,
155
+ systemName: 'web',
156
+ systemVersion: 'web',
157
+ deviceType: 'web',
158
+ metadata: {},
159
+ }),
160
+ );
161
+ } catch {
162
+ // ignore
163
+ }
164
+ },
165
+ {appVersion},
166
+ );
167
+
168
+ return {getUsersRequests};
169
+ };
170
+
171
+ test.describe('client details Users tab load browser smoke (#759)', () => {
172
+ test('opening Users tab requests GET /users?people=/people/{id}', async ({
173
+ page,
174
+ }) => {
175
+ const api = await mockClientDetailsUsersTabApi(page);
176
+
177
+ await page.goto(
178
+ '/client-details?clientId=31468&contextKey=employee&initialTab=users',
179
+ );
180
+
181
+ await expect(page.getByText(/Usuários|Users|Dados Cadastrais/i).first()).toBeVisible({
182
+ timeout: 15000,
183
+ });
184
+
185
+ await expect.poll(() => api.getUsersRequests.length, {timeout: 15000}).toBeGreaterThan(0);
186
+
187
+ const last = api.getUsersRequests[api.getUsersRequests.length - 1];
188
+ expect(String(last.people || '')).toMatch(/\/people\/31468/);
189
+ expect(String(last.itemsPerPage || '')).toBe('100');
190
+ expect(last.company).toBeNull();
191
+
192
+ await expect(page.getByText('user.31468')).toBeVisible({timeout: 15000});
193
+ });
194
+ });