From 0005bf0cbd453620cc8523f17fb0317198237cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Thu, 29 Oct 2026 12:47:00 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BA=D0=B0=D0=B4=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D0=BA=20=D0=B2=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83?= =?UTF-8?q?=D1=82=D0=B5=20=D0=BE=D1=82=D0=BF=D1=83=D1=81=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20=D1=81=D0=BE=D0=B4=D0=B5=D1=80=D0=B6=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D1=82?= =?UTF-8?q?=20=D0=BE=D1=82=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В маршрут #15 добавлен тот же шаг кадровика, что в ежегодном отпуске: в офисе — кадровик компании, на объекте — кадровик объекта. Форма теперь передаёт проект в approval-route, поле «Проект» обязательно и только из справочника. Сборка «организация + проект» вынесена в общий organizationProjectRouteFieldValues — ей пользуются обе формы отпуска. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../forms/UnpaidLeave/UnpaidLeaveForm.tsx | 14 ++++++++----- .../requestPortal/forms/UnpaidLeave/model.ts | 2 +- .../UnpaidLeave/sections/RequestSection.tsx | 2 +- .../forms/UnpaidLeave/validation.ts | 12 +++++++++++ .../forms/Vacation/VacationForm.tsx | 11 +++++----- .../forms/Vacation/routeFields.ts | 20 ------------------- .../organizationRouteFields.test.ts | 17 +++++++++++++++- .../dictionary/organizationRouteFields.ts | 13 ++++++++++++ 8 files changed, 58 insertions(+), 33 deletions(-) delete mode 100644 src/webparts/requestPortal/forms/Vacation/routeFields.ts diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx index c46091c..c896389 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx @@ -47,7 +47,7 @@ import { dataListTitleForListId } from '../../shared/listTitles'; import { FORM_ID, IFormData, EMPTY_DATA, SetField, NOTICE_TITLE, NOTICE_ITEMS } from './model'; -import { organizationRouteFieldValues } from '../../shared/dictionary/organizationRouteFields'; +import { organizationProjectRouteFieldValues } from '../../shared/dictionary/organizationRouteFields'; import { formIssues } from './validation'; import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit'; import { loadUnpaidLeaveRequest } from './load'; @@ -164,10 +164,14 @@ const UnpaidLeaveForm: React.FC = ({ // пользователя) — так работает подача за другого. У заявки на редактировании // маршрут читается из уже записанных строк, чтобы не затереть правки инициатора. // - // Кадровый сотрудник и исполнитель зависят от организации сотрудника (условия - // «Organization содержит …» в админке) — см. shared/dictionary/organizationRouteFields.ts. - // Организация приходит позже выбора сотрудника, поэтому она в ключе перезагрузки. - const routeFieldValues = useMemo(() => organizationRouteFieldValues(data.Organization), [data.Organization]); + // Кадровый сотрудник и исполнитель зависят от организации сотрудника и проекта + // (условия «Organization содержит …», «Project содержит …» в админке) — см. + // shared/dictionary/organizationRouteFields.ts. Организация приходит позже + // выбора сотрудника, поэтому она в ключе перезагрузки. + const routeFieldValues = useMemo( + () => organizationProjectRouteFieldValues(data.Organization, data.Project), + [data.Organization, data.Project] + ); const routeOrgKey = routeFieldValues.map((f) => f.value).join('|'); const loadMatrix = useCallback( () => diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/model.ts b/src/webparts/requestPortal/forms/UnpaidLeave/model.ts index baca43f..31f08a2 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/model.ts +++ b/src/webparts/requestPortal/forms/UnpaidLeave/model.ts @@ -47,7 +47,7 @@ export interface IFormData { Department: string; /** Id отдела из справочника; '' — если введён вручную без выбора из списка. */ DepartmentId: string; - /** Проект (справочник /Project, см. shared/dictionary/ProjectPicker) — необязательное, справочное. */ + /** Проект (справочник /Project, см. shared/dictionary/ProjectPicker) — обязательный, от него зависит кадровик в маршруте. */ Project: string; /** oid проекта ('' — если введён вручную без выбора из списка). */ ProjectId: string; diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/sections/RequestSection.tsx index 83919d8..416b1c7 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/sections/RequestSection.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/sections/RequestSection.tsx @@ -39,7 +39,7 @@ export const RequestSection: React.FC = ({ data, set, context, on }} /> - + = [ data.Employee ? undefined : { field: 'Employee', label: 'Сотрудник' }, data.Position.trim() ? undefined : { field: 'Position', label: 'Должность' }, + projectIssue(data), startDateIssue(data), daysCountIssue(data), data.ReasonType diff --git a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx index d317554..d2d425f 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx @@ -48,7 +48,7 @@ import { } from '../../shared/listTitles'; import { FORM_ID, IFormData, EMPTY_DATA, SetField, NOTICE_TITLE, NOTICE_ITEMS } from './model'; import { formIssues } from './validation'; -import { vacationRouteFieldValues } from './routeFields'; +import { organizationProjectRouteFieldValues } from '../../shared/dictionary/organizationRouteFields'; import { saveVacationRequest, vacationApprovalService } from './submit'; import { loadVacationRequest } from './load'; import { RequestSection } from './sections/RequestSection'; @@ -176,11 +176,12 @@ const VacationForm: React.FC = ({ // // Кадровый сотрудник и исполнитель в маршруте зависят от организации // сотрудника и проекта (условия «Organization содержит …», «Project - // содержит …» в админке) — см. routeFields.ts. Организация приходит позже - // выбора сотрудника (Employee/ByEmail), поэтому она в ключе перезагрузки: - // маршрут пересчитается, когда придёт, а не останется рассчитанным без условий. + // содержит …» в админке) — см. shared/dictionary/organizationRouteFields.ts. + // Организация приходит позже выбора сотрудника (Employee/ByEmail), поэтому + // она в ключе перезагрузки: маршрут пересчитается, когда придёт, а не + // останется рассчитанным без условий. const routeFieldValues = useMemo( - () => vacationRouteFieldValues({ Organization: data.Organization, Project: data.Project }), + () => organizationProjectRouteFieldValues(data.Organization, data.Project), [data.Organization, data.Project] ); const routeOrgKey = routeFieldValues.map((f) => f.value).join('|'); diff --git a/src/webparts/requestPortal/forms/Vacation/routeFields.ts b/src/webparts/requestPortal/forms/Vacation/routeFields.ts deleted file mode 100644 index 0bdfd04..0000000 --- a/src/webparts/requestPortal/forms/Vacation/routeFields.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { IResolveFieldValue } from '../../shared/approval/models/IApprovalRouteApi'; -import { organizationRouteFieldValues } from '../../shared/dictionary/organizationRouteFields'; -import { IFormData } from './model'; - -/** Поля заявки, от которых зависит маршрут отпуска (условия шагов в админке маршрута #9). */ -export type VacationRouteInput = Pick; - -/** - * Значения полей для условных шагов маршрута (fieldValues при resolve). - * - * Кадровик на шаге 1 зависит и от компании, и от проекта: в офисе — кадровик - * компании («Organization содержит TANSU И Project содержит Офис TANSU»), на - * объекте — кадровик объекта («Project содержит Топар»). Проект — название из - * справочника /Project, как его показывает поле формы. - */ -export function vacationRouteFieldValues(input: VacationRouteInput): IResolveFieldValue[] { - const fieldValues = organizationRouteFieldValues(input.Organization); - const project = input.Project.trim(); - return project ? [...fieldValues, { fieldName: 'Project', value: project }] : fieldValues; -} diff --git a/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.test.ts b/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.test.ts index 21c352e..84a40a2 100644 --- a/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.test.ts +++ b/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.test.ts @@ -1,4 +1,4 @@ -import { organizationRouteFieldValues } from './organizationRouteFields'; +import { organizationProjectRouteFieldValues, organizationRouteFieldValues } from './organizationRouteFields'; describe('organizationRouteFieldValues', () => { it('одна организация — её название', () => { @@ -28,3 +28,18 @@ describe('organizationRouteFieldValues', () => { expect(organizationRouteFieldValues('')).toEqual([]); }); }); + +describe('organizationProjectRouteFieldValues', () => { + it('организация и проект — две записи', () => { + expect(organizationProjectRouteFieldValues('ТОО TANSU Construction — Инженер', ' Офис TANSU ')).toEqual([ + { fieldName: 'Organization', value: 'ТОО TANSU Construction' }, + { fieldName: 'Project', value: 'Офис TANSU' }, + ]); + }); + + it('проект не выбран — только организация', () => { + expect(organizationProjectRouteFieldValues('ТОО АСТЕЛ-К — Экономист', '')).toEqual([ + { fieldName: 'Organization', value: 'ТОО АСТЕЛ-К' }, + ]); + }); +}); diff --git a/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.ts b/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.ts index 7159f89..56b599b 100644 --- a/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.ts +++ b/src/webparts/requestPortal/shared/dictionary/organizationRouteFields.ts @@ -22,3 +22,16 @@ export function organizationRouteFieldValues(organization: string): IResolveFiel const names = companies.filter((name, i) => companies.indexOf(name) === i).sort().join(' '); return names ? [{ fieldName: 'Organization', value: names }] : []; } + +/** + * Организация + проект — для форм, где кадровик зависит и от места работы + * (отпуск, отпуск без содержания): в офисе — кадровик компании («Organization + * содержит TANSU И Project содержит Офис TANSU»), на объекте — кадровик + * объекта («Project содержит …»). Проект — название из справочника /Project, + * как его показывает поле формы. + */ +export function organizationProjectRouteFieldValues(organization: string, project: string): IResolveFieldValue[] { + const fieldValues = organizationRouteFieldValues(organization); + const projectName = project.trim(); + return projectName ? [...fieldValues, { fieldName: 'Project', value: projectName }] : fieldValues; +}