@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.
- package/.github/agents/developer.agent.md +30 -0
- package/.github/agents/devops.agent.md +30 -0
- package/.github/agents/qa.agent.md +30 -0
- package/.github/agents/security.agent.md +30 -0
- package/.scrutinizer.yml +20 -0
- package/AGENTS.md +50 -0
- package/FUNDING.yml +1 -0
- package/README.md +23 -0
- package/docs/technical/Client-Details-Franquia-Filial.md +18 -0
- package/docs/technical/Client-Details-People-Name-Case.md +103 -0
- package/docs/technical/Cliente-Vendedor-Vinculo-e-Permissoes.md +152 -0
- package/docs/wiki.md +1 -0
- package/package.json +32 -0
- package/scripts/validate-wiki-pointer.cjs +13 -0
- package/src/react/components/tabs/AddressesTab.js +494 -0
- package/src/react/components/tabs/CategoriesTab.js +196 -0
- package/src/react/components/tabs/ClassificationChips.js +494 -0
- package/src/react/components/tabs/ContactTab.js +621 -0
- package/src/react/components/tabs/ContactTab.styles.js +119 -0
- package/src/react/components/tabs/ContractsTab.js +13 -0
- package/src/react/components/tabs/DocumentsTab.js +463 -0
- package/src/react/components/tabs/DocumentsTab.styles.js +133 -0
- package/src/react/components/tabs/EmployeeContactRow.js +83 -0
- package/src/react/components/tabs/EmployeesTab.js +454 -0
- package/src/react/components/tabs/EmployeesTab.styles.js +172 -0
- package/src/react/components/tabs/FiscalTab.js +6 -0
- package/src/react/components/tabs/FranchiseCommissionSection.js +307 -0
- package/src/react/components/tabs/FranchiseLinksManageModal.js +215 -0
- package/src/react/components/tabs/FranchiseLinksTab.js +282 -0
- package/src/react/components/tabs/GeneralTab.js +466 -0
- package/src/react/components/tabs/GeneralTab.styles.js +101 -0
- package/src/react/components/tabs/MediaTab.js +356 -0
- package/src/react/components/tabs/PeopleCategoriesPanel.js +601 -0
- package/src/react/components/tabs/ProductsTab.js +236 -0
- package/src/react/components/tabs/ProductsTab.styles.js +143 -0
- package/src/react/components/tabs/SalesmanCommissionBlock.js +120 -0
- package/src/react/components/tabs/SalesmanManageModal.js +208 -0
- package/src/react/components/tabs/SalesmanTab.js +436 -0
- package/src/react/components/tabs/SalesmanTab.styles.js +4 -0
- package/src/react/components/tabs/UserApiKeyModal.js +90 -0
- package/src/react/components/tabs/UserFormModal.js +301 -0
- package/src/react/components/tabs/UsersTab.js +426 -0
- package/src/react/components/tabs/UsersTab.styles.js +283 -0
- package/src/react/components/tabs/UsersTab.styles.test.js +42 -0
- package/src/react/components/tabs/contactMutationMessage.js +18 -0
- package/src/react/components/tabs/employeeContacts.js +214 -0
- package/src/react/components/tabs/employeeUnlink.js +68 -0
- package/src/react/components/tabs/employeesTabHelpers.js +227 -0
- package/src/react/components/tabs/franchiseCommissionHelpers.js +134 -0
- package/src/react/components/tabs/franchiseLinksTab.helpers.js +247 -0
- package/src/react/components/tabs/generalTabHelpers.js +120 -0
- package/src/react/components/tabs/salesmanTab.helpers.js +114 -0
- package/src/react/components/tabs/salesmanTabHelpers.js +173 -0
- package/src/react/components/tabs/salesmanTabMedia.js +55 -0
- package/src/react/components/tabs/salesmanTabSession.js +39 -0
- package/src/react/components/tabs/useEmployeeUnlink.js +58 -0
- package/src/react/components/tabs/useFranchiseLinksManage.js +215 -0
- package/src/react/components/tabs/useSalesmanManage.js +222 -0
- package/src/react/components/tabs/usersTabHelpers.js +221 -0
- package/src/react/domain/productSupplierFlow.js +37 -0
- package/src/react/index.js +4 -0
- package/src/react/pages/ClientDetailsSkeleton.js +84 -0
- package/src/react/pages/clientDetailsHelpers.js +17 -0
- package/src/react/pages/clients.js +27 -0
- package/src/react/pages/details.js +529 -0
- package/src/react/pages/details.page.styles.js +105 -0
- package/src/react/pages/details.styles.js +15 -0
- package/src/react/pages/employees.js +69 -0
- package/src/react/pages/franchisees.js +30 -0
- package/src/react/pages/prospects.js +21 -0
- package/src/react/pages/provider.js +21 -0
- package/src/react/router/routes.js +104 -0
- package/src/react/styles/details.js +571 -0
- package/src/store/people_categories/index.js +72 -0
- package/src/tests/browser/manager/client-details-create-user.spec.js +321 -0
- package/src/tests/browser/manager/client-details-employee-unlink.spec.js +215 -0
- package/src/tests/browser/manager/client-details-employees-refresh.spec.js +271 -0
- package/src/tests/browser/manager/client-details-enable.spec.js +241 -0
- package/src/tests/browser/manager/client-details-fiscal-tabs.spec.js +410 -0
- package/src/tests/browser/manager/client-details-franchise-commission.spec.js +318 -0
- package/src/tests/browser/manager/client-details-franchise-links.spec.js +365 -0
- package/src/tests/browser/manager/client-details-media.spec.js +405 -0
- package/src/tests/browser/manager/client-details-relationships.spec.js +313 -0
- package/src/tests/browser/manager/client-details-salesman-commission.spec.js +291 -0
- package/src/tests/browser/manager/client-details-salesman-manage.spec.js +448 -0
- package/src/tests/react/components/tabs/contactMutationMessage.test.js +25 -0
- package/src/tests/react/components/tabs/employeeContacts.test.js +266 -0
- package/src/tests/react/components/tabs/employeeUnlink.test.js +67 -0
- package/src/tests/react/components/tabs/employeesTabHelpers.media.test.js +78 -0
- package/src/tests/react/components/tabs/employeesTabHelpers.test.js +64 -0
- package/src/tests/react/components/tabs/fiscalTabContract.test.mjs +29 -0
- package/src/tests/react/components/tabs/franchiseCommissionHelpers.test.js +128 -0
- package/src/tests/react/components/tabs/franchiseLinksTab.helpers.test.mjs +166 -0
- package/src/tests/react/components/tabs/generalTabHelpers.test.js +130 -0
- package/src/tests/react/components/tabs/peopleMediaTabs.test.js +322 -0
- package/src/tests/react/components/tabs/salesmanTab.helpers.test.mjs +75 -0
- package/src/tests/react/components/tabs/salesmanTabHelpers.test.js +142 -0
- package/src/tests/react/components/tabs/salesmanTabHelpers.test.mjs +92 -0
- package/src/tests/react/components/tabs/usersTabHelpers.test.js +59 -0
- package/src/tests/react/domain/productSupplierFlow.test.mjs +55 -0
- package/src/tests/react/pages/clientDetailsHelpers.test.js +98 -0
- package/src/tests/react/pages/employees.test.js +44 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: Salvar button must be a visible primary action (not text-only styles).
|
|
3
|
+
* Root cause of app-community#458 / #635: after task-428 modularization,
|
|
4
|
+
* inlineStyle_348_20 kept Cancelar text styles and was applied to the Salvar
|
|
5
|
+
* TouchableOpacity → white label on white modal made Salvar invisible.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
inlineStyle_339_16,
|
|
9
|
+
inlineStyle_341_14,
|
|
10
|
+
inlineStyle_348_20,
|
|
11
|
+
timezoneListStyle,
|
|
12
|
+
} from './UsersTab.styles';
|
|
13
|
+
|
|
14
|
+
describe('UsersTab.styles — UserFormModal footer / Salvar visibility (#635)', () => {
|
|
15
|
+
it('footer row has padding and top border so actions stay visible', () => {
|
|
16
|
+
expect(inlineStyle_339_16.paddingHorizontal).toBe(24);
|
|
17
|
+
expect(inlineStyle_339_16.paddingBottom).toBe(24);
|
|
18
|
+
expect(inlineStyle_339_16.borderTopWidth).toBe(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('Cancelar keeps outlined secondary style', () => {
|
|
22
|
+
expect(inlineStyle_341_14.flex).toBe(1);
|
|
23
|
+
expect(inlineStyle_341_14.borderWidth).toBe(1);
|
|
24
|
+
expect(inlineStyle_341_14.borderColor).toBe('#64748B');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('Salvar is primary button with solid background (not text-only styles)', () => {
|
|
28
|
+
expect(inlineStyle_348_20.flex).toBe(1);
|
|
29
|
+
expect(inlineStyle_348_20.backgroundColor).toBe('#0F172A');
|
|
30
|
+
expect(inlineStyle_348_20.paddingVertical).toBe(14);
|
|
31
|
+
expect(inlineStyle_348_20.borderRadius).toBe(12);
|
|
32
|
+
expect(inlineStyle_348_20.alignItems).toBe('center');
|
|
33
|
+
// must NOT be the old text-only shape
|
|
34
|
+
expect(inlineStyle_348_20.fontSize).toBeUndefined();
|
|
35
|
+
expect(inlineStyle_348_20.color).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('timezone list is height-capped so long lists do not push footer off-screen', () => {
|
|
39
|
+
expect(timezoneListStyle.maxHeight).toBe(220);
|
|
40
|
+
expect(timezoneListStyle.overflow).toBe('hidden');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {resolveSystemErrorMessage} from '@controleonline/ui-common/src/react/utils/systemErrorMessage'
|
|
2
|
+
|
|
3
|
+
const PEOPLE_ITEM_NOT_FOUND_PATTERN = /Item not found for "\/people\/\d+"\./i
|
|
4
|
+
|
|
5
|
+
export const resolveContactMutationMessage = (
|
|
6
|
+
error,
|
|
7
|
+
fallbackMessage = 'Nao foi possivel salvar este contato agora.',
|
|
8
|
+
) => {
|
|
9
|
+
const message = resolveSystemErrorMessage(error)
|
|
10
|
+
|
|
11
|
+
if (PEOPLE_ITEM_NOT_FOUND_PATTERN.test(message)) {
|
|
12
|
+
return 'Este contato esta vinculado a empresa, mas o cadastro direto dele nao esta disponivel para edicao no momento.'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return message || fallbackMessage
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default resolveContactMutationMessage
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const EMPLOYEE_CONTACT_LINK_TYPES = [
|
|
2
|
+
'employee',
|
|
3
|
+
'owner',
|
|
4
|
+
'director',
|
|
5
|
+
'manager',
|
|
6
|
+
'salesman',
|
|
7
|
+
'after-sales',
|
|
8
|
+
'courier',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const extractId = value => {
|
|
12
|
+
if (value == null || value === '') {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === 'object') {
|
|
16
|
+
return extractId(value.id ?? value['@id'] ?? '');
|
|
17
|
+
}
|
|
18
|
+
return String(value).replace(/\D/g, '');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const normalizeEmployeeLinkTypeStrict = value => {
|
|
22
|
+
const normalized = String(value || '')
|
|
23
|
+
.trim()
|
|
24
|
+
.toLowerCase();
|
|
25
|
+
|
|
26
|
+
return EMPLOYEE_CONTACT_LINK_TYPES.includes(normalized) ? normalized : '';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const normalizeEmployeeLinkType = value => {
|
|
30
|
+
return normalizeEmployeeLinkTypeStrict(value) || 'employee';
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const extractPeopleLinkItems = payload => {
|
|
34
|
+
if (Array.isArray(payload)) {
|
|
35
|
+
return payload;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(payload?.member)) {
|
|
39
|
+
return payload.member;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(payload?.['hydra:member'])) {
|
|
43
|
+
return payload['hydra:member'];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return [];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build GET /people_links query params.
|
|
51
|
+
* - company/people as numeric ids
|
|
52
|
+
* - linkType as array (API Platform SearchFilter)
|
|
53
|
+
* - itemsPerPage to avoid truncating contacts on large companies (#636)
|
|
54
|
+
*/
|
|
55
|
+
export const buildPeopleLinkReadParams = ({
|
|
56
|
+
companyId = '',
|
|
57
|
+
peopleId = '',
|
|
58
|
+
linkType = '',
|
|
59
|
+
linkTypes = [],
|
|
60
|
+
itemsPerPage,
|
|
61
|
+
} = {}) => {
|
|
62
|
+
const params = {};
|
|
63
|
+
const normalizedCompanyId = extractId(companyId);
|
|
64
|
+
const normalizedPeopleId = extractId(peopleId);
|
|
65
|
+
const normalizedLinkType = String(linkType || '').trim();
|
|
66
|
+
const normalizedLinkTypes = Array.isArray(linkTypes)
|
|
67
|
+
? linkTypes
|
|
68
|
+
.map(value => String(value || '').trim())
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
: [];
|
|
71
|
+
|
|
72
|
+
if (normalizedCompanyId) {
|
|
73
|
+
params.company = normalizedCompanyId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (normalizedPeopleId) {
|
|
77
|
+
params.people = normalizedPeopleId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (normalizedLinkType) {
|
|
81
|
+
// Scalar becomes a one-element array — API Platform rejects bare scalars here.
|
|
82
|
+
params.linkType = [normalizedLinkType];
|
|
83
|
+
} else if (normalizedLinkTypes.length > 0) {
|
|
84
|
+
params.linkType = normalizedLinkTypes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const pageSize = Number(itemsPerPage);
|
|
88
|
+
if (Number.isFinite(pageSize) && pageSize > 0) {
|
|
89
|
+
params.itemsPerPage = pageSize;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return params;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const filterPeopleLinksByScope = (
|
|
96
|
+
payload,
|
|
97
|
+
{companyId = '', peopleId = '', linkTypes = []} = {},
|
|
98
|
+
) => {
|
|
99
|
+
const normalizedCompanyId = extractId(companyId);
|
|
100
|
+
const normalizedPeopleId = extractId(peopleId);
|
|
101
|
+
const normalizedLinkTypes = Array.isArray(linkTypes)
|
|
102
|
+
? linkTypes
|
|
103
|
+
.map(value => String(value || '').trim().toLowerCase())
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
: [];
|
|
106
|
+
|
|
107
|
+
return extractPeopleLinkItems(payload).filter(link => {
|
|
108
|
+
// Accept embedded object or plain IRI string (/people/123).
|
|
109
|
+
const linkCompanyId = extractId(
|
|
110
|
+
link?.company?.id || link?.company?.['@id'] || link?.company,
|
|
111
|
+
);
|
|
112
|
+
const linkPeopleId = extractId(
|
|
113
|
+
link?.people?.id || link?.people?.['@id'] || link?.people,
|
|
114
|
+
);
|
|
115
|
+
const linkType = String(link?.linkType || '')
|
|
116
|
+
.trim()
|
|
117
|
+
.toLowerCase();
|
|
118
|
+
|
|
119
|
+
if (normalizedCompanyId && linkCompanyId !== normalizedCompanyId) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (normalizedPeopleId && linkPeopleId !== normalizedPeopleId) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
normalizedLinkTypes.length > 0 &&
|
|
129
|
+
!normalizedLinkTypes.includes(linkType)
|
|
130
|
+
) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const buildSalesmanLinksFromPeopleLinks = (
|
|
139
|
+
payload,
|
|
140
|
+
{clientId = '', linkType = 'sellers-client'} = {},
|
|
141
|
+
) => {
|
|
142
|
+
return filterPeopleLinksByScope(payload, {
|
|
143
|
+
peopleId: clientId,
|
|
144
|
+
linkTypes: [linkType],
|
|
145
|
+
}).filter(link =>
|
|
146
|
+
extractId(link?.company?.id || link?.company?.['@id'] || link?.company),
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const buildEmployeeContactsFromPeopleLinks = (
|
|
151
|
+
payload,
|
|
152
|
+
{parentPeopleId = '', allowedLinkTypes = EMPLOYEE_CONTACT_LINK_TYPES} = {},
|
|
153
|
+
) => {
|
|
154
|
+
const companyId = extractId(parentPeopleId);
|
|
155
|
+
const normalizedAllowedLinkTypes = Array.isArray(allowedLinkTypes)
|
|
156
|
+
? allowedLinkTypes.map(normalizeEmployeeLinkTypeStrict).filter(Boolean)
|
|
157
|
+
: EMPLOYEE_CONTACT_LINK_TYPES;
|
|
158
|
+
|
|
159
|
+
return filterPeopleLinksByScope(payload, {
|
|
160
|
+
companyId,
|
|
161
|
+
linkTypes: normalizedAllowedLinkTypes,
|
|
162
|
+
})
|
|
163
|
+
.map(link => {
|
|
164
|
+
const person = link?.people;
|
|
165
|
+
const personId = extractId(
|
|
166
|
+
typeof person === 'string' || typeof person === 'number'
|
|
167
|
+
? person
|
|
168
|
+
: person?.id || person?.['@id'],
|
|
169
|
+
);
|
|
170
|
+
const normalizedLinkType = normalizeEmployeeLinkTypeStrict(link?.linkType);
|
|
171
|
+
|
|
172
|
+
if (!personId || (companyId && personId === companyId)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
person &&
|
|
178
|
+
typeof person === 'object' &&
|
|
179
|
+
String(person?.peopleType || '').toUpperCase() === 'J'
|
|
180
|
+
) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
person &&
|
|
186
|
+
typeof person === 'object' &&
|
|
187
|
+
(person?.deleted === true || person?.deleted === 1 || person?.deleted === '1')
|
|
188
|
+
) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const enableRaw = link?.enable ?? link?.enabled;
|
|
193
|
+
if (enableRaw === 0 || enableRaw === false || enableRaw === '0') {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const base =
|
|
198
|
+
person && typeof person === 'object'
|
|
199
|
+
? {...person}
|
|
200
|
+
: {id: personId, '@id': `/people/${personId}`};
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
...base,
|
|
204
|
+
// Prefer numeric id when the payload provided one; otherwise digits-only string.
|
|
205
|
+
id: base.id != null && base.id !== '' ? base.id : personId,
|
|
206
|
+
linkType: normalizedLinkType,
|
|
207
|
+
peopleLink: link,
|
|
208
|
+
peopleLinkId: extractId(link?.id || link?.['@id']),
|
|
209
|
+
};
|
|
210
|
+
})
|
|
211
|
+
.filter(Boolean);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export default buildEmployeeContactsFromPeopleLinks;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {Alert} from 'react-native';
|
|
2
|
+
import {extractId} from './employeesTabHelpers';
|
|
3
|
+
|
|
4
|
+
export const EMPLOYEE_UNLINK_TITLE = 'Remover colaborador';
|
|
5
|
+
export const EMPLOYEE_UNLINK_MESSAGE =
|
|
6
|
+
'O colaborador será marcado como removido e o vínculo com a empresa será desativado. O registro permanece no banco (exclusão lógica). Deseja continuar?';
|
|
7
|
+
|
|
8
|
+
export const extractEmployeeUnlinkIds = employee => ({
|
|
9
|
+
peopleId: extractId(employee?.id || employee?.['@id']),
|
|
10
|
+
linkId:
|
|
11
|
+
extractId(employee?.peopleLinkId) ||
|
|
12
|
+
extractId(employee?.peopleLink?.id || employee?.peopleLink?.['@id']),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const confirmEmployeeRemoval = ({showDialog, onConfirm, onCancel}) => {
|
|
16
|
+
if (typeof showDialog === 'function') {
|
|
17
|
+
showDialog({
|
|
18
|
+
title: EMPLOYEE_UNLINK_TITLE,
|
|
19
|
+
message: EMPLOYEE_UNLINK_MESSAGE,
|
|
20
|
+
confirmLabel: 'Remover',
|
|
21
|
+
cancelLabel: 'Cancelar',
|
|
22
|
+
onConfirm,
|
|
23
|
+
onCancel,
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Alert.alert(EMPLOYEE_UNLINK_TITLE, EMPLOYEE_UNLINK_MESSAGE, [
|
|
29
|
+
{text: 'Cancelar', style: 'cancel', onPress: onCancel},
|
|
30
|
+
{text: 'Remover', style: 'destructive', onPress: onConfirm},
|
|
31
|
+
]);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const removeEmployeeFromCompany = async ({
|
|
35
|
+
peopleId,
|
|
36
|
+
linkId,
|
|
37
|
+
removePeople,
|
|
38
|
+
removePeopleLink,
|
|
39
|
+
}) => {
|
|
40
|
+
if (peopleId && typeof removePeople === 'function') {
|
|
41
|
+
await removePeople(peopleId);
|
|
42
|
+
return {mode: 'people', peopleId, linkId};
|
|
43
|
+
}
|
|
44
|
+
if (linkId && typeof removePeopleLink === 'function') {
|
|
45
|
+
await removePeopleLink(linkId);
|
|
46
|
+
return {mode: 'link', peopleId, linkId};
|
|
47
|
+
}
|
|
48
|
+
const error = new Error(
|
|
49
|
+
'Não foi possível remover o colaborador (ação indisponível).',
|
|
50
|
+
);
|
|
51
|
+
error.code = 'UNAVAILABLE';
|
|
52
|
+
throw error;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const filterEmployeesAfterUnlink = (list, {peopleId, linkId}) =>
|
|
56
|
+
(list || []).filter(item => {
|
|
57
|
+
const itemPeopleId = extractId(item?.id || item?.['@id']);
|
|
58
|
+
const itemLinkId = extractId(
|
|
59
|
+
item?.peopleLink?.id || item?.peopleLink?.['@id'] || item?.peopleLinkId,
|
|
60
|
+
);
|
|
61
|
+
if (peopleId && itemPeopleId === peopleId) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (linkId && itemLinkId === linkId) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { resolveFileImageUrl } from '@controleonline/ui-common/src/react/utils/fileUrl';
|
|
2
|
+
import {
|
|
3
|
+
} from '@controleonline/ui-common/src/react/utils/entityDisplay';
|
|
4
|
+
import { normalizeEmployeeLinkType } from './employeeContacts';
|
|
5
|
+
|
|
6
|
+
export const extractId = value => String(value || '').replace(/\D/g, '');
|
|
7
|
+
export const normalizeIdentityValue = value => String(value);
|
|
8
|
+
|
|
9
|
+
export const normalizeCollection = payload => {
|
|
10
|
+
if (Array.isArray(payload)) return payload;
|
|
11
|
+
if (!payload || typeof payload !== 'object') return [];
|
|
12
|
+
if (Array.isArray(payload.member)) return payload.member;
|
|
13
|
+
if (Array.isArray(payload['hydra:member'])) return payload['hydra:member'];
|
|
14
|
+
if (Array.isArray(payload.items)) return payload.items;
|
|
15
|
+
return [];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const extractPeopleMediaUrl = (person, mediaType = 'avatar') => {
|
|
19
|
+
if (!person || typeof person !== 'object') {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const collection = normalizeCollection(
|
|
24
|
+
person.peopleMedia || person.people_media || person.media,
|
|
25
|
+
);
|
|
26
|
+
const wanted = String(mediaType || '').trim().toLowerCase();
|
|
27
|
+
const match =
|
|
28
|
+
collection.find(item => {
|
|
29
|
+
const type = String(
|
|
30
|
+
item?.mediaType?.type || item?.mediaType || item?.type || '',
|
|
31
|
+
)
|
|
32
|
+
.trim()
|
|
33
|
+
.toLowerCase();
|
|
34
|
+
return wanted ? type === wanted : Boolean(type);
|
|
35
|
+
}) || collection[0];
|
|
36
|
+
|
|
37
|
+
return resolveFileImageUrl(match?.file) || '';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const fetchPeopleMediaUrls = async ({ peopleActions, mediaType, peopleIds }) => {
|
|
41
|
+
const uniqueIds = [...new Set((peopleIds || []).map(extractId).filter(Boolean))];
|
|
42
|
+
if (uniqueIds.length === 0) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const buildMapFromCollection = response => {
|
|
47
|
+
const byPeopleId = {};
|
|
48
|
+
for (const media of normalizeCollection(response)) {
|
|
49
|
+
const peopleId = extractId(
|
|
50
|
+
media?.people?.['@id'] || media?.people?.id || media?.people,
|
|
51
|
+
);
|
|
52
|
+
if (!peopleId || byPeopleId[peopleId]) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const imageUrl = resolveFileImageUrl(media?.file);
|
|
56
|
+
if (imageUrl) {
|
|
57
|
+
byPeopleId[peopleId] = imageUrl;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return byPeopleId;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Batch: uma chamada people_media com people[] (SearchFilter exact multi-valor).
|
|
64
|
+
try {
|
|
65
|
+
const response = await peopleActions.getPeopleMedia({
|
|
66
|
+
people: uniqueIds.map(id => `/people/${id}`),
|
|
67
|
+
'mediaType.type': mediaType,
|
|
68
|
+
itemsPerPage: Math.max(uniqueIds.length, 1),
|
|
69
|
+
});
|
|
70
|
+
return buildMapFromCollection(response);
|
|
71
|
+
} catch {
|
|
72
|
+
// Fallback: N chamadas só se o filtro batch não for aceito pelo backend.
|
|
73
|
+
const entries = await Promise.all(
|
|
74
|
+
uniqueIds.map(async peopleId => {
|
|
75
|
+
try {
|
|
76
|
+
const response = await peopleActions.getPeopleMedia({
|
|
77
|
+
people: `/people/${peopleId}`,
|
|
78
|
+
'mediaType.type': mediaType,
|
|
79
|
+
itemsPerPage: 1,
|
|
80
|
+
});
|
|
81
|
+
const media = normalizeCollection(response)[0];
|
|
82
|
+
const imageUrl = resolveFileImageUrl(media?.file);
|
|
83
|
+
return imageUrl ? [peopleId, imageUrl] : null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return entries
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.reduce((accumulator, [peopleId, imageUrl]) => {
|
|
93
|
+
accumulator[peopleId] = imageUrl;
|
|
94
|
+
return accumulator;
|
|
95
|
+
}, {});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const formatDateInput = text => {
|
|
100
|
+
const numbers = String(text || '').replace(/\D/g, '').slice(0, 8);
|
|
101
|
+
if (!numbers) {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (numbers.length <= 2) {
|
|
106
|
+
return numbers;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (numbers.length <= 4) {
|
|
110
|
+
return `${numbers.slice(0, 2)}/${numbers.slice(2)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return `${numbers.slice(0, 2)}/${numbers.slice(2, 4)}/${numbers.slice(4)}`;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const parseBrDateToYmd = value => {
|
|
117
|
+
const formatted = formatDateInput(value);
|
|
118
|
+
if (formatted.length !== 10) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const [day, month, year] = formatted.split('/').map(v => parseInt(v, 10));
|
|
123
|
+
if (!day || !month || !year) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const candidate = new Date(year, month - 1, day);
|
|
128
|
+
const isValid =
|
|
129
|
+
candidate.getFullYear() === year &&
|
|
130
|
+
candidate.getMonth() === month - 1 &&
|
|
131
|
+
candidate.getDate() === day;
|
|
132
|
+
|
|
133
|
+
if (!isValid) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return `${year.toString().padStart(4, '0')}-${month
|
|
138
|
+
.toString()
|
|
139
|
+
.padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const LINK_TYPE_OPTIONS = [
|
|
143
|
+
{ value: 'employee', translationKey: 'employee' },
|
|
144
|
+
{ value: 'owner', translationKey: 'owner' },
|
|
145
|
+
{ value: 'director', translationKey: 'director' },
|
|
146
|
+
{ value: 'manager', translationKey: 'manager' },
|
|
147
|
+
{ value: 'courier', translationKey: 'courier' },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
export const resolveEmployeeLinkType = employee =>
|
|
151
|
+
normalizeEmployeeLinkType(
|
|
152
|
+
employee?.linkType ||
|
|
153
|
+
employee?.link?.linkType ||
|
|
154
|
+
employee?.peopleLink?.linkType ||
|
|
155
|
+
(Array.isArray(employee?.link) ? employee.link[0]?.linkType : ''),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Builds ClientDetails navigation params when opening an employee from
|
|
161
|
+
* EmployeesTab (juridical client). Always opens on the "general" tab so the
|
|
162
|
+
* PF detail does not land on contracts/contacts by mistake.
|
|
163
|
+
* Refs: ControleOnline/app-community#9
|
|
164
|
+
*/
|
|
165
|
+
export const buildEmployeeDetailNavParams = ({
|
|
166
|
+
employee,
|
|
167
|
+
parentPeopleId,
|
|
168
|
+
}) => {
|
|
169
|
+
const clientId = extractId(employee?.id || employee?.['@id']);
|
|
170
|
+
if (!clientId) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
clientId,
|
|
176
|
+
contextKey: 'contacts',
|
|
177
|
+
initialTab: 'general',
|
|
178
|
+
parentCompanyId: parentPeopleId,
|
|
179
|
+
linkType: resolveEmployeeLinkType(employee),
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const resolveEmployeeContactLinkType = employee =>
|
|
184
|
+
String(
|
|
185
|
+
employee?.peopleLink?.linkType ||
|
|
186
|
+
employee?.linkType ||
|
|
187
|
+
employee?.link?.linkType ||
|
|
188
|
+
(Array.isArray(employee?.link) ? employee.link[0]?.linkType : '') ||
|
|
189
|
+
'',
|
|
190
|
+
).trim();
|
|
191
|
+
|
|
192
|
+
export const formatEmployeeContactTitle = employee => {
|
|
193
|
+
const name = String(employee?.name || '').replace(/\s+/g, ' ').trim() || '-';
|
|
194
|
+
const alias = String(employee?.alias || '').replace(/\s+/g, ' ').trim();
|
|
195
|
+
|
|
196
|
+
return alias ? `${name} / ${alias}` : name;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const formatEmployeeContactMeta = employee => {
|
|
200
|
+
const id = extractId(employee?.id || employee?.['@id']) || '-';
|
|
201
|
+
const linkType = resolveEmployeeContactLinkType(employee);
|
|
202
|
+
|
|
203
|
+
return linkType ? `ID: ${id} / ${linkType}` : `ID: ${id}`;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const buildEmployeeCreatePayload = ({
|
|
207
|
+
name,
|
|
208
|
+
alias,
|
|
209
|
+
foundationDate,
|
|
210
|
+
linkType,
|
|
211
|
+
parentPeopleId,
|
|
212
|
+
}) => {
|
|
213
|
+
const payload = {
|
|
214
|
+
name,
|
|
215
|
+
alias,
|
|
216
|
+
peopleType: 'F',
|
|
217
|
+
company: `/people/${parentPeopleId}`,
|
|
218
|
+
linkType: normalizeEmployeeLinkType(linkType),
|
|
219
|
+
'extra-data': {},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (foundationDate) {
|
|
223
|
+
payload.foundationDate = foundationDate;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return payload;
|
|
227
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for franchise commission (people_link linkType=franchisee).
|
|
3
|
+
* Extracted to keep UI components under the absolute 500-line limit.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const extractId = value => String(value || '').replace(/\D/g, '');
|
|
7
|
+
|
|
8
|
+
const normalizeNumber = value => {
|
|
9
|
+
if (value === null || value === undefined || value === '') {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const n = Number(value);
|
|
13
|
+
return Number.isFinite(n) ? n : null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detect whether a people_link represents a franchisee of the franchisor.
|
|
18
|
+
*/
|
|
19
|
+
export const isFranchiseeLink = (link, {peopleId = '', companyId = ''} = {}) => {
|
|
20
|
+
if (!link) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const linkType = String(link?.linkType || '').trim().toLowerCase();
|
|
24
|
+
if (linkType !== 'franchisee') {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const normalizedPeopleId = extractId(peopleId);
|
|
28
|
+
const normalizedCompanyId = extractId(companyId);
|
|
29
|
+
if (normalizedPeopleId) {
|
|
30
|
+
const linkPeopleId = extractId(link?.people?.id || link?.people?.['@id'] || link?.people);
|
|
31
|
+
if (linkPeopleId && linkPeopleId !== normalizedPeopleId) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (normalizedCompanyId) {
|
|
36
|
+
const linkCompanyId = extractId(link?.company?.id || link?.company?.['@id'] || link?.company);
|
|
37
|
+
if (linkCompanyId && linkCompanyId !== normalizedCompanyId) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pick the first franchisee link from a collection payload / array.
|
|
46
|
+
*/
|
|
47
|
+
export const pickFranchiseeLink = (payload, scope = {}) => {
|
|
48
|
+
const items = Array.isArray(payload)
|
|
49
|
+
? payload
|
|
50
|
+
: Array.isArray(payload?.member)
|
|
51
|
+
? payload.member
|
|
52
|
+
: Array.isArray(payload?.['hydra:member'])
|
|
53
|
+
? payload['hydra:member']
|
|
54
|
+
: [];
|
|
55
|
+
|
|
56
|
+
return items.find(item => isFranchiseeLink(item, scope)) || null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* superadmin (ROLE_SUPER) or owner of the franchisor (owner_enabled).
|
|
61
|
+
*/
|
|
62
|
+
export const canEditFranchiseCommission = ({user = null, currentCompany = null} = {}) => {
|
|
63
|
+
const roles = Array.isArray(user?.roles) ? user.roles : [];
|
|
64
|
+
if (roles.includes('ROLE_SUPER')) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return Boolean(currentCompany?.user?.owner_enabled);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build read params for franchisee people_link lookup.
|
|
72
|
+
*/
|
|
73
|
+
export const buildFranchiseeLinkParams = ({peopleId = '', companyId = ''} = {}) => {
|
|
74
|
+
// API Platform exposes linkType as an array filter on people_links.
|
|
75
|
+
const params = {linkType: ['franchisee']};
|
|
76
|
+
const pid = extractId(peopleId);
|
|
77
|
+
const cid = extractId(companyId);
|
|
78
|
+
if (pid) {
|
|
79
|
+
params.people = pid;
|
|
80
|
+
}
|
|
81
|
+
if (cid) {
|
|
82
|
+
params.company = cid;
|
|
83
|
+
}
|
|
84
|
+
return params;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Normalize commission form values for display / save.
|
|
89
|
+
*/
|
|
90
|
+
export const normalizeCommissionFields = (link = {}) => ({
|
|
91
|
+
comission: normalizeNumber(link?.comission ?? link?.commission) ?? 0,
|
|
92
|
+
minimum_comission:
|
|
93
|
+
normalizeNumber(link?.minimum_comission ?? link?.minimumComission) ?? 0,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate commission inputs before save.
|
|
98
|
+
* comission is a percentage (0–100); minimum_comission is a monetary minimum (>= 0).
|
|
99
|
+
*/
|
|
100
|
+
export const validateCommissionFields = ({comission, minimum_comission} = {}) => {
|
|
101
|
+
const c = normalizeNumber(comission);
|
|
102
|
+
const m = normalizeNumber(minimum_comission);
|
|
103
|
+
if (c === null || c < 0 || c > 100) {
|
|
104
|
+
return {ok: false, error: 'invalidComission'};
|
|
105
|
+
}
|
|
106
|
+
if (m === null || m < 0) {
|
|
107
|
+
return {ok: false, error: 'invalidMinimumComission'};
|
|
108
|
+
}
|
|
109
|
+
return {ok: true, comission: c, minimum_comission: m};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Format percentage for read-only display.
|
|
114
|
+
*/
|
|
115
|
+
export const formatPercent = value => {
|
|
116
|
+
const n = normalizeNumber(value);
|
|
117
|
+
if (n === null) {
|
|
118
|
+
return '—';
|
|
119
|
+
}
|
|
120
|
+
return `${n}%`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Format monetary minimum for read-only display (raw number, no currency symbol).
|
|
125
|
+
*/
|
|
126
|
+
export const formatMinimum = value => {
|
|
127
|
+
const n = normalizeNumber(value);
|
|
128
|
+
if (n === null) {
|
|
129
|
+
return '—';
|
|
130
|
+
}
|
|
131
|
+
return String(n);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export {extractId, normalizeNumber};
|