feat: маршрут отпуска и отпуска без содержания учитывает организацию сотрудника
Кадровый сотрудник и исполнитель в этих маршрутах зависят от компании сотрудника (Ольга Кудрящова и Айнур — Тансу/КПС/CT, Мольдир Хамит — остальные), но формы не передавали организацию в расчёт маршрута, и условные шаги в админке не срабатывали. Теперь названия компаний из поля «Организация» (Employee/ByEmail) уходят как Organization через пробел — и в согласование, и в исполнителей; маршрут пересчитывается, когда организация подгрузится. Общий хелпер — shared/dictionary/organizationRouteFields. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ import {
|
|||||||
dataListTitleForListId
|
dataListTitleForListId
|
||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
|
import { organizationRouteFieldValues } from '../../shared/dictionary/organizationRouteFields';
|
||||||
import { formIssues } from './validation';
|
import { formIssues } from './validation';
|
||||||
import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit';
|
import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit';
|
||||||
import { loadUnpaidLeaveRequest } from './load';
|
import { loadUnpaidLeaveRequest } from './load';
|
||||||
@@ -162,16 +163,22 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Планируемый маршрут резолвится по e-mail ВЫБРАННОГО сотрудника (не текущего
|
// Планируемый маршрут резолвится по e-mail ВЫБРАННОГО сотрудника (не текущего
|
||||||
// пользователя) — так работает подача за другого. У заявки на редактировании
|
// пользователя) — так работает подача за другого. У заявки на редактировании
|
||||||
// маршрут читается из уже записанных строк, чтобы не затереть правки инициатора.
|
// маршрут читается из уже записанных строк, чтобы не затереть правки инициатора.
|
||||||
|
//
|
||||||
|
// Кадровый сотрудник и исполнитель зависят от организации сотрудника (условия
|
||||||
|
// «Organization содержит …» в админке) — см. shared/dictionary/organizationRouteFields.ts.
|
||||||
|
// Организация приходит позже выбора сотрудника, поэтому она в ключе перезагрузки.
|
||||||
|
const routeFieldValues = useMemo(() => organizationRouteFieldValues(data.Organization), [data.Organization]);
|
||||||
|
const routeOrgKey = routeFieldValues.map((f) => f.value).join('|');
|
||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
loadRoutePhase(approval, editId, requestStatus, 'Approval', () =>
|
loadRoutePhase(approval, editId, requestStatus, 'Approval', () =>
|
||||||
getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval')
|
getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval', null, routeFieldValues)
|
||||||
),
|
),
|
||||||
[context, approval, editId, requestStatus, data.Employee, requestTypeId]
|
[context, approval, editId, requestStatus, data.Employee, requestTypeId, routeFieldValues]
|
||||||
);
|
);
|
||||||
const matrix = useMatrixEditor(
|
const matrix = useMatrixEditor(
|
||||||
loadMatrix,
|
loadMatrix,
|
||||||
`${data.Employee}-${editId ?? 'new'}`,
|
`${data.Employee}-${editId ?? 'new'}-${routeOrgKey}`,
|
||||||
!readOnly,
|
!readOnly,
|
||||||
isOpenedRouteReady(editId, requestStatus, requestTypeId) && (!!editId || (!!data.Employee && !!requestTypeId)),
|
isOpenedRouteReady(editId, requestStatus, requestTypeId) && (!!editId || (!!data.Employee && !!requestTypeId)),
|
||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
@@ -185,7 +192,8 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
requestTypeId,
|
requestTypeId,
|
||||||
userEmail: data.Employee,
|
userEmail: data.Employee,
|
||||||
editId,
|
editId,
|
||||||
requestStatus
|
requestStatus,
|
||||||
|
fieldValues: routeFieldValues
|
||||||
});
|
});
|
||||||
|
|
||||||
// Проблемы полей (см. validation.ts) подсвечиваются под полями через FieldIssuesProvider.
|
// Проблемы полей (см. validation.ts) подсвечиваются под полями через FieldIssuesProvider.
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
dataListTitleForListId
|
dataListTitleForListId
|
||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField, MIN_NOTICE_DAYS, MAX_DAYS } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField, MIN_NOTICE_DAYS, MAX_DAYS } from './model';
|
||||||
|
import { organizationRouteFieldValues } from '../../shared/dictionary/organizationRouteFields';
|
||||||
import { saveVacationRequest, vacationApprovalService } from './submit';
|
import { saveVacationRequest, vacationApprovalService } from './submit';
|
||||||
import { loadVacationRequest } from './load';
|
import { loadVacationRequest } from './load';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
@@ -170,16 +171,24 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
// Планируемый маршрут резолвится по e-mail ВЫБРАННОГО сотрудника (не текущего
|
// Планируемый маршрут резолвится по e-mail ВЫБРАННОГО сотрудника (не текущего
|
||||||
// пользователя) — так работает подача за другого. У заявки на редактировании
|
// пользователя) — так работает подача за другого. У заявки на редактировании
|
||||||
// маршрут читается из уже записанных строк, чтобы не затереть правки инициатора.
|
// маршрут читается из уже записанных строк, чтобы не затереть правки инициатора.
|
||||||
|
//
|
||||||
|
// Кадровый сотрудник и исполнитель в маршруте зависят от организации
|
||||||
|
// сотрудника (условия «Organization содержит …» в админке) — см.
|
||||||
|
// shared/dictionary/organizationRouteFields.ts. Организация приходит позже
|
||||||
|
// выбора сотрудника (Employee/ByEmail), поэтому она в ключе перезагрузки:
|
||||||
|
// маршрут пересчитается, когда придёт, а не останется рассчитанным без условий.
|
||||||
|
const routeFieldValues = useMemo(() => organizationRouteFieldValues(data.Organization), [data.Organization]);
|
||||||
|
const routeOrgKey = routeFieldValues.map((f) => f.value).join('|');
|
||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
loadRoutePhase(approval, editId, requestStatus, 'Approval', () =>
|
loadRoutePhase(approval, editId, requestStatus, 'Approval', () =>
|
||||||
getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval')
|
getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval', null, routeFieldValues)
|
||||||
),
|
),
|
||||||
[context, approval, editId, requestStatus, data.Employee, requestTypeId]
|
[context, approval, editId, requestStatus, data.Employee, requestTypeId, routeFieldValues]
|
||||||
);
|
);
|
||||||
const matrix = useMatrixEditor(
|
const matrix = useMatrixEditor(
|
||||||
loadMatrix,
|
loadMatrix,
|
||||||
`${data.Employee}-${editId ?? 'new'}`,
|
`${data.Employee}-${editId ?? 'new'}-${routeOrgKey}`,
|
||||||
!readOnly,
|
!readOnly,
|
||||||
isOpenedRouteReady(editId, requestStatus, requestTypeId) && (!!editId || (!!data.Employee && !!requestTypeId)),
|
isOpenedRouteReady(editId, requestStatus, requestTypeId) && (!!editId || (!!data.Employee && !!requestTypeId)),
|
||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
@@ -194,7 +203,8 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
requestTypeId,
|
requestTypeId,
|
||||||
userEmail: data.Employee,
|
userEmail: data.Employee,
|
||||||
editId,
|
editId,
|
||||||
requestStatus
|
requestStatus,
|
||||||
|
fieldValues: routeFieldValues
|
||||||
});
|
});
|
||||||
|
|
||||||
const daysNum = Number(data.DaysCount);
|
const daysNum = Number(data.DaysCount);
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { organizationRouteFieldValues } from './organizationRouteFields';
|
||||||
|
|
||||||
|
describe('organizationRouteFieldValues', () => {
|
||||||
|
it('одна организация — её название', () => {
|
||||||
|
expect(organizationRouteFieldValues('ТОО TANSU Construction — Инженер — ИТ департамент')).toEqual([
|
||||||
|
{ fieldName: 'Organization', value: 'ТОО TANSU Construction' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('строки без подразделения (формат отпуска без содержания)', () => {
|
||||||
|
expect(organizationRouteFieldValues('ТОО АСТЕЛ-К — Экономист')).toEqual([
|
||||||
|
{ fieldName: 'Organization', value: 'ТОО АСТЕЛ-К' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('несколько организаций — все названия через пробел, без повторов', () => {
|
||||||
|
const organization = [
|
||||||
|
'ТОО АСТЕЛ-К — Экономист',
|
||||||
|
'ТОО TANSU Construction — Инженер — ИТ департамент',
|
||||||
|
'ТОО АСТЕЛ-К — Аналитик',
|
||||||
|
].join('\n');
|
||||||
|
expect(organizationRouteFieldValues(organization)).toEqual([
|
||||||
|
{ fieldName: 'Organization', value: 'ТОО TANSU Construction ТОО АСТЕЛ-К' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('организация ещё не загружена — условий нет', () => {
|
||||||
|
expect(organizationRouteFieldValues('')).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Напрямую из модулей, а не из баррелей: баррели тянут @microsoft/sp-http,
|
||||||
|
// который не грузится в Jest (см. features/myRequests/filtersStorage.ts).
|
||||||
|
import type { IResolveFieldValue } from '../approval/models/IApprovalRouteApi';
|
||||||
|
import { parseOrganizationLine } from './organizationLines';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Значения для условных шагов маршрута (fieldValues при resolve) по
|
||||||
|
* организации сотрудника — для форм, где кадровый сотрудник и исполнитель
|
||||||
|
* зависят от компании (заявка на отпуск, отпуск без содержания). Условия в
|
||||||
|
* админке approval-route: «Organization содержит TANSU …».
|
||||||
|
*
|
||||||
|
* Названия компаний — из поля «Организация» (строки «Компания — должность[ —
|
||||||
|
* подразделение]», источник Employee/ByEmail), одной строкой через пробел:
|
||||||
|
* бэкенд принимает только строку (массив → 400), а «содержит» ищет подстроку.
|
||||||
|
* Сотрудник в нескольких компаниях попадает под условия каждой — так и задумано.
|
||||||
|
*/
|
||||||
|
export function organizationRouteFieldValues(organization: string): IResolveFieldValue[] {
|
||||||
|
const companies = organization
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => parseOrganizationLine(line).company)
|
||||||
|
.filter((name) => !!name);
|
||||||
|
const names = companies.filter((name, i) => companies.indexOf(name) === i).sort().join(' ');
|
||||||
|
return names ? [{ fieldName: 'Organization', value: names }] : [];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user