diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx index a036a2b..d297ae7 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx @@ -47,6 +47,7 @@ import { dataListTitleForListId } from '../../shared/listTitles'; import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { organizationRouteFieldValues } from '../../shared/dictionary/organizationRouteFields'; import { formIssues } from './validation'; import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit'; import { loadUnpaidLeaveRequest } from './load'; @@ -162,16 +163,22 @@ const UnpaidLeaveForm: React.FC = ({ // Планируемый маршрут резолвится по 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( () => 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( loadMatrix, - `${data.Employee}-${editId ?? 'new'}`, + `${data.Employee}-${editId ?? 'new'}-${routeOrgKey}`, !readOnly, isOpenedRouteReady(editId, requestStatus, requestTypeId) && (!!editId || (!!data.Employee && !!requestTypeId)), (login) => approval.getJobTitle(login) @@ -185,7 +192,8 @@ const UnpaidLeaveForm: React.FC = ({ requestTypeId, userEmail: data.Employee, editId, - requestStatus + requestStatus, + fieldValues: routeFieldValues }); // Проблемы полей (см. validation.ts) подсвечиваются под полями через FieldIssuesProvider. diff --git a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx index d09752a..b271c99 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx @@ -46,6 +46,7 @@ import { dataListTitleForListId } from '../../shared/listTitles'; 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 { loadVacationRequest } from './load'; import { RequestSection } from './sections/RequestSection'; @@ -170,16 +171,24 @@ const VacationForm: React.FC = ({ // Планируемый маршрут резолвится по 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( () => 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( loadMatrix, - `${data.Employee}-${editId ?? 'new'}`, + `${data.Employee}-${editId ?? 'new'}-${routeOrgKey}`, !readOnly, isOpenedRouteReady(editId, requestStatus, requestTypeId) && (!!editId || (!!data.Employee && !!requestTypeId)), (login) => approval.getJobTitle(login) @@ -194,7 +203,8 @@ const VacationForm: React.FC = ({ requestTypeId, userEmail: data.Employee, editId, - requestStatus + requestStatus, + fieldValues: routeFieldValues }); const daysNum = Number(data.DaysCount); diff --git a/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.test.ts b/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.test.ts new file mode 100644 index 0000000..21c352e --- /dev/null +++ b/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.test.ts @@ -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([]); + }); +}); diff --git a/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.ts b/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.ts new file mode 100644 index 0000000..7159f89 --- /dev/null +++ b/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.ts @@ -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 }] : []; +}