Files
tnsu-approval-route/frontend/src/webparts/approvalAdmin/texts.ts
T
2026-08-14 14:25:15 +05:00

453 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Все пользовательские тексты интерфейса — в одном месте.
// Так их видно целиком (легко вычитать формулировки) и это задел под локализацию:
// компонентам не нужно знать, откуда пришла строка.
//
// Строки с подстановкой оформлены функциями, чтобы шаблон и данные не разъезжались.
import { ComparisonOperator, HeadType, ProjectRole, RouteStepStatus } from './models';
import { pluralRu } from './utils/plural';
// ---- Подписи enum'ов ----
// Record<enum, string> гарантирует, что при добавлении значения в enum
// компилятор потребует и подпись к нему.
export const routeStepStatusLabels: Record<RouteStepStatus, string> = {
[RouteStepStatus.None]: 'Не задан',
[RouteStepStatus.Approval]: 'Согласование',
[RouteStepStatus.Execution]: 'Исполнение'
};
export const projectRoleLabels: Record<ProjectRole, string> = {
[ProjectRole.None]: '—',
[ProjectRole.ProjectManager]: 'Руководитель проекта',
[ProjectRole.Planner]: 'Планировщик',
[ProjectRole.ManagerHSE]: 'Менеджер HSE',
[ProjectRole.Security]: 'Безопасность',
[ProjectRole.Keeper]: 'Заведующий склада'
};
export const headTypeLabels: Record<HeadType, string> = {
[HeadType.None]: '—',
[HeadType.Chief]: 'Непосредственный руководитель',
[HeadType.DirectionHead]: 'Руководитель направления',
[HeadType.DirectorTansu]: 'Директор Tansu',
[HeadType.ManagingDirectorTansu]: 'Управляющий директор Tansu',
[HeadType.DepartmentHead]: 'Руководитель указанного отдела'
};
export const comparisonOperatorLabels: Record<ComparisonOperator, string> = {
[ComparisonOperator.None]: '—',
[ComparisonOperator.GreaterThan]: 'Больше (>)',
[ComparisonOperator.LessThan]: 'Меньше (<)',
[ComparisonOperator.Equal]: 'Равно (=)'
};
export const comparisonOperatorSymbols: Record<ComparisonOperator, string> = {
[ComparisonOperator.None]: '',
[ComparisonOperator.GreaterThan]: '>',
[ComparisonOperator.LessThan]: '<',
[ComparisonOperator.Equal]: '='
};
export const texts = {
common: {
home: 'Главная',
cancel: 'Отмена',
save: 'Сохранить',
saving: 'Сохранение...',
remove: 'Удалить',
edit: 'Редактировать',
close: 'Закрыть',
loading: 'Загрузка...',
yes: 'Да',
no: 'Нет',
/**
* Третье состояние для настроек с наследованием (SLA, опрос причины
* просрочки): пусто значит «взять значение уровнем выше», и это НЕ то же
* самое, что «Нет» или «0» — их выбирают осознанно.
*/
inherited: 'Не задано',
/** Прочерк вместо пустого значения — чтобы ячейка таблицы не выглядела сломанной. */
empty: '—'
},
nav: {
title: 'Панель настройки маршрутов',
dashboard: 'Дашборд',
requestTypes: 'Типы заявок',
routes: 'Маршруты',
resolve: 'Проверка маршрута',
manageRequests: 'Редактирование заявок'
},
app: {
connecting: 'Подключение к API...',
connectFailed: (message: string): string => `Не удалось подключиться к API: ${message}`,
connectHint: 'Проверьте свойства веб-части (Api Base Url, режим авторизации) в панели настроек.'
},
dashboard: {
requestTypesStat: 'Типов заявок',
routesStat: 'Маршрутов согласования',
sections: 'Разделы',
requestTypesCard: {
title: 'Типы заявок',
description: 'Справочник типов заявок и их страниц'
},
routesCard: {
title: 'Маршруты согласования',
description: 'Маршруты, шаги и исполнители'
},
resolveCard: {
title: 'Проверка маршрута',
description: 'Как разрешится маршрут для конкретного пользователя'
}
},
requestTypes: {
breadcrumb: 'Типы заявок',
subtitle: 'Справочник',
title: 'Типы заявок',
add: 'Добавить тип',
emptyList: 'Типов заявок пока нет.',
columnName: 'Название',
columnPageUrl: 'Url страницы',
columnSection: 'Раздел',
columnListId: 'Список SharePoint',
columnSiteUrl: 'Сайт SharePoint',
columnSla: 'SLA',
createTitle: 'Новый тип заявки',
editTitle: 'Редактировать тип заявки',
nameLabel: 'Название',
namePlaceholder: 'Например, Заявка на командировку',
pageUrlLabel: 'Url страницы',
pageUrlHint: 'Страница формы заявки на портале.',
pageUrlPlaceholder: '/pages/business-trip',
sectionLabel: 'Раздел',
sectionPlaceholder: 'Например, Кадры',
siteUrlLabel: 'Сайт SharePoint',
siteUrlHint: 'Сайт, где живёт список — например, https://tnsukz.sharepoint.com/sites/hrm.',
siteUrlPlaceholder: 'https://tnsukz.sharepoint.com/sites/hrm',
siteUrlSearchPlaceholder: 'Поиск сайта по названию...',
listIdLabel: 'Список SharePoint',
listIdHint: 'Источник полей (field_name) для условий в шагах маршрута.',
listIdPlaceholder: 'Сначала укажите сайт SharePoint',
listIdLoading: 'Загрузка списков сайта...',
listIdEmpty: 'На этом сайте нет доступных списков',
listIdError: (message: string): string => `Не удалось получить списки сайта: ${message}`,
slaHoursLabel: 'SLA, часов',
slaHoursHint:
'Через сколько часов простоя шага напоминать. Пусто — напоминания по этому типу заявок не работают. ' +
'Отсчёт идёт от момента, когда шаг стал активным.',
slaHoursPlaceholder: 'Например, 24',
askOverdueReasonLabel: 'Спрашивать причину просрочки',
askOverdueReasonHint: 'Значение по умолчанию для всех шагов этого типа заявок.',
validationSlaHours: 'SLA укажите целым числом часов (0 и больше) или оставьте поле пустым.',
validationRequired: 'Заполните имя, Url страницы и раздел.',
validationListReference: 'Сайт SharePoint и список должны быть заполнены оба вместе или не заполнены вовсе.',
deleteTitle: 'Удалить тип заявки?',
deleteText: (name: string): string =>
`Тип «${name}» будет удалён безвозвратно. Связанные маршруты могут перестать разрешаться.`
},
routes: {
breadcrumb: 'Маршруты согласования',
subtitle: 'Настройка согласований',
title: 'Маршруты согласования',
add: 'Добавить маршрут',
filterLabel: 'Фильтр по типу заявки',
filterAll: 'Все типы заявок',
dictionaryFailed: (message: string): string => `Справочник компаний и отделов недоступен: ${message}`,
emptyList: 'Маршрутов пока нет.',
columnId: '№ маршрута',
columnRequestType: 'Тип заявки',
columnCompany: 'Компания',
columnDepartment: 'Отдел',
columnByProject: 'По проекту',
columnControl: 'Контроль',
controlRequired: 'Требуется',
openSteps: 'Шаги',
createTitle: 'Новый маршрут',
editTitle: (routeId: number): string => `Маршрут #${routeId}`,
requestTypeLabel: 'Тип заявки',
requestTypePlaceholder: 'Выберите тип заявки',
companyLabel: 'Компания',
companyHint: 'Необязательно — пусто означает запасной маршрут, если для компании инициатора своего маршрута нет.',
companyPlaceholder: 'Без компании (запасной маршрут)',
companySearchPlaceholder: 'Поиск по названию компании...',
companyClear: 'Без компании (запасной маршрут)',
departmentLabel: 'Отдел',
departmentHint: 'Необязательно — пусто означает маршрут «по умолчанию» для всей компании.',
departmentHintNoCompany: 'Сначала выберите компанию.',
departmentPlaceholder: 'Без отдела (маршрут по умолчанию)',
departmentSearchPlaceholder: 'Поиск по названию отдела...',
departmentEmpty: 'У выбранной компании нет отделов в справочнике',
byProjectLabel: 'По проекту',
byProjectCheckbox: 'Маршрут учитывает конкретный проект',
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} и все его шаги будут удалены безвозвратно.`
},
routeSteps: {
breadcrumb: (routeId: number): string => `Шаги маршрута #${routeId}`,
title: (routeId: number): string => `Маршрут #${routeId}`,
/** Подзаголовок экрана шагов: по нему видно, ЧЕЙ это маршрут (тип заявки · компания · отдел). */
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: 'Статус',
columnReassign: 'Переназначение',
columnCondition: 'Условие',
reassignAllowed: 'Разрешено',
executorRole: (roleLabel: string): string => `Роль: ${roleLabel}`,
executorHead: (headLabel: string): string => `Руководитель: ${headLabel}`,
createTitle: 'Новый шаг маршрута',
editTitle: (orderNo: number): string => `Шаг #${orderNo}`,
orderNoLabel: 'Порядковый номер',
statusLabel: 'Статус шага',
kindLabel: 'Кто исполнитель',
kindHint: 'Должно быть заполнено ровно одно из полей.',
kindEmail: 'Конкретный сотрудник',
kindPosition: 'По должности в компании инициатора',
kindProjectRole: 'По роли в проекте',
kindProjectRoleDisabled: 'Недоступно: маршрут не учитывает проект (по проекту = нет).',
kindHeadType: 'По типу руководителя',
executorLabel: 'Сотрудник',
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: 'Тип руководителя',
reassignLabel: 'Переназначение',
reassignCheckbox: 'Разрешить переназначать другим согласующим',
slaHoursLabel: 'SLA шага, часов',
slaHoursHint:
'Пусто — берётся SLA типа заявки. Заданное здесь значение его перекрывает; 0 означает «напоминать сразу».',
slaHoursPlaceholder: 'Как у типа заявки',
askOverdueReasonLabel: 'Спрашивать причину просрочки',
askOverdueReasonHint: '«Не задано» — берётся значение типа заявки.',
conditionLabel: 'Условие видимости шага',
conditionCheckbox: 'Показывать шаг только при выполнении условия',
conditionDisabledHint:
'У типа заявки не указан список SharePoint (list_name) — доступно только служебное поле «Инициатор — руководитель».',
conditionFieldIsManagerLabel: 'Должность в заявке — руководящая (IsManager)',
conditionFieldNameLabel: 'Поле списка',
conditionFieldNamePlaceholder: 'Выберите столбец списка',
conditionFieldNameLoading: 'Загрузка столбцов списка...',
conditionFieldNameEmpty: 'В списке нет доступных столбцов',
conditionFieldNameError: (message: string): string => `Не удалось получить столбцы списка: ${message}`,
conditionOperationLabel: 'Оператор сравнения',
conditionValueLabel: 'Значение',
conditionValuePlaceholder: 'Например, 1000',
validationOrderNo: 'Порядковый номер шага обязателен.',
validationExecutor: 'Выберите сотрудника-исполнителя.',
validationPosition: 'Укажите должность.',
validationCondition: 'Заполните поле списка, оператор и значение условия.',
validationConditionNumber: 'Выбранное поле числовое — значение условия должно быть числом.',
validationSlaHours: 'SLA укажите целым числом часов (0 и больше) или оставьте поле пустым.',
deleteTitle: 'Удалить шаг?',
deleteText: (orderNo: number): string => `Шаг #${orderNo} будет удалён безвозвратно.`
},
resolve: {
breadcrumb: 'Проверка маршрута',
subtitle: 'Диагностика',
title: 'Проверка разрешения маршрута',
requestTypeLabel: 'Тип заявки',
requestTypePlaceholder: 'Выберите тип заявки',
userEmailLabel: 'Инициатор',
userEmailHint: (currentUserEmail: string): string => `Пусто = текущий пользователь (${currentUserEmail}).`,
userEmailHintUnknown: 'Пусто = текущий пользователь из токена.',
userEmailPlaceholder: 'Выберите сотрудника',
userEmailClear: 'Текущий пользователь',
projectLabel: 'Проект',
projectHint: 'Заполните, если маршрут зависит от проекта (ByProject = true).',
projectPlaceholder: 'Не выбран',
projectLoadingPlaceholder: 'Справочник загружается...',
projectSearchPlaceholder: 'Поиск проекта по названию...',
projectClear: 'Без проекта',
projectDictionaryLoading: 'Справочник проектов ещё загружается.',
departmentLabel: 'Отдел (для шага «Руководитель указанного отдела»)',
departmentHint: 'Заполните, если в маршруте есть шаг с типом руководителя «Руководитель указанного отдела».',
departmentPlaceholder: 'Не выбран',
departmentSearchPlaceholder: 'Поиск по названию отдела...',
departmentClear: 'Без отдела',
departmentEmpty: 'Отделы не найдены',
statusLabel: 'Статус',
conditionsLabel: 'Условия (значения полей заявки)',
conditionsDisabledHint: 'Недоступно: у выбранного типа заявки не указан список SharePoint (list_name).',
conditionFieldPlaceholder: 'Выберите столбец списка',
conditionFieldLoading: 'Загрузка столбцов списка...',
conditionValuePlaceholder: 'Значение для проверки',
conditionAdd: 'Добавить условие',
run: 'Проверить',
running: 'Проверка...',
validationRequestType: 'Выберите тип заявки.',
validationConditionField: 'В каждом условии заполните и поле, и значение.',
summary: (routeId: number, requiresControl: boolean): string =>
`Маршрут #${routeId}${requiresControl ? ' · требуется контроль исполнения' : ''}`,
unresolved: (count: number): string =>
`Исполнитель не определён: ${count} ${pluralRu(count, ['шаг', 'шага', 'шагов'])} — причина указана в строке.`,
positionMissing: 'Должность не указана в карточке сотрудника.',
columnOrderNo: '№',
columnExecutor: 'Исполнитель',
columnPosition: 'Должность',
columnReassign: 'Переназначение',
reassignAllowed: 'Разрешено',
emptySteps: 'Маршрут разрешился без шагов для этого статуса.'
},
manageRequests: {
breadcrumb: 'Редактирование заявок',
subtitle: 'Правка маршрута согласования',
title: 'Редактирование заявок',
requestTypeLabel: 'Тип заявки',
requestTypePlaceholder: 'Выберите тип заявки',
requestTypeEmpty: 'Нет типов заявок с привязанным списком SharePoint.',
requestLabel: 'Заявка',
requestPlaceholder: 'Выберите заявку',
requestSearchPlaceholder: 'Поиск по названию...',
requestDisabledHint: 'Сначала выберите тип заявки.',
noApprovalList: 'У этого типа заявки список согласования не найден (Title списка данных не оканчивается на «— данные»).',
requestStatusLabel: 'Статус заявки',
fullyClosedNotice: 'Заявка полностью закрыта — маршрут доступен только для просмотра.',
approvalPhaseTitle: 'Согласование',
executionPhaseTitle: 'Исполнение',
columnOrder: 'Шаг',
columnApprover: 'Согласующий',
columnPosition: 'Должность',
columnStatus: 'Статус',
columnDate: 'Дата',
emptyPhase: 'Шагов нет.',
emptyMatrix: 'У этой заявки маршрут ещё не записан.',
reassign: 'Переназначить',
remove: 'Удалить шаг',
notify: 'Отправить уведомление',
notifyNotActive: 'Уведомить можно только текущий активный шаг (не «Ждёт очереди»)',
notifySuccess: 'Уведомление отправлено',
notifyError: 'Не удалось отправить уведомление',
addApproverPlaceholder: 'Добавить согласующего: начните вводить ФИО…',
addExecutorPlaceholder: 'Добавить исполнителя: начните вводить ФИО…',
lockedHint: 'Решение уже принято — недоступно для правки.',
ghostHint: 'Сохраняется…',
phaseClosedHint: 'Эта фаза уже завершена — добавить в неё новый шаг нельзя.',
requestRejectedHint: 'Заявка отклонена — добавить исполнителя нельзя.',
moveUp: 'Шаг выше',
moveDown: 'Шаг ниже',
mergeWithPrev: 'Объединить с предыдущим шагом (выполнять параллельно)',
splitFromPrev: 'Отделить от предыдущего шага (выполнять после него)',
moveBlockedByDecision: 'Нельзя переставить — соседний шаг уже решён.',
reassignTitle: 'Переназначить шаг',
columnSla: 'SLA',
slaInherited: 'от типа заявки',
slaHoursLabel: 'SLA, часов',
slaHoursHint:
'Через сколько часов простоя напоминать по этому шагу. Пусто — берётся SLA типа заявки; 0 — напоминать сразу.',
slaHoursPlaceholder: 'Как у типа заявки',
askOverdueReasonLabel: 'Спрашивать причину просрочки',
askOverdueReasonHint: '«Не задано» — берётся значение типа заявки.',
validationSlaHours: 'SLA укажите целым числом часов (0 и больше) или оставьте поле пустым.',
approverLabel: 'Сотрудник',
approverPlaceholder: 'Выберите сотрудника',
validationApprover: 'Выберите сотрудника.',
validationDuplicateApprover: 'Этот сотрудник уже стоит в этой фазе на другом шаге.',
deleteTitle: 'Удалить шаг?',
deleteText: (position: string): string => `Шаг «${position || 'без должности'}» будет удалён безвозвратно.`,
orderFloorHint: (order: number): string =>
`Шаг встанет на позицию ${order} — раньше уже принятых решений в этой фазе поставить нельзя.`,
loadError: 'Не удалось загрузить заявку',
saveError: 'Не удалось сохранить'
},
// Значения по умолчанию для переиспользуемых контролов выбора.
select: {
placeholder: 'Выберите значение',
searchPlaceholder: 'Поиск...',
nothingFound: 'Ничего не найдено',
notSelected: 'Не выбрано',
missingInDictionary: 'Нет в справочнике',
loadingDictionary: 'Загрузка справочника...',
searching: 'Поиск...',
// Не перечисляем поля: искать можно и по ФИО, и по email, и по должности
// (см. toEmployeeOption — должность попадает в sublabel, а SearchableSelect
// ищет и по нему). Перечисление всё равно устарело бы при следующем поле.
employeeSearchPlaceholder: 'Поиск...',
employeesEmpty: 'Сотрудники не найдены в справочнике'
}
};