Compare commits
4 Commits
4f99a3c5d8
...
d4868933f1
| Author | SHA1 | Date | |
|---|---|---|---|
| d4868933f1 | |||
| 053ca9d8cd | |||
| bce2283bf3 | |||
| 50bb3e2307 |
@@ -22,6 +22,7 @@
|
|||||||
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` |
|
| Заменять и удалять исполнителей, **в том числе активного** («На исполнении») | Алтынай Бултурукова | тот же email (`canEditRoute`) | **Только** Заявка на подбор, матрица «Исполнение» | `canEditRoute` + `forms/RecruitingSelection/RecruitingSelectionView.tsx` |
|
||||||
| Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде (список по каждой форме) | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
|
| Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде (список по каждой форме) | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` |
|
||||||
| То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` |
|
| То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` |
|
||||||
|
| Заменять человека на шаге согласования (не удалять шаг) **до отправки**: новая заявка, черновик, доработка | Любой инициатор | право по статусу заявки (форма редактируема) | **Только** Организация рабочего места | `forms/WorkplaceSetup/WorkplaceSetupForm.tsx` (`canReplaceRow`) + `useMatrixEditor.replace` |
|
||||||
| Заполнять «Необходимое ПО» и «Доступы» на своём шаге согласования | Первый шаг маршрута согласования (по маршруту — непосредственный руководитель) | порядок шага в маршруте | Организация рабочего места | `forms/WorkplaceSetup/useApproverRights.ts` (`canEditReviewFields`) |
|
| Заполнять «Необходимое ПО» и «Доступы» на своём шаге согласования | Первый шаг маршрута согласования (по маршруту — непосредственный руководитель) | порядок шага в маршруте | Организация рабочего места | `forms/WorkplaceSetup/useApproverRights.ts` (`canEditReviewFields`) |
|
||||||
| Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` |
|
| Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` |
|
||||||
| Ставить дату приёма (2.2) | Активный исполнитель | роль в маршруте (шаг «Исполнение»), без проверки должности | Заявка на приём | `forms/RecruitingHire/RecruitingHireView.tsx` (`hireDateEditable`) |
|
| Ставить дату приёма (2.2) | Активный исполнитель | роль в маршруте (шаг «Исполнение»), без проверки должности | Заявка на приём | `forms/RecruitingHire/RecruitingHireView.tsx` (`hireDateEditable`) |
|
||||||
|
|||||||
@@ -34,8 +34,11 @@ import {
|
|||||||
isEditableStatus,
|
isEditableStatus,
|
||||||
IApproverToWrite,
|
IApproverToWrite,
|
||||||
APPROVAL_PHASE,
|
APPROVAL_PHASE,
|
||||||
|
IResolveFieldValue,
|
||||||
unassignedStepMessage
|
unassignedStepMessage
|
||||||
} from '../../shared/approval';
|
} from '../../shared/approval';
|
||||||
|
import { useDebouncedQuery } from '../../shared/usePersistedQuery';
|
||||||
|
import { routeFieldValues } from './routeFields';
|
||||||
import {
|
import {
|
||||||
uploadAttachments,
|
uploadAttachments,
|
||||||
getAttachments,
|
getAttachments,
|
||||||
@@ -43,7 +46,6 @@ import {
|
|||||||
IAttachmentInfo
|
IAttachmentInfo
|
||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
import { effectiveUserEmail } from '../../shared/impersonation';
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { getEmployeeOrganizations, employeeCompanies } from '../../shared/dictionary';
|
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import {
|
import {
|
||||||
@@ -141,8 +143,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
|||||||
loadHirePrefill(sourceId)
|
loadHirePrefill(sourceId)
|
||||||
.then((prefilled) => {
|
.then((prefilled) => {
|
||||||
if (alive) {
|
if (alive) {
|
||||||
// Merge, а не replace: организация заявителя (ниже) резолвится отдельным
|
// Merge, а не replace: пользователь мог успеть что-то ввести до ответа.
|
||||||
// эффектом и могла успеть записаться раньше — не затираем её.
|
|
||||||
setData((prev) => ({ ...prev, ...prefilled }));
|
setData((prev) => ({ ...prev, ...prefilled }));
|
||||||
setSourceHire((prev) => (prev ? { ...prev, candidateName: prefilled.FullName } : prev));
|
setSourceHire((prev) => (prev ? { ...prev, candidateName: prefilled.FullName } : prev));
|
||||||
}
|
}
|
||||||
@@ -163,37 +164,19 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
|||||||
const approval = useMemo(() => workplaceApprovalService(editListId), [editListId]);
|
const approval = useMemo(() => workplaceApprovalService(editListId), [editListId]);
|
||||||
const userEmail = effectiveUserEmail(context);
|
const userEmail = effectiveUserEmail(context);
|
||||||
|
|
||||||
// Организации заявителя (Employee/ByEmail) — форма не о самом заявителе, а о
|
|
||||||
// новом сотруднике (FullName — обычный текст, предзаполняется из «Заявки на
|
|
||||||
// прием», и его чаще всего ещё нет в системе кадров), поэтому организация
|
|
||||||
// тянется по ТЕКУЩЕМУ пользователю. Это только предложение: поле дальше
|
|
||||||
// редактируется вручную. На редактировании уже сохранённое значение не перезатираем.
|
|
||||||
useEffect(() => {
|
|
||||||
if (editId || !userEmail) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let alive = true;
|
|
||||||
getEmployeeOrganizations(context, userEmail)
|
|
||||||
.then((records) => {
|
|
||||||
if (alive) {
|
|
||||||
setData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
Companies: prev.Companies.length > 0 ? prev.Companies : employeeCompanies(records)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => undefined);
|
|
||||||
return () => {
|
|
||||||
alive = false;
|
|
||||||
};
|
|
||||||
// context — единственный экземпляр WebPartContext на всё время жизни
|
|
||||||
// веб-части, так что перезапуска эффекта эта зависимость не добавляет.
|
|
||||||
}, [editId, userEmail, context]);
|
|
||||||
|
|
||||||
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
||||||
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
||||||
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
||||||
const requestTypeId = requestTypeIdState.id;
|
const requestTypeId = requestTypeIdState.id;
|
||||||
|
// Условные шаги маршрута зависят от организаций и ПО (см. routeFields.ts).
|
||||||
|
// ПО — свободный текст: в маршрут уходит после паузы в наборе, а не на
|
||||||
|
// каждую букву. Сравниваем по содержимому — массив пересоздаётся на каждый рендер.
|
||||||
|
const softwareForRoute = useDebouncedQuery(data.Software);
|
||||||
|
const fieldValuesKey = JSON.stringify(routeFieldValues(data, softwareForRoute));
|
||||||
|
const fieldValues = useMemo(() => JSON.parse(fieldValuesKey) as IResolveFieldValue[], [fieldValuesKey]);
|
||||||
|
// Отдел и поля условий влияют на резолв маршрута — их смена у НОВОЙ заявки перепланирует матрицу.
|
||||||
|
const matrixKey = `${userEmail}-${editId ?? 'new'}-${data.DepartmentId}-${fieldValuesKey}`;
|
||||||
|
|
||||||
// У новой заявки маршрут ПЛАНИРУЕТСЯ (resolve по внешнему API), у открытой на
|
// У новой заявки маршрут ПЛАНИРУЕТСЯ (resolve по внешнему API), у открытой на
|
||||||
// редактирование — читается из уже записанных строк: их могли поправить
|
// редактирование — читается из уже записанных строк: их могли поправить
|
||||||
// вручную при первом сохранении, и планирование затёрло бы эти правки.
|
// вручную при первом сохранении, и планирование затёрло бы эти правки.
|
||||||
@@ -203,13 +186,12 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
|||||||
? approval
|
? approval
|
||||||
.getMatrixForRequest(editId)
|
.getMatrixForRequest(editId)
|
||||||
.then((rows) => rows.filter((r) => r.phase !== APPROVAL_PHASE.execution))
|
.then((rows) => rows.filter((r) => r.phase !== APPROVAL_PHASE.execution))
|
||||||
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', data.DepartmentId),
|
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval', data.DepartmentId, fieldValues),
|
||||||
[context, userEmail, approval, editId, data.DepartmentId, requestTypeId]
|
[context, userEmail, approval, editId, data.DepartmentId, requestTypeId, fieldValues]
|
||||||
);
|
);
|
||||||
const matrix = useMatrixEditor(
|
const matrix = useMatrixEditor(
|
||||||
loadMatrix,
|
loadMatrix,
|
||||||
// Отдел влияет на резолв маршрута — смена отдела у НОВОЙ заявки должна перепланировать матрицу.
|
matrixKey,
|
||||||
`${userEmail}-${editId ?? 'new'}-${data.DepartmentId}`,
|
|
||||||
!readOnly,
|
!readOnly,
|
||||||
!!editId || !!requestTypeId,
|
!!editId || !!requestTypeId,
|
||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
@@ -222,12 +204,12 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
|||||||
? approval
|
? approval
|
||||||
.getMatrixForRequest(editId)
|
.getMatrixForRequest(editId)
|
||||||
.then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution))
|
.then((rows) => rows.filter((r) => r.phase === APPROVAL_PHASE.execution))
|
||||||
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Execution', data.DepartmentId),
|
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Execution', data.DepartmentId, fieldValues),
|
||||||
[context, userEmail, approval, editId, data.DepartmentId, requestTypeId]
|
[context, userEmail, approval, editId, data.DepartmentId, requestTypeId, fieldValues]
|
||||||
);
|
);
|
||||||
const executors = useMatrixEditor(
|
const executors = useMatrixEditor(
|
||||||
loadExecutors,
|
loadExecutors,
|
||||||
`${userEmail}-${editId ?? 'new'}-${data.DepartmentId}`,
|
matrixKey,
|
||||||
canEditExecutors(context, readOnly),
|
canEditExecutors(context, readOnly),
|
||||||
!!editId || !!requestTypeId,
|
!!editId || !!requestTypeId,
|
||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
@@ -432,6 +414,11 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
|
|||||||
editor={matrix}
|
editor={matrix}
|
||||||
search={(q) => approval.searchUsers(q)}
|
search={(q) => approval.searchUsers(q)}
|
||||||
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
canRemoveRow={composeRemoveRowRule(effectiveUserEmail(context))}
|
||||||
|
// Пока заявка не отправлена, любой инициатор может поставить на шаг
|
||||||
|
// другого человека (напр. согласующий в отпуске), но не убрать шаг:
|
||||||
|
// состав маршрута из админки остаётся гарантированным.
|
||||||
|
canReplaceRow={readOnly ? undefined : () => true}
|
||||||
|
onReplaceRow={matrix.replace}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!executors.loading && (executors.editable || executors.rows.length > 0) && (
|
{!executors.loading && (executors.editable || executors.rows.length > 0) && (
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export interface IFormData {
|
|||||||
StartDate: string;
|
StartDate: string;
|
||||||
/**
|
/**
|
||||||
* Организации заявителя — мультивыбор из справочника (CompanyPickerMulti).
|
* Организации заявителя — мультивыбор из справочника (CompanyPickerMulti).
|
||||||
* Предзаполняется из Employee/ByEmail по e-mail заявителя (не нового
|
* Не предзаполняется (решение Тимура 2026-10-07: организация инициатора
|
||||||
* сотрудника — его ещё нет в системе кадров), дальше редактируется вручную.
|
* подставлялась не та) — выбирается вручную.
|
||||||
* В SP хранится JSON-ом в колонке WorkplaceCompaniesJson (см. submit.ts).
|
* В SP хранится JSON-ом в колонке WorkplaceCompaniesJson (см. submit.ts).
|
||||||
*/
|
*/
|
||||||
Companies: ICompany[];
|
Companies: ICompany[];
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { IResolveFieldValue } from '../../shared/approval';
|
||||||
|
import { COMPANIES_JSON_FIELD, IFormData } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Значения полей заявки для условных шагов маршрута (fieldValues при resolve).
|
||||||
|
* В админке approval-route условия заданы по internalName колонок:
|
||||||
|
* «WorkplaceCompaniesJson содержит АСТЕЛ-К», «Organization содержит …»,
|
||||||
|
* «Software содержит AutoCAD».
|
||||||
|
*
|
||||||
|
* Организации — одной строкой, имена через пробел: бэкенд принимает только
|
||||||
|
* строку (массив → 400), а «содержит» ищет подстроку. Тот же формат, что в
|
||||||
|
* заявках на приём и подбор (см. RecruitingHireForm).
|
||||||
|
*
|
||||||
|
* Маршрут планируется при подаче, поэтому условие по Software сработает,
|
||||||
|
* только если «Необходимое ПО» указал сам инициатор (руководитель заполняет
|
||||||
|
* его позже, на своём шаге).
|
||||||
|
*/
|
||||||
|
export function routeFieldValues(
|
||||||
|
data: Pick<IFormData, 'Companies'>,
|
||||||
|
software: string
|
||||||
|
): IResolveFieldValue[] {
|
||||||
|
const fieldValues: IResolveFieldValue[] = [];
|
||||||
|
const companyNames = data.Companies.map((c) => c.Name).sort().join(' ');
|
||||||
|
if (companyNames) {
|
||||||
|
fieldValues.push({ fieldName: COMPANIES_JSON_FIELD, value: companyNames });
|
||||||
|
fieldValues.push({ fieldName: 'Organization', value: companyNames });
|
||||||
|
}
|
||||||
|
const softwareText = software.trim();
|
||||||
|
if (softwareText) {
|
||||||
|
fieldValues.push({ fieldName: 'Software', value: softwareText });
|
||||||
|
}
|
||||||
|
return fieldValues;
|
||||||
|
}
|
||||||
@@ -17,6 +17,12 @@ export interface IUseMatrixEditor {
|
|||||||
moveBlockUp: (index: number) => void;
|
moveBlockUp: (index: number) => void;
|
||||||
moveBlockDown: (index: number) => void;
|
moveBlockDown: (index: number) => void;
|
||||||
remove: (index: number) => void;
|
remove: (index: number) => void;
|
||||||
|
/**
|
||||||
|
* Поставить в строку другого человека. Шаг (порядок, роль, SLA и прочие
|
||||||
|
* настройки из админки) остаётся прежним — меняется только исполнитель шага.
|
||||||
|
* Человек, уже стоящий в маршруте, не подставляется (дубль шага).
|
||||||
|
*/
|
||||||
|
replace: (row: IMatrixRow, user: ISiteUser) => void;
|
||||||
/** Переключить «идёт параллельно с предыдущим шагом». Для первой строки — no-op. */
|
/** Переключить «идёт параллельно с предыдущим шагом». Для первой строки — no-op. */
|
||||||
toggleParallel: (index: number) => void;
|
toggleParallel: (index: number) => void;
|
||||||
/** Границы шагов: индексы строк, объединённых общим order. */
|
/** Границы шагов: индексы строк, объединённых общим order. */
|
||||||
@@ -285,6 +291,13 @@ export function useMatrixEditor(
|
|||||||
const remove = (i: number): void =>
|
const remove = (i: number): void =>
|
||||||
setRows((prev) => withOrders(prev.filter((_, idx) => idx !== i)));
|
setRows((prev) => withOrders(prev.filter((_, idx) => idx !== i)));
|
||||||
|
|
||||||
|
const replace = (row: IMatrixRow, user: ISiteUser): void =>
|
||||||
|
setRows((prev) => {
|
||||||
|
if (user.email && prev.some((r) => r !== row && r.email === user.email)) return prev;
|
||||||
|
// warning убираем: он был про незаполненную роль шага, а теперь человек выбран вручную.
|
||||||
|
return prev.map((r) => (r === row ? { ...r, who: user.name, email: user.email, warning: null } : r));
|
||||||
|
});
|
||||||
|
|
||||||
const toggleParallel = (i: number): void =>
|
const toggleParallel = (i: number): void =>
|
||||||
setRows((prev) =>
|
setRows((prev) =>
|
||||||
i <= 0
|
i <= 0
|
||||||
@@ -302,6 +315,7 @@ export function useMatrixEditor(
|
|||||||
moveBlockUp,
|
moveBlockUp,
|
||||||
moveBlockDown,
|
moveBlockDown,
|
||||||
remove,
|
remove,
|
||||||
|
replace,
|
||||||
toggleParallel,
|
toggleParallel,
|
||||||
blocks: blockRanges(rows),
|
blocks: blockRanges(rows),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user