Обходной лист (#7)
Tests / frontend-test (push) Successful in 1m16s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/7
This commit was merged in pull request #7.
This commit is contained in:
2026-08-17 05:43:38 +00:00
parent a937e33f2e
commit 364835eaf7
47 changed files with 1962 additions and 359 deletions
@@ -21,6 +21,7 @@ import {
useMatrixEditor,
getPlannedMatrix,
notifyActiveStep,
useRequestTypeId,
REQUEST_STATUS,
isEditableStatus,
IApproverToWrite,
@@ -32,15 +33,10 @@ import {
deleteAttachment,
IAttachmentInfo
} from '../../shared/attachments/attachments.service';
import { effectiveUserEmail } from '../../shared/impersonation';
import { IRequestFormProps } from '../../models/types';
import { IFormData, EMPTY_DATA, SetField } from './model';
import {
saveSelectionRequest,
selectionApprovalService,
RECRUITING_SELECTION_REQUEST_TYPE_ID,
DATA_LIST_TITLE,
APPROVAL_LIST_TITLE
} from './submit';
import { saveSelectionRequest, selectionApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
import { loadSelectionRequest } from './load';
import { ApplicationSection } from './sections/ApplicationSection';
import { DepartmentSection } from './sections/DepartmentSection';
@@ -91,27 +87,33 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
// У заявки на редактировании маршрут читается из уже записанных строк, чтобы не
// затереть правки инициатора при первом сохранении.
const approval = useMemo(() => selectionApprovalService(), []);
const userEmail = context.pageContext.user.email;
const userEmail = effectiveUserEmail(context);
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
const requestTypeIdState = useRequestTypeId(context, requestType.id);
const requestTypeId = requestTypeIdState.id;
const loadMatrix = useCallback(
() =>
editId
? approval.getMatrixForRequest(editId)
: getPlannedMatrix(context, RECRUITING_SELECTION_REQUEST_TYPE_ID, userEmail, approval, 'Approval'),
[context, userEmail, approval, editId]
: getPlannedMatrix(context, requestTypeId as number, userEmail, approval, 'Approval'),
[context, userEmail, approval, editId, requestTypeId]
);
const matrix = useMatrixEditor(
loadMatrix,
`${userEmail}-${editId ?? 'new'}`,
!readOnly,
true,
!!editId || !!requestTypeId,
(login) => approval.getJobTitle(login)
);
const isValid = !!(data.Responsible && data.DepartmentName.trim() && data.Position.trim());
const validate = (): string | undefined =>
isValid
? undefined
: 'Заполните обязательные поля: ответственный за подачу, название отдела, должность.';
const validate = (): string | undefined => {
if (!isValid) return 'Заполните обязательные поля: ответственный за подачу, название отдела, должность.';
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
return undefined;
};
const toApprovers = (rows: typeof matrix.rows): IApproverToWrite[] =>
rows.map((r) => ({
@@ -135,7 +137,14 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
};
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
const { id, code } = await saveSelectionRequest(requestId, data, toApprovers(matrix.rows), status);
// validate()/validateSubmit() уже гарантировали requestTypeId к моменту вызова
// (см. useRequestSubmit.run — валидирует ДО save), но TS этого через границу
// замыкания не видит — локальный const и явная проверка снимают both проблемы.
const rtId = requestTypeId;
if (!rtId) {
throw new Error('Тип заявки не определён — обновите страницу и повторите.');
}
const { id, code } = await saveSelectionRequest(requestId, rtId, data, toApprovers(matrix.rows), status);
setRequestId(id);
setRequestStatus(status);
if (code) {
@@ -152,7 +161,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
if (status === REQUEST_STATUS.onApproval) {
await notifyActiveStep(context, approval, id, {
requestTypeId: RECRUITING_SELECTION_REQUEST_TYPE_ID,
requestTypeId: rtId,
dataListName: DATA_LIST_TITLE,
approvalListName: APPROVAL_LIST_TITLE
});
@@ -9,17 +9,13 @@ import {
StatusBadge,
ApprovalMatrix,
useMatrixEditor,
notifyActiveStep
notifyActiveStep,
useRequestTypeId
} from '../../shared/approval';
import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service';
import { IFormData, EMPTY_DATA, SetField } from './model';
import { loadSelectionRequest } from './load';
import {
selectionApprovalService,
RECRUITING_SELECTION_REQUEST_TYPE_ID,
DATA_LIST_TITLE,
APPROVAL_LIST_TITLE
} from './submit';
import { selectionApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
import { ApplicationSection } from './sections/ApplicationSection';
import { DepartmentSection } from './sections/DepartmentSection';
import { VacancySection } from './sections/VacancySection';
@@ -29,6 +25,8 @@ import { ConditionsSection } from './sections/ConditionsSection';
interface IRecruitingSelectionViewProps {
context: WebPartContext;
itemId: number;
/** pageUrl формы — см. IApprovalViewProps в forms/viewRegistry.ts. */
formKey: string;
}
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
@@ -36,7 +34,7 @@ const NOOP_SET: SetField = () => undefined;
// Заявка «на подбор» только для просмотра (режим согласующего). Переиспользует те же
// секции, что и форма подачи; редактирование выключено через <FormModeProvider readOnly>.
const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ context, itemId }) => {
const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ context, itemId, formKey }) => {
const [data, setData] = useState<IFormData>(EMPTY_DATA);
const [status, setStatus] = useState<string>('');
const [code, setCode] = useState<string>('');
@@ -45,6 +43,7 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
const [error, setError] = useState<string | null>(null);
const approval = useMemo(() => selectionApprovalService(), []);
const { id: requestTypeId } = useRequestTypeId(context, formKey);
const approverService = useMemo(() => new ApproverService(approval, DATA_LIST_TITLE), [approval]);
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
@@ -94,8 +93,9 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
const sectionProps = { data, set: NOOP_SET, context };
const notify = (): void => {
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
requestTypeId: RECRUITING_SELECTION_REQUEST_TYPE_ID,
requestTypeId,
dataListName: DATA_LIST_TITLE,
approvalListName: APPROVAL_LIST_TITLE
}).catch(() => undefined);
@@ -5,8 +5,6 @@ import { IFormData } from './model';
export const DATA_LIST_TITLE = 'Заявка на подбор — данные';
export const APPROVAL_LIST_TITLE = 'Заявка на подбор — согласование';
// Тип заявки во внешнем approval-route API (GET /api/request-types → id 4).
export const RECRUITING_SELECTION_REQUEST_TYPE_ID = 4;
// Поля-люди (один человек): логин → ensureUser → Id в колонку <Field>Id.
const PERSON_FIELDS: (keyof IFormData)[] = ['Responsible', 'ProposedMentor', 'DirectManager'];
@@ -75,6 +73,7 @@ export interface ISaveResult {
*/
export async function saveSelectionRequest(
existingId: number | undefined,
requestTypeId: number,
data: IFormData,
approvers: IApproverToWrite[],
status: string
@@ -93,7 +92,7 @@ export async function saveSelectionRequest(
const added = await list.items.add(item);
id = (added as { Id: number }).Id;
// Код известен только после создания элемента (нужен его Id).
code = buildRequestUniqueCode(RECRUITING_SELECTION_REQUEST_TYPE_ID, id, new Date());
code = buildRequestUniqueCode(requestTypeId, id, new Date());
await list.items.getById(id).update({ RequestUniqueCode: code });
}