fix: внес изменения в заявках рекрутинга от 22.09.2026
Tests / frontend-test (push) Successful in 1m19s
Tests / frontend-test (push) Successful in 1m19s
This commit is contained in:
@@ -26,7 +26,9 @@ import {
|
||||
REQUEST_STATUS,
|
||||
isEditableStatus,
|
||||
IApproverToWrite,
|
||||
unassignedStepMessage
|
||||
unassignedStepMessage,
|
||||
resolveApproverOrgInfo,
|
||||
canEditRoute
|
||||
} from '../../shared/approval';
|
||||
import {
|
||||
uploadAttachments,
|
||||
@@ -45,6 +47,7 @@ import {
|
||||
dataListTitleForListId
|
||||
} from '../../shared/listTitles';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField, todayIso } from './model';
|
||||
import { missingRequiredFields } from './validation';
|
||||
import { saveSelectionRequest, selectionApprovalService } from './submit';
|
||||
import { loadSelectionRequest } from './load';
|
||||
import { ApplicationSection } from './sections/ApplicationSection';
|
||||
@@ -169,6 +172,9 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
||||
// selectionApprovalService можно звать не опасаясь исключения.
|
||||
const approval = useMemo(() => selectionApprovalService(editListId), [editListId]);
|
||||
const userEmail = effectiveUserEmail(context);
|
||||
// Право удалять согласующих из маршрута ещё на этапе составления заявки —
|
||||
// см. shared/approval/routeEditRights.ts (сейчас — Алтынай Бултурукова).
|
||||
const canRemoveApprover = canEditRoute(userEmail);
|
||||
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
||||
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
||||
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
||||
@@ -204,7 +210,8 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
||||
`${userEmail}-${editId ?? 'new'}-${projectId}-${companiesFieldValue}`,
|
||||
!readOnly,
|
||||
matrixReady,
|
||||
(login) => approval.getJobTitle(login)
|
||||
(login) => approval.getJobTitle(login),
|
||||
resolveApproverOrgInfo(context)
|
||||
);
|
||||
|
||||
// ObjectProjectId обязателен (а не просто текст ObjectProject): маршрут
|
||||
@@ -229,6 +236,11 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Остальные поля формы обязательны только для ОТПРАВКИ на согласование —
|
||||
// черновик по-прежнему можно сохранить не заполнив их (см. validation.ts).
|
||||
const missingFields = missingRequiredFields(data);
|
||||
const isSubmitValid = isValid && missingFields.length === 0;
|
||||
|
||||
const toApprovers = (rows: typeof matrix.rows): IApproverToWrite[] =>
|
||||
rows.map((r) => ({
|
||||
email: r.email,
|
||||
@@ -306,6 +318,9 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
||||
const validateSubmit = (): string | undefined => {
|
||||
const formError = validate();
|
||||
if (formError) return formError;
|
||||
if (missingFields.length > 0) {
|
||||
return `Заполните обязательные поля: ${missingFields.join(', ')}.`;
|
||||
}
|
||||
if (matrix.error) {
|
||||
return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`;
|
||||
}
|
||||
@@ -389,11 +404,13 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
waiting={!matrixReady}
|
||||
waitingText="Выберите «Объект/Проект» (п. 1.7), чтобы построить маршрут согласования."
|
||||
canRemoveRow={canRemoveApprover ? () => true : undefined}
|
||||
/>
|
||||
|
||||
<SubmitActions
|
||||
submit={submit}
|
||||
isValid={isValid}
|
||||
submitValid={isSubmitValid}
|
||||
readOnly={readOnly}
|
||||
onCancel={onCancel}
|
||||
onSaveDraft={handleSaveDraft}
|
||||
|
||||
@@ -11,9 +11,14 @@ import {
|
||||
useMatrixEditor,
|
||||
notifyActiveStep,
|
||||
useRequestTypeId,
|
||||
isApprovedStatus
|
||||
isApprovedStatus,
|
||||
resolveApproverOrgInfo,
|
||||
canEditRoute,
|
||||
REQUEST_STATUS,
|
||||
IMatrixRow
|
||||
} from '../../shared/approval';
|
||||
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
|
||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||
import {
|
||||
approvalListTitle,
|
||||
@@ -59,6 +64,11 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
const [attachments, setAttachments] = useState<IAttachmentInfo[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Право удалять согласующих из уже поданного маршрута — см.
|
||||
// shared/approval/routeEditRights.ts (сейчас — Алтынай Бултурукова). Не
|
||||
// привязано к активному шагу — не зависит от того, чья сейчас очередь.
|
||||
const canRemoveApprover = canEditRoute(effectiveUserEmail(context));
|
||||
const [routeError, setRouteError] = useState<string | undefined>(undefined);
|
||||
|
||||
// listId уже проверен в ApproveFeature (не смонтировались бы сюда иначе) —
|
||||
// selectionApprovalService можно звать не опасаясь исключения.
|
||||
@@ -74,9 +84,19 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
const [decisionSeq, setDecisionSeq] = useState<number>(0);
|
||||
const loadApprovers = useCallback(() => approval.getMatrixForRequest(itemId), [approval, itemId]);
|
||||
const approvers = useMatrixEditor(loadApprovers, `a-${itemId}-${decisionSeq}`, false, true, (l) =>
|
||||
approval.getJobTitle(l)
|
||||
approval.getJobTitle(l), resolveApproverOrgInfo(context)
|
||||
);
|
||||
|
||||
// Удалить ещё не решённый шаг маршрута (см. ApproverService.deleteQueuedStep).
|
||||
const handleRemoveApprover = (row: IMatrixRow): void => {
|
||||
if (!row.id) return;
|
||||
setRouteError(undefined);
|
||||
approverService
|
||||
.deleteQueuedStep(itemId, row.id)
|
||||
.then(() => setDecisionSeq((n) => n + 1))
|
||||
.catch((e) => setRouteError(friendlyErrorMessage(e)));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
setLoading(true);
|
||||
@@ -160,7 +180,13 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
</div>
|
||||
</FormCard>
|
||||
|
||||
<ApprovalMatrix editor={approvers} search={(q) => approval.searchUsers(q)} />
|
||||
{routeError && <Alert variant="err">{routeError}</Alert>}
|
||||
<ApprovalMatrix
|
||||
editor={approvers}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
canRemoveRow={canRemoveApprover ? (row) => row.status === REQUEST_STATUS.queued : undefined}
|
||||
onRemoveRow={handleRemoveApprover}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Панель решения — ВНЕ печати и FormModeProvider (комментарий редактируемый). */}
|
||||
|
||||
+7
-5
@@ -14,7 +14,9 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
|
||||
const reason = data.VacancyOpenReason;
|
||||
const showReasonDetails = reason !== '' && reason !== OPEN_REASON_NEW;
|
||||
const reasonDetailsLabel =
|
||||
reason === OPEN_REASON_REPLACE || reason === OPEN_REASON_TRANSFER ? 'ФИО сотрудника' : 'Уточните причину';
|
||||
reason === OPEN_REASON_REPLACE || reason === OPEN_REASON_TRANSFER
|
||||
? 'ФИО сотрудника (Полностью, пример: Иванов Иван Иванович)'
|
||||
: 'Уточните причину';
|
||||
|
||||
return (
|
||||
<CollapseSection title="1. Заявка" icon="clipboard">
|
||||
@@ -34,10 +36,10 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
|
||||
placeholder="Подставится автоматически по выбранному ответственному"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="1.3 Дата подачи заявки" htmlFor="SubmitDate">
|
||||
<FormField label="1.3 Дата подачи заявки" htmlFor="SubmitDate" required>
|
||||
<TextInput id="SubmitDate" type="date" value={data.SubmitDate} onChange={(v) => set('SubmitDate', v)} readOnly />
|
||||
</FormField>
|
||||
<FormField label="1.4 Крайний срок закрытия" htmlFor="ClosingDeadline">
|
||||
<FormField label="1.4 Крайний срок закрытия" htmlFor="ClosingDeadline" required>
|
||||
<TextInput
|
||||
id="ClosingDeadline"
|
||||
type="date"
|
||||
@@ -46,7 +48,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
|
||||
min={data.SubmitDate || undefined}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="1.5 Причина открытия вакансии" htmlFor="VacancyOpenReason">
|
||||
<FormField label="1.5 Причина открытия вакансии" htmlFor="VacancyOpenReason" required>
|
||||
<Select
|
||||
id="VacancyOpenReason"
|
||||
value={data.VacancyOpenReason}
|
||||
@@ -67,7 +69,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField label="1.6 Организация" htmlFor="Companies" full>
|
||||
<FormField label="1.6 Организация" htmlFor="Companies" required full>
|
||||
<CompanyPickerMulti
|
||||
context={context}
|
||||
id="Companies"
|
||||
|
||||
@@ -25,14 +25,14 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
context,
|
||||
}) => (
|
||||
<CollapseSection title="5. Условия работы" icon="chart">
|
||||
<FormField label="5.1 Ставка на ИС" htmlFor="RateProbation">
|
||||
<FormField label="5.1 Ставка на ИС" htmlFor="RateProbation" required>
|
||||
<MoneyInput
|
||||
id="RateProbation"
|
||||
value={data.RateProbation}
|
||||
onChange={(v) => set("RateProbation", v)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.2 Ставка после ИС" htmlFor="RateAfterProbation">
|
||||
<FormField label="5.2 Ставка после ИС" htmlFor="RateAfterProbation" required>
|
||||
<MoneyInput
|
||||
id="RateAfterProbation"
|
||||
value={data.RateAfterProbation}
|
||||
@@ -46,7 +46,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
onLabel="Да"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.4 График работы" htmlFor="WorkSchedule">
|
||||
<FormField label="5.4 График работы" htmlFor="WorkSchedule" required>
|
||||
<Select
|
||||
id="WorkSchedule"
|
||||
value={data.WorkSchedule}
|
||||
@@ -67,7 +67,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField label="5.5 Время работы" htmlFor="WorkHours">
|
||||
<FormField label="5.5 Время работы" htmlFor="WorkHours" required>
|
||||
<Select
|
||||
id="WorkHours"
|
||||
value={data.WorkHours}
|
||||
@@ -95,7 +95,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
onLabel="Да"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.7 Обеспечение рабочего места" full>
|
||||
<FormField label="5.7 Обеспечение рабочего места" required full>
|
||||
<MultiChoiceField
|
||||
value={data.WorkplaceProvision}
|
||||
onChange={(v) => set("WorkplaceProvision", v)}
|
||||
@@ -111,7 +111,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
onLabel="Да"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.9 Предполагаемый наставник">
|
||||
<FormField label="5.9 Предполагаемый наставник" required>
|
||||
<PeopleField
|
||||
context={context}
|
||||
value={data.ProposedMentor}
|
||||
@@ -119,7 +119,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
placeholder={PERSON_PLACEHOLDER}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.10 Непосредственный руководитель">
|
||||
<FormField label="5.10 Непосредственный руководитель" required>
|
||||
<PeopleField
|
||||
context={context}
|
||||
value={data.DirectManager}
|
||||
@@ -127,7 +127,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
placeholder={PERSON_PLACEHOLDER}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.11 Кто принимает решение о приеме (собеседования)" full>
|
||||
<FormField label="5.11 Кто принимает решение о приеме (собеседования)" required full>
|
||||
<PeopleFieldMulti
|
||||
context={context}
|
||||
value={data.HiringDecisionMaker}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ISectionProps } from '../model';
|
||||
|
||||
export const DepartmentSection: React.FC<ISectionProps> = ({ data, set, context }) => (
|
||||
<CollapseSection title="2. Отдел / Подразделение" icon="building">
|
||||
<FormField label="2.1 Подразделение" htmlFor="Department" full>
|
||||
<FormField label="2.1 Подразделение" htmlFor="Department" required full>
|
||||
<DepartmentPicker
|
||||
id="Department"
|
||||
context={context}
|
||||
@@ -16,7 +16,7 @@ export const DepartmentSection: React.FC<ISectionProps> = ({ data, set, context
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="2.2 Количество открытых вакансий" htmlFor="OpenVacanciesCount">
|
||||
<FormField label="2.2 Количество открытых вакансий" htmlFor="OpenVacanciesCount" required>
|
||||
<TextInput
|
||||
id="OpenVacanciesCount"
|
||||
type="number"
|
||||
|
||||
@@ -24,7 +24,7 @@ export const VacancySection: React.FC<ISectionProps> = ({ data, set, context })
|
||||
placeholder="Начните вводить название должности…"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="3.2 Характер занятости" htmlFor="EmploymentType">
|
||||
<FormField label="3.2 Характер занятости" htmlFor="EmploymentType" required>
|
||||
<Select
|
||||
id="EmploymentType"
|
||||
value={data.EmploymentType}
|
||||
@@ -33,16 +33,16 @@ export const VacancySection: React.FC<ISectionProps> = ({ data, set, context })
|
||||
placeholder="— выберите —"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="3.3 Функционал" htmlFor="JobFunction" full>
|
||||
<FormField label="3.3 Функционал" htmlFor="JobFunction" required full>
|
||||
<TextArea id="JobFunction" value={data.JobFunction} onChange={(v) => set('JobFunction', v)} />
|
||||
</FormField>
|
||||
<FormField label="3.4 Компетенции" htmlFor="Competencies" full>
|
||||
<FormField label="3.4 Компетенции" htmlFor="Competencies" required full>
|
||||
<TextArea id="Competencies" value={data.Competencies} onChange={(v) => set('Competencies', v)} />
|
||||
</FormField>
|
||||
<FormField label="3.5 Личностные характеристики" htmlFor="PersonalTraits" full>
|
||||
<FormField label="3.5 Личностные характеристики" htmlFor="PersonalTraits" required full>
|
||||
<TextArea id="PersonalTraits" value={data.PersonalTraits} onChange={(v) => set('PersonalTraits', v)} />
|
||||
</FormField>
|
||||
<FormField label="3.6 Образование" htmlFor="Education">
|
||||
<FormField label="3.6 Образование" htmlFor="Education" required>
|
||||
<Select
|
||||
id="Education"
|
||||
value={data.Education}
|
||||
@@ -51,7 +51,7 @@ export const VacancySection: React.FC<ISectionProps> = ({ data, set, context })
|
||||
placeholder="— выберите —"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="3.7 Опыт работы" htmlFor="WorkExperience">
|
||||
<FormField label="3.7 Опыт работы" htmlFor="WorkExperience" required>
|
||||
<Select
|
||||
id="WorkExperience"
|
||||
value={data.WorkExperience}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { IFormData } from './model';
|
||||
|
||||
/**
|
||||
* Поля формы, обязательные для ОТПРАВКИ на согласование (черновик по-прежнему
|
||||
* можно сохранить не заполнив их). Исключения по требованию заказчика: 3.8
|
||||
* «Необходимые знания в программах» (Programs), 3.9 «Иностранные языки»
|
||||
* (Languages), 3.11 «Водительские права, стаж вождения» (DrivingLicense).
|
||||
* Toggle-поля (BusinessTrips, PieceworkPay, MaterialResponsibility,
|
||||
* ReferralProgram) и условные «другое»-подполя в проверку не входят — у
|
||||
* булевых нет пустого состояния, а «другое» раскрывается только при выборе
|
||||
* соответствующего варианта в уже обязательном родительском поле.
|
||||
*/
|
||||
const REQUIRED_FIELDS: ReadonlyArray<{ label: string; ok: (d: IFormData) => boolean }> = [
|
||||
{ label: '1.3 дата подачи заявки', ok: (d) => !!d.SubmitDate },
|
||||
{ label: '1.4 крайний срок закрытия', ok: (d) => !!d.ClosingDeadline },
|
||||
{ label: '1.5 причина открытия вакансии', ok: (d) => !!d.VacancyOpenReason },
|
||||
{ label: '1.6 организация', ok: (d) => d.Companies.length > 0 },
|
||||
{ label: '2.1 подразделение', ok: (d) => !!d.Department.trim() },
|
||||
{ label: '2.2 количество открытых вакансий', ok: (d) => !!d.OpenVacanciesCount.trim() },
|
||||
{ label: '3.2 характер занятости', ok: (d) => !!d.EmploymentType },
|
||||
{ label: '3.3 функционал', ok: (d) => !!d.JobFunction.trim() },
|
||||
{ label: '3.4 компетенции', ok: (d) => !!d.Competencies.trim() },
|
||||
{ label: '3.5 личностные характеристики', ok: (d) => !!d.PersonalTraits.trim() },
|
||||
{ label: '3.6 образование', ok: (d) => !!d.Education },
|
||||
{ label: '3.7 опыт работы', ok: (d) => !!d.WorkExperience },
|
||||
{ label: '5.1 ставка на ИС', ok: (d) => !!d.RateProbation.trim() },
|
||||
{ label: '5.2 ставка после ИС', ok: (d) => !!d.RateAfterProbation.trim() },
|
||||
{ label: '5.4 график работы', ok: (d) => !!d.WorkSchedule },
|
||||
{ label: '5.5 время работы', ok: (d) => !!d.WorkHours },
|
||||
{ label: '5.7 обеспечение рабочего места', ok: (d) => d.WorkplaceProvision.length > 0 },
|
||||
{ label: '5.9 предполагаемый наставник', ok: (d) => !!d.ProposedMentor },
|
||||
{ label: '5.10 непосредственный руководитель', ok: (d) => !!d.DirectManager },
|
||||
{ label: '5.11 кто принимает решение о приёме', ok: (d) => d.HiringDecisionMaker.length > 0 }
|
||||
];
|
||||
|
||||
/** Подписи незаполненных обязательных-для-отправки полей (пусто — всё заполнено). */
|
||||
export function missingRequiredFields(data: IFormData): string[] {
|
||||
return REQUIRED_FIELDS.filter((f) => !f.ok(data)).map((f) => f.label);
|
||||
}
|
||||
Reference in New Issue
Block a user