From 25c1188c3292c9e0d2ac401964a6e79ce35f0a70 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: Mon, 21 Sep 2026 09:29:34 +0500 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=80=D0=B5=D0=BA=D1=80=D1=83=D1=82=D0=B8=D0=BD=D0=B3=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B4=D0=B0=D0=B2=D0=B0=D0=BB=D0=B8=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=B2=D1=8B=D1=8C=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0,=20?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BF=D1=80=D0=BE=D0=BF=D1=83=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=BF=D1=83=D1=81=D1=82=D1=83=D1=8E=20=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D1=80=D0=B8=D1=86=D1=83=20=D1=81=D0=BE=D0=B3=D0=BB?= =?UTF-8?q?=D0=B0=D1=81=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecruitingHire/RecruitingHireForm.tsx | 28 +++- .../sections/ApplicationSection.tsx | 2 +- .../sections/CandidateSection.tsx | 11 +- .../RecruitingSelectionForm.tsx | 27 +++- .../sections/ApplicationSection.tsx | 2 +- .../sections/VacancySection.tsx | 11 +- .../requestPortal/shared/dictionary/index.ts | 4 + .../shared/dictionary/models/IPosition.ts | 14 ++ .../shared/dictionary/positions.service.ts | 27 ++++ .../dictionary/ui/PositionPicker.module.scss | 90 ++++++++++++ .../shared/dictionary/ui/PositionPicker.tsx | 136 ++++++++++++++++++ 11 files changed, 342 insertions(+), 10 deletions(-) create mode 100644 src/webparts/requestPortal/shared/dictionary/models/IPosition.ts create mode 100644 src/webparts/requestPortal/shared/dictionary/positions.service.ts create mode 100644 src/webparts/requestPortal/shared/dictionary/ui/PositionPicker.module.scss create mode 100644 src/webparts/requestPortal/shared/dictionary/ui/PositionPicker.tsx diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx index 1ed32c8..5af07e9 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx @@ -196,10 +196,20 @@ const RecruitingHireForm: React.FC = ({ // При совмещении (2+ компании в «Организация») нужно явно указать, какая из них основная. const mainCompanyMissing = data.Companies.length >= 2 && !data.MainCompanyId; - const isValid = data.HireCandidateName.trim().length > 0 && data.Position.trim().length > 0 && !mainCompanyMissing; + // ProjectId обязателен (а не просто текст Project): маршрут резолвится ПО + // ПРОЕКТУ (ByProject, см. projectId выше), и без выбранного из списка + // варианта (id) approval-route вернёт 404 — форма должна не пускать на + // отправку раньше, чем выяснится, что маршрут вообще некому строить (баг: + // раньше это не проверялось, и заявка молча уходила на согласование с + // пустой матрицей). + const isValid = + data.HireCandidateName.trim().length > 0 && + data.Position.trim().length > 0 && + !!data.ProjectId && + !mainCompanyMissing; const validate = (): string | undefined => { if (mainCompanyMissing) return 'Выбрано несколько организаций — укажите, какая из них основное место работы.'; - if (!isValid) return 'Заполните обязательные поля: Ф.И.О. кандидата, Должность.'; + if (!isValid) return 'Заполните обязательные поля: Ф.И.О. кандидата, Должность, Проект (выберите вариант из списка).'; if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`; if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.'; return undefined; @@ -296,6 +306,20 @@ const RecruitingHireForm: React.FC = ({ const validateSubmit = (): string | undefined => { const formError = validate(); if (formError) return formError; + if (matrix.error) { + return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; + } + if (matrix.loading) { + return 'Маршрут согласования ещё формируется — подождите немного и повторите.'; + } + // Матрица пуста (не только «есть шаг без человека») — раньше это молча + // пропускалось (findUnassignedStep на пустом массиве ошибки не даёт), и + // заявка уходила на согласование без единого согласующего. У фазы + // исполнения (executors) пустой список — нормальное состояние (не у + // всех маршрутов есть шаги исполнения), там эта проверка не нужна. + if (matrix.rows.length === 0) { + return 'Маршрут согласования не найден для выбранного проекта — обратитесь к администратору.'; + } const unassignedApproverError = unassignedStepMessage( matrix.rows, (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.` diff --git a/src/webparts/requestPortal/forms/RecruitingHire/sections/ApplicationSection.tsx b/src/webparts/requestPortal/forms/RecruitingHire/sections/ApplicationSection.tsx index 0ace1f6..4b4a817 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/sections/ApplicationSection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/sections/ApplicationSection.tsx @@ -49,7 +49,7 @@ export const ApplicationSection: React.FC = ({ data, set, context shares={data.CompanyShares} onSharesChange={(v) => set('CompanyShares', v)} /> - + = ({ data, set }) => ( +export const CandidateSection: React.FC = ({ data, set, context }) => ( set('HireCandidateName', v)} /> @@ -11,7 +12,13 @@ export const CandidateSection: React.FC = ({ data, set }) => ( set('HireHireDate', v)} /> - set('Position', v)} /> + set('Position', v)} + placeholder="Начните вводить название должности…" + /> set('WorkKind', v)} /> diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index 5c8c744..7c22e33 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -207,9 +207,23 @@ const RecruitingSelectionForm: React.FC = ({ (login) => approval.getJobTitle(login) ); - const isValid = !!(data.Responsible && data.DepartmentName.trim() && data.Position.trim() && data.Residence); + // ObjectProjectId обязателен (а не просто текст ObjectProject): маршрут + // согласования резолвится ПО ПРОЕКТУ (ByProject, см. projectId выше), и без + // выбранного из списка варианта (id) approval-route вернёт 404 — форма + // должна не пускать на отправку раньше, чем выяснится, что маршрут вообще + // некому строить (баг: раньше это не проверялось, и заявка молча уходила + // на согласование с пустой матрицей). + const isValid = !!( + data.Responsible && + data.DepartmentName.trim() && + data.Position.trim() && + data.Residence && + data.ObjectProjectId + ); const validate = (): string | undefined => { - if (!isValid) return 'Заполните обязательные поля: ответственный за подачу, название отдела, должность, проживание.'; + if (!isValid) { + return 'Заполните обязательные поля: ответственный за подачу, название отдела, должность, проживание, объект/проект (выберите вариант из списка).'; + } if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`; if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.'; return undefined; @@ -295,6 +309,15 @@ const RecruitingSelectionForm: React.FC = ({ if (matrix.error) { return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; } + if (matrix.loading) { + return 'Маршрут согласования ещё формируется — подождите немного и повторите.'; + } + // Матрица пуста (не только «есть шаг без человека») — раньше это молча + // пропускалось (findUnassignedStep на пустом массиве ошибки не даёт), и + // заявка уходила на согласование без единого согласующего. + if (matrix.rows.length === 0) { + return 'Маршрут согласования не найден для выбранного объекта/проекта — обратитесь к администратору.'; + } const unassignedError = unassignedStepMessage( matrix.rows, (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.` diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/sections/ApplicationSection.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/sections/ApplicationSection.tsx index 025ceb8..8f1a63b 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/sections/ApplicationSection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/sections/ApplicationSection.tsx @@ -76,7 +76,7 @@ export const ApplicationSection: React.FC = ({ data, set, context placeholder="Начните вводить название организации…" /> - + = ({ data, set }) => ( +export const VacancySection: React.FC = ({ data, set, context }) => ( - set('Position', v)} /> + set('Position', v)} + placeholder="Начните вводить название должности…" + /> { + onChange(e.target.value); + setOpen(true); + }} + onFocus={() => setOpen(true)} + onBlur={() => setOpen(false)} + /> + {/* Быстрая очистка. preventDefault на mousedown — чтобы клик не «съел» + фокус до onClick (как в списке ниже). */} + {!effectiveReadOnly && value.length > 0 && ( + + )} + {!effectiveReadOnly && open && loading && ( +
+
Поиск…
+
+ )} + {!effectiveReadOnly && open && !loading && suggestions.length > 0 && ( +
+ {suggestions.map((p) => ( + + ))} +
+ )} + + ); +};