Обходной лист (#12)
Tests / backend-test (push) Successful in 1m38s
Tests / frontend-test (push) Successful in 47s

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/12
This commit was merged in pull request #12.
This commit is contained in:
2026-08-17 05:43:52 +00:00
parent d076f4e98d
commit 48ede1420d
37 changed files with 1351 additions and 435 deletions
+78 -3
View File
@@ -31,6 +31,7 @@ export const headTypeLabels: Record<HeadType, string> = {
[HeadType.Chief]: 'Непосредственный руководитель',
[HeadType.DirectionHead]: 'Руководитель направления',
[HeadType.DirectorTansu]: 'Директор Tansu',
[HeadType.ManagingDirectorTansu]: 'Управляющий директор Tansu',
[HeadType.DepartmentHead]: 'Руководитель указанного отдела'
};
@@ -158,6 +159,7 @@ export const texts = {
filterAll: 'Все типы заявок',
dictionaryFailed: (message: string): string => `Справочник компаний и отделов недоступен: ${message}`,
emptyList: 'Маршрутов пока нет.',
columnId: '№ маршрута',
columnRequestType: 'Тип заявки',
columnCompany: 'Компания',
columnDepartment: 'Отдел',
@@ -185,6 +187,38 @@ export const texts = {
controlLabel: 'Контроль',
controlCheckbox: 'Требуется отдельный контроль исполнения',
validationRequestType: 'Выберите тип заявки.',
// ---- Копирование маршрута ----
copy: 'Копировать',
copyTitle: (routeId: number): string => `Копия маршрута #${routeId}`,
copyAction: 'Создать копию',
copyInProgress: 'Копирование...',
copyHint:
'Копия создаётся для того же типа заявки вместе со всеми шагами. Маршрут различается по компании, ' +
'отделу и признаку «по проекту» — измените хотя бы одно из них, иначе копия совпадёт с исходным маршрутом.',
copySource: (requestTypeName: string, company: string, department: string): string =>
`Копируем: ${requestTypeName} · ${company} · ${department}`,
copyStepsCount: (count: number): string =>
`Шагов будет скопировано: ${count} ${pluralRu(count, ['шаг', 'шага', 'шагов'])}`,
copyStepsLoading: 'Загрузка шагов исходного маршрута...',
copyStepsEmpty: 'В исходном маршруте нет шагов — копия будет пустой.',
validationCopySame:
'Копия совпадает с исходным маршрутом. Измените компанию, отдел или признак «по проекту» — ' +
'иначе такой маршрут уже существует.',
// Роль в проекте допустима только у маршрута с признаком «по проекту» (проверка
// на бэкенде, TemplateRouteStepService) — предупреждаем ДО копирования, иначе
// копия сорвётся на середине переноса шагов.
copyProjectRoleWarning: (count: number): string =>
`В исходном маршруте ${count} ${pluralRu(count, ['шаг', 'шага', 'шагов'])} с ролью в проекте. ` +
'Без признака «по проекту» такие шаги скопировать нельзя — включите его или удалите эти шаги в исходном маршруте.',
copyStepFailed: (orderNo: number, message: string): string =>
`Не удалось скопировать шаг ${orderNo}: ${message}`,
// Частично скопированный маршрут опаснее, чем несозданный: он бы резолвился
// с недостающими согласующими. Поэтому при ошибке копию удаляем целиком.
copyRolledBack: 'Копия не создана — маршрут удалён, чтобы не остался наполовину скопированным.',
copyRollbackFailed: (routeId: number): string =>
`ВНИМАНИЕ: маршрут #${routeId} создан, но шаги скопированы не полностью, и удалить его автоматически не вышло. Удалите маршрут вручную.`,
deleteTitle: 'Удалить маршрут?',
deleteText: (routeId: number): string =>
`Маршрут #${routeId} и все его шаги будут удалены безвозвратно.`
@@ -193,10 +227,20 @@ export const texts = {
routeSteps: {
breadcrumb: (routeId: number): string => `Шаги маршрута #${routeId}`,
title: (routeId: number): string => `Маршрут #${routeId}`,
subtitle: (requestTypeName: string): string => requestTypeName,
/** Подзаголовок экрана шагов: по нему видно, ЧЕЙ это маршрут (тип заявки · компания · отдел). */
subtitle: (requestTypeName: string, company: string, department: string): string =>
`${requestTypeName} · ${company} · ${department}`,
/** Маршрут без компании/отдела — запасной, применяется когда своего маршрута нет. */
anyCompany: 'любая компания',
anyDepartment: 'любой отдел',
back: 'К маршрутам',
add: 'Добавить шаг',
emptyList: 'Шагов пока нет — добавьте первый.',
moveUp: 'Шаг выше',
moveDown: 'Шаг ниже',
mergeWithPrev: 'Объединить с предыдущим шагом (выполнять параллельно)',
splitFromPrev: 'Отделить от предыдущего шага (выполнять после него)',
reorderFailed: (message: string): string => `Не удалось изменить порядок шагов: ${message}`,
columnOrderNo: '№',
columnExecutor: 'Исполнитель',
columnStatus: 'Статус',
@@ -220,6 +264,32 @@ export const texts = {
executorPlaceholder: 'Выберите сотрудника',
positionLabel: 'Должность',
positionHint: 'Ищется в справочнике api.tnsu.kz по компании инициатора.',
// Подсказка «кого найдёт эта должность» — считается по загруженному справочнику.
positionPreviewLoading: 'Проверяем по справочнику сотрудников...',
positionPreviewFound: (count: number): string =>
count === 1
? 'Сейчас эту должность занимает:'
: `Эту должность занимают ${count} ${pluralRu(count, ['сотрудник', 'сотрудника', 'сотрудников'])} — ` +
'на согласование пойдёт тот, чья компания совпадёт с компанией инициатора:',
// У маршрута задана конкретная компания (не запасной «любая компания») —
// резолвиться будет только сотрудник этой компании, остальные найденные
// (в других компаниях) этому маршруту не подойдут ни при каких условиях.
positionPreviewFoundInCompany: (company: string, count: number): string =>
count === 1
? `Эту должность в компании «${company}» (компания маршрута) занимает:`
: `Эту должность в компании «${company}» (компания маршрута) занимают ${count} ` +
`${pluralRu(count, ['сотрудник', 'сотрудника', 'сотрудников'])}:`,
positionPreviewWrongCompany: (company: string): string =>
`Такая должность есть в справочнике, но не в компании «${company}» — а маршрут применяется именно к её ` +
'инициаторам. При резолве шаг останется без исполнителя.',
positionPreviewOtherCompanies: 'Есть в других компаниях:',
positionPreviewNone:
'Точно такой должности в справочнике нет — на резолве шаг останется без исполнителя. Проверьте написание.',
positionPreviewSimilar: 'Похожие должности в справочнике (нажмите, чтобы подставить точное написание):',
/** Сколько ещё человек занимают ту же должность, кроме показанного. */
positionPreviewMore: (count: number): string =>
`и ещё ${count} ${pluralRu(count, ['человек', 'человека', 'человек'])}`,
positionPreviewNoDictionary: 'Справочник сотрудников недоступен — проверить должность не удалось.',
positionPlaceholder: 'Главный инженер',
projectRoleLabel: 'Роль в проекте',
headTypeLabel: 'Тип руководителя',
@@ -233,7 +303,9 @@ export const texts = {
askOverdueReasonHint: '«Не задано» — берётся значение типа заявки.',
conditionLabel: 'Условие видимости шага',
conditionCheckbox: 'Показывать шаг только при выполнении условия',
conditionDisabledHint: 'Недоступно: у типа заявки не указан список SharePoint (list_name).',
conditionDisabledHint:
'У типа заявки не указан список SharePoint (list_name) — доступно только служебное поле «Инициатор — руководитель».',
conditionFieldIsManagerLabel: 'Должность в заявке — руководящая (IsManager)',
conditionFieldNameLabel: 'Поле списка',
conditionFieldNamePlaceholder: 'Выберите столбец списка',
conditionFieldNameLoading: 'Загрузка столбцов списка...',
@@ -371,7 +443,10 @@ export const texts = {
missingInDictionary: 'Нет в справочнике',
loadingDictionary: 'Загрузка справочника...',
searching: 'Поиск...',
employeeSearchPlaceholder: 'Поиск по ФИО или email...',
// Не перечисляем поля: искать можно и по ФИО, и по email, и по должности
// (см. toEmployeeOption — должность попадает в sublabel, а SearchableSelect
// ищет и по нему). Перечисление всё равно устарело бы при следующем поле.
employeeSearchPlaceholder: 'Поиск...',
employeesEmpty: 'Сотрудники не найдены в справочнике'
}
};