воринги теперь кнопка отрьрадаетвя

This commit is contained in:
gorbenko.pavel
2026-08-19 10:11:36 +05:00
parent 80d8790ae1
commit 4825443a9d
12 changed files with 101 additions and 57 deletions
@@ -23,7 +23,7 @@ import {
isEditableStatus, isEditableStatus,
IApproverToWrite, IApproverToWrite,
APPROVAL_PHASE, APPROVAL_PHASE,
findUnassignedStep unassignedStepMessage
} from '../../shared/approval'; } from '../../shared/approval';
import { IRequestFormProps } from '../../models/types'; import { IRequestFormProps } from '../../models/types';
import { import {
@@ -236,14 +236,16 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
if (matrix.error) { if (matrix.error) {
return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`;
} }
const unassigned = findUnassignedStep(matrix.rows); const unassignedApproverError = unassignedStepMessage(
if (unassigned) { matrix.rows,
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
} );
const unassignedExecutor = findUnassignedStep(executors.rows); if (unassignedApproverError) return unassignedApproverError;
if (unassignedExecutor) { const unassignedExecutorError = unassignedStepMessage(
return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`; executors.rows,
} (r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`
);
if (unassignedExecutorError) return unassignedExecutorError;
return undefined; return undefined;
}; };
@@ -27,7 +27,7 @@ import {
isEditableStatus, isEditableStatus,
IApproverToWrite, IApproverToWrite,
APPROVAL_PHASE, APPROVAL_PHASE,
findUnassignedStep unassignedStepMessage
} from '../../shared/approval'; } from '../../shared/approval';
import { import {
uploadAttachments, uploadAttachments,
@@ -237,14 +237,16 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
const validateSubmit = (): string | undefined => { const validateSubmit = (): string | undefined => {
const formError = validate(); const formError = validate();
if (formError) return formError; if (formError) return formError;
const unassigned = findUnassignedStep(matrix.rows); const unassignedApproverError = unassignedStepMessage(
if (unassigned) { matrix.rows,
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
} );
const unassignedExecutor = findUnassignedStep(executors.rows); if (unassignedApproverError) return unassignedApproverError;
if (unassignedExecutor) { const unassignedExecutorError = unassignedStepMessage(
return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`; executors.rows,
} (r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`
);
if (unassignedExecutorError) return unassignedExecutorError;
return undefined; return undefined;
}; };
@@ -26,7 +26,7 @@ import {
REQUEST_STATUS, REQUEST_STATUS,
isEditableStatus, isEditableStatus,
IApproverToWrite, IApproverToWrite,
findUnassignedStep unassignedStepMessage
} from '../../shared/approval'; } from '../../shared/approval';
import { import {
uploadAttachments, uploadAttachments,
@@ -211,10 +211,11 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
if (matrix.error) { if (matrix.error) {
return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`;
} }
const unassigned = findUnassignedStep(matrix.rows); const unassignedError = unassignedStepMessage(
if (unassigned) { matrix.rows,
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
} );
if (unassignedError) return unassignedError;
return undefined; return undefined;
}; };
@@ -26,7 +26,7 @@ import {
REQUEST_STATUS, REQUEST_STATUS,
isEditableStatus, isEditableStatus,
IApproverToWrite, IApproverToWrite,
findUnassignedStep unassignedStepMessage
} from '../../shared/approval'; } from '../../shared/approval';
import { import {
uploadAttachments, uploadAttachments,
@@ -296,10 +296,11 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
// Матрица нередактируема (маршрут задаётся видом работ, а не заявителем) — // Матрица нередактируема (маршрут задаётся видом работ, а не заявителем) —
// незаполненный шаг заявитель исправить сам не может, только администратор // незаполненный шаг заявитель исправить сам не может, только администратор
// маршрутов (роль в шаге не разрешилась в конкретного человека). // маршрутов (роль в шаге не разрешилась в конкретного человека).
const unassigned = findUnassignedStep(matrix.rows); const unassignedError = unassignedStepMessage(
if (unassigned) { matrix.rows,
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») не назначен на конкретного человека. Обратитесь к администратору маршрутов согласования.`; (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») не назначен на конкретного человека. Обратитесь к администратору маршрутов согласования.`
} );
if (unassignedError) return unassignedError;
return undefined; return undefined;
}; };
@@ -26,7 +26,7 @@ import {
isEditableStatus, isEditableStatus,
IApproverToWrite, IApproverToWrite,
APPROVAL_PHASE, APPROVAL_PHASE,
findUnassignedStep, unassignedStepMessage,
resolveWriteListId resolveWriteListId
} from '../../shared/approval'; } from '../../shared/approval';
import { import {
@@ -245,14 +245,16 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({
const validateSubmit = (): string | undefined => { const validateSubmit = (): string | undefined => {
const formError = validate(); const formError = validate();
if (formError) return formError; if (formError) return formError;
const unassigned = findUnassignedStep(matrix.rows); const unassignedApproverError = unassignedStepMessage(
if (unassigned) { matrix.rows,
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
} );
const unassignedExecutor = findUnassignedStep(executors.rows); if (unassignedApproverError) return unassignedApproverError;
if (unassignedExecutor) { const unassignedExecutorError = unassignedStepMessage(
return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`; executors.rows,
} (r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`
);
if (unassignedExecutorError) return unassignedExecutorError;
return undefined; return undefined;
}; };
@@ -21,7 +21,7 @@ import {
REQUEST_STATUS, REQUEST_STATUS,
isEditableStatus, isEditableStatus,
IApproverToWrite, IApproverToWrite,
findUnassignedStep, unassignedStepMessage,
resolveWriteListId resolveWriteListId
} from '../../shared/approval'; } from '../../shared/approval';
import { IRequestFormProps } from '../../models/types'; import { IRequestFormProps } from '../../models/types';
@@ -217,10 +217,11 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
if (matrix.error) { if (matrix.error) {
return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`;
} }
const unassigned = findUnassignedStep(matrix.rows); const unassignedError = unassignedStepMessage(
if (unassigned) { matrix.rows,
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
} );
if (unassignedError) return unassignedError;
return undefined; return undefined;
}; };
@@ -22,7 +22,7 @@ import {
REQUEST_STATUS, REQUEST_STATUS,
isEditableStatus, isEditableStatus,
IApproverToWrite, IApproverToWrite,
findUnassignedStep unassignedStepMessage
} from '../../shared/approval'; } from '../../shared/approval';
import { IRequestFormProps } from '../../models/types'; import { IRequestFormProps } from '../../models/types';
import { import {
@@ -215,10 +215,11 @@ const VacationForm: React.FC<IRequestFormProps> = ({
if (matrix.error) { if (matrix.error) {
return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`; return `Не удалось определить маршрут согласования: ${matrix.error}. Попробуйте ещё раз или обратитесь к администратору.`;
} }
const unassigned = findUnassignedStep(matrix.rows); const unassignedError = unassignedStepMessage(
if (unassigned) { matrix.rows,
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
} );
if (unassignedError) return unassignedError;
return undefined; return undefined;
}; };
@@ -27,7 +27,7 @@ import {
isEditableStatus, isEditableStatus,
IApproverToWrite, IApproverToWrite,
APPROVAL_PHASE, APPROVAL_PHASE,
findUnassignedStep unassignedStepMessage
} from '../../shared/approval'; } from '../../shared/approval';
import { import {
uploadAttachments, uploadAttachments,
@@ -262,14 +262,16 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({
const validateSubmit = (): string | undefined => { const validateSubmit = (): string | undefined => {
const formError = validate(); const formError = validate();
if (formError) return formError; if (formError) return formError;
const unassigned = findUnassignedStep(matrix.rows); const unassignedApproverError = unassignedStepMessage(
if (unassigned) { matrix.rows,
return `Шаг ${unassigned.order} маршрута согласования («${unassigned.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`; (r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») без назначенного человека — выберите согласующего или удалите шаг.`
} );
const unassignedExecutor = findUnassignedStep(executors.rows); if (unassignedApproverError) return unassignedApproverError;
if (unassignedExecutor) { const unassignedExecutorError = unassignedStepMessage(
return `Шаг ${unassignedExecutor.order} исполнения («${unassignedExecutor.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`; executors.rows,
} (r) => `Шаг ${r.order} исполнения («${r.position || 'без должности'}») без назначенного человека — выберите исполнителя или удалите шаг.`
);
if (unassignedExecutorError) return unassignedExecutorError;
return undefined; return undefined;
}; };
@@ -38,6 +38,7 @@ export async function getPlannedMatrix(
phase, phase,
slaHours: s.slaHours, slaHours: s.slaHours,
askOverdueReason: s.askOverdueReason, askOverdueReason: s.askOverdueReason,
warning: s.warning,
})) }))
); );
} }
@@ -50,7 +50,7 @@ export type { ApproverDecision, IPendingRow } from './approverDecision.service';
export { getPlannedMatrix, writeApprovers, writeRoute } from './approvalSubmission.service'; export { getPlannedMatrix, writeApprovers, writeRoute } from './approvalSubmission.service';
export { notifyActiveStep } from './notify.service'; export { notifyActiveStep } from './notify.service';
export type { INotifyTarget } from './notify.service'; export type { INotifyTarget } from './notify.service';
export { useMatrixEditor, findUnassignedStep } from './useMatrixEditor'; export { useMatrixEditor, findUnassignedStep, unassignedStepMessage } from './useMatrixEditor';
export type { IUseMatrixEditor } from './useMatrixEditor'; export type { IUseMatrixEditor } from './useMatrixEditor';
export { ApprovalMatrix } from './ui/ApprovalMatrix'; export { ApprovalMatrix } from './ui/ApprovalMatrix';
export { UserPicker } from './ui/UserPicker'; export { UserPicker } from './ui/UserPicker';
@@ -31,4 +31,16 @@ export interface IMatrixRow {
slaHours?: number | null; slaHours?: number | null;
// eslint-disable-next-line @rushstack/no-new-null -- см. slaHours // eslint-disable-next-line @rushstack/no-new-null -- см. slaHours
askOverdueReason?: boolean | null; askOverdueReason?: boolean | null;
/**
* Предупреждение approval-route по ЭТОМУ шагу (см. IResolvedRouteStep.warning) —
* например, роль не разрешилась в конкретного человека, и почему именно.
* Только у ПЛАНИРУЕМОГО маршрута (см. getPlannedMatrix): у уже записанных строк
* (loadXxxRequest) неоткуда взяться — резолв запускался один раз при подаче.
*
* findUnassignedStep показывает его вместо своего общего текста, когда он есть —
* бэкенд знает причину точнее («роль не назначена в справочнике отдела» и т.п.),
* а не просто «нет назначенного человека».
*/
// eslint-disable-next-line @rushstack/no-new-null -- зеркалит nullable-контракт бэкенда (IResolvedRouteStep.warning)
warning?: string | null;
} }
@@ -118,6 +118,25 @@ export function findUnassignedStep(rows: IMatrixRow[]): IMatrixRow | undefined {
return rows.filter((r) => !r.email).sort((a, b) => a.order - b.order)[0]; return rows.filter((r) => !r.email).sort((a, b) => a.order - b.order)[0];
} }
/**
* Текст ошибки под первый незанятый шаг — то, что раньше было скопировано в
* validateSubmit каждой формы (findUnassignedStep + `row.warning || <шаблон>`).
* fallback принимает саму строку и решает, что писать, если approval-route не
* прислал свою причину (warning) — у большинства форм это общий шаблон «без
* назначенного человека — выберите …», но у некоторых (см. Ticket1CForm) текст
* другой, поэтому шаблон не зашит сюда, а передаётся вызывающей стороной.
*
* undefined — назначенных не хватает не для этих rows (или все шаги заняты).
*/
export function unassignedStepMessage(
rows: IMatrixRow[],
fallback: (row: IMatrixRow) => string
): string | undefined {
const row = findUnassignedStep(rows);
if (!row) return undefined;
return row.warning || fallback(row);
}
/** /**
* Загружает матрицу согласования и (если editable) позволяет её редактировать. * Загружает матрицу согласования и (если editable) позволяет её редактировать.
* @param load загрузчик строк (планируемый маршрут из resolve или строки заявки). * @param load загрузчик строк (планируемый маршрут из resolve или строки заявки).