Обходной лист (#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
@@ -17,6 +17,7 @@ import {
useMatrixEditor,
getPlannedMatrix,
notifyActiveStep,
useRequestTypeId,
REQUEST_STATUS,
isEditableStatus,
IApproverToWrite,
@@ -24,13 +25,7 @@ import {
} from '../../shared/approval';
import { IRequestFormProps } from '../../models/types';
import { IFormData, EMPTY_DATA, SetField } from './model';
import {
saveVacationRequest,
vacationApprovalService,
VACATION_REQUEST_TYPE_ID,
DATA_LIST_TITLE,
APPROVAL_LIST_TITLE
} from './submit';
import { saveVacationRequest, vacationApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
import { loadVacationRequest } from './load';
import { RequestSection } from './sections/RequestSection';
@@ -45,6 +40,10 @@ const VacationForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
const approval = useMemo(() => vacationApprovalService(), []);
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
const requestTypeIdState = useRequestTypeId(context, requestType.id);
const requestTypeId = requestTypeIdState.id;
// Открыта СВОЯ заявка на редактирование (черновик/возврат) — подгружаем содержимое и статус.
useEffect(() => {
@@ -112,14 +111,14 @@ const VacationForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
() =>
editId
? approval.getMatrixForRequest(editId)
: getPlannedMatrix(context, VACATION_REQUEST_TYPE_ID, data.Employee, approval, 'Approval'),
[context, approval, editId, data.Employee]
: getPlannedMatrix(context, requestTypeId as number, data.Employee, approval, 'Approval'),
[context, approval, editId, data.Employee, requestTypeId]
);
const matrix = useMatrixEditor(
loadMatrix,
`${data.Employee}-${editId ?? 'new'}`,
!readOnly,
!!editId || !!data.Employee,
!!editId || (!!data.Employee && !!requestTypeId),
(login) => approval.getJobTitle(login)
);
@@ -131,8 +130,12 @@ const VacationForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
Number.isFinite(daysNum) &&
daysNum > 0
);
const validate = (): string | undefined =>
isValid ? undefined : 'Заполните обязательные поля: сотрудник, должность, дата начала, количество дней (> 0).';
const validate = (): string | undefined => {
if (!isValid) return 'Заполните обязательные поля: сотрудник, должность, дата начала, количество дней (> 0).';
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
return undefined;
};
const toApprovers = (rows: typeof matrix.rows): IApproverToWrite[] =>
rows.map((r) => ({
@@ -147,7 +150,14 @@ const VacationForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
}));
const save = (status: string): (() => Promise<void>) => async (): Promise<void> => {
const { id, code } = await saveVacationRequest(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 saveVacationRequest(requestId, rtId, data, toApprovers(matrix.rows), status);
setRequestId(id);
setRequestStatus(status);
if (code) {
@@ -159,7 +169,7 @@ const VacationForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
// Отправка на согласование — уведомляем первый активный шаг (поток оповещения).
if (status === REQUEST_STATUS.onApproval) {
await notifyActiveStep(context, approval, id, {
requestTypeId: VACATION_REQUEST_TYPE_ID,
requestTypeId: rtId,
dataListName: DATA_LIST_TITLE,
approvalListName: APPROVAL_LIST_TITLE
});
@@ -9,21 +9,19 @@ import {
StatusBadge,
ApprovalMatrix,
useMatrixEditor,
notifyActiveStep
notifyActiveStep,
useRequestTypeId
} from '../../shared/approval';
import { IFormData, EMPTY_DATA, SetField } from './model';
import { loadVacationRequest } from './load';
import {
vacationApprovalService,
VACATION_REQUEST_TYPE_ID,
DATA_LIST_TITLE,
APPROVAL_LIST_TITLE
} from './submit';
import { vacationApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
import { RequestSection } from './sections/RequestSection';
interface IVacationViewProps {
context: WebPartContext;
itemId: number;
/** pageUrl формы — см. IApprovalViewProps в forms/viewRegistry.ts. */
formKey: string;
}
// В режиме чтения поля не редактируются, поэтому обновление — пустышка.
@@ -32,7 +30,7 @@ const NOOP_SET: SetField = () => undefined;
// Заявка на отпуск только для просмотра (режим согласующего). Переиспользует ту
// же секцию, что и форма подачи; редактирование выключено через
// <FormModeProvider readOnly> — поля сами становятся неизменяемыми.
const VacationView: React.FC<IVacationViewProps> = ({ context, itemId }) => {
const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey }) => {
const [data, setData] = useState<IFormData>(EMPTY_DATA);
const [status, setStatus] = useState<string>('');
const [code, setCode] = useState<string>('');
@@ -40,6 +38,7 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId }) => {
const [error, setError] = useState<string | null>(null);
const approval = useMemo(() => vacationApprovalService(), []);
const { id: requestTypeId } = useRequestTypeId(context, formKey);
const approverService = useMemo(() => new ApproverService(approval, DATA_LIST_TITLE), [approval]);
// Матрица маршрута из фактических строк заявки (read-only). decisionSeq в
@@ -88,8 +87,12 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId }) => {
const sectionProps = { data, set: NOOP_SET, context };
const notify = (): void => {
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
// обновлена), просто нечего уведомлять на следующем шаге; не проваливаем
// весь UI ради необязательного оповещения.
if (!requestTypeId) return;
notifyActiveStep(context, approval, itemId, {
requestTypeId: VACATION_REQUEST_TYPE_ID,
requestTypeId,
dataListName: DATA_LIST_TITLE,
approvalListName: APPROVAL_LIST_TITLE
}).catch(() => undefined);
@@ -5,11 +5,6 @@ import { IFormData } from './model';
export const DATA_LIST_TITLE = 'Заявка на отпуск — данные';
export const APPROVAL_LIST_TITLE = 'Заявка на отпуск — согласование';
// Тип заявки «отпуск» во внешнем approval-route API (GET /api/request-types → id 11,
// name «Заявка на отпуск», pageUrl «hr-vacation»). Захардкожен, как
// HIRE_REQUEST_TYPE_ID=5 в RecruitingHire. NB: в старой web part pto-tracker было 1 —
// при заведении типа в админке портала он получил новый id 11, берём его.
export const VACATION_REQUEST_TYPE_ID = 11;
/** Экземпляр сервиса списка согласования этой формы. */
export function vacationApprovalService(): ApprovalListService {
@@ -67,6 +62,7 @@ export interface ISaveResult {
*/
export async function saveVacationRequest(
existingId: number | undefined,
requestTypeId: number,
data: IFormData,
approvers: IApproverToWrite[],
status: string
@@ -84,7 +80,7 @@ export async function saveVacationRequest(
const added = await list.items.add(item);
id = (added as { Id: number }).Id;
// Код известен только после создания элемента (нужен его Id).
code = buildRequestUniqueCode(VACATION_REQUEST_TYPE_ID, id, new Date());
code = buildRequestUniqueCode(requestTypeId, id, new Date());
await list.items.getById(id).update({ RequestUniqueCode: code });
}