0e6a7064d2
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz> Reviewed-on: http://git.tnsu.kz/gorbenko.pavel/tnsu-approval-route/pulls/29
600 lines
40 KiB
TypeScript
600 lines
40 KiB
TypeScript
// Все пользовательские тексты интерфейса — в одном месте.
|
||
// Так их видно целиком (легко вычитать формулировки) и это задел под локализацию:
|
||
// компонентам не нужно знать, откуда пришла строка.
|
||
//
|
||
// Строки с подстановкой оформлены функциями, чтобы шаблон и данные не разъезжались.
|
||
|
||
import { ComparisonOperator, ConditionsLogic, 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]: 'Руководитель указанного отдела',
|
||
[HeadType.CompanyDirectors]: 'Директора компаний инициатора'
|
||
};
|
||
|
||
export const comparisonOperatorLabels: Record<ComparisonOperator, string> = {
|
||
[ComparisonOperator.None]: '—',
|
||
[ComparisonOperator.GreaterThan]: 'Больше (>)',
|
||
[ComparisonOperator.LessThan]: 'Меньше (<)',
|
||
[ComparisonOperator.Equal]: 'Равно (=)',
|
||
[ComparisonOperator.NotEqual]: 'Не равно (≠)',
|
||
[ComparisonOperator.Contains]: 'Содержит',
|
||
[ComparisonOperator.NotContains]: 'Не содержит'
|
||
};
|
||
|
||
export const comparisonOperatorSymbols: Record<ComparisonOperator, string> = {
|
||
[ComparisonOperator.None]: '',
|
||
[ComparisonOperator.GreaterThan]: '>',
|
||
[ComparisonOperator.LessThan]: '<',
|
||
[ComparisonOperator.Equal]: '=',
|
||
[ComparisonOperator.NotEqual]: '≠',
|
||
[ComparisonOperator.Contains]: 'содержит',
|
||
[ComparisonOperator.NotContains]: 'не содержит'
|
||
};
|
||
|
||
export const conditionsLogicLabels: Record<ConditionsLogic, string> = {
|
||
[ConditionsLogic.And]: 'И (выполняются все условия)',
|
||
[ConditionsLogic.Or]: 'ИЛИ (достаточно одного условия)'
|
||
};
|
||
|
||
/** Как условия шага склеиваются в одну строку в колонке «Условие» таблицы шагов. */
|
||
export const conditionsLogicJoiners: Record<ConditionsLogic, string> = {
|
||
[ConditionsLogic.And]: ' И ',
|
||
[ConditionsLogic.Or]: ' ИЛИ '
|
||
};
|
||
|
||
export const texts = {
|
||
common: {
|
||
home: 'Главная',
|
||
cancel: 'Отмена',
|
||
save: 'Сохранить',
|
||
saving: 'Сохранение...',
|
||
remove: 'Удалить',
|
||
edit: 'Редактировать',
|
||
close: 'Закрыть',
|
||
loading: 'Загрузка...',
|
||
yes: 'Да',
|
||
no: 'Нет',
|
||
/**
|
||
* Третье состояние для настроек с наследованием (SLA, опрос причины
|
||
* просрочки): пусто значит «взять значение уровнем выше», и это НЕ то же
|
||
* самое, что «Нет» или «0» — их выбирают осознанно.
|
||
*/
|
||
inherited: 'Не задано',
|
||
/** Прочерк вместо пустого значения — чтобы ячейка таблицы не выглядела сломанной. */
|
||
empty: '—'
|
||
},
|
||
|
||
nav: {
|
||
title: 'Панель настройки маршрутов',
|
||
dashboard: 'Дашборд',
|
||
requestTypes: 'Типы заявок',
|
||
requestTypeAccess: 'Доступ к заявкам',
|
||
routes: 'Маршруты',
|
||
resolve: 'Проверка маршрута',
|
||
routesExport: 'Выгрузка маршрутов',
|
||
manageRequests: 'Редактирование заявок',
|
||
manageByUser: 'Заявки по участнику'
|
||
},
|
||
|
||
app: {
|
||
connecting: 'Подключение к API...',
|
||
connectFailed: (message: string): string => `Не удалось подключиться к API: ${message}`,
|
||
connectHint: 'Проверьте свойства веб-части (Api Base Url, режим авторизации) в панели настроек.'
|
||
},
|
||
|
||
dashboard: {
|
||
requestTypesStat: 'Типов заявок',
|
||
routesStat: 'Маршрутов согласования',
|
||
listPartitionsStat: 'Партиций списков SharePoint',
|
||
sections: 'Разделы',
|
||
requestTypesCard: {
|
||
title: 'Типы заявок',
|
||
description: 'Справочник типов заявок и их страниц'
|
||
},
|
||
routesCard: {
|
||
title: 'Маршруты согласования',
|
||
description: 'Маршруты, шаги и исполнители'
|
||
},
|
||
resolveCard: {
|
||
title: 'Проверка маршрута',
|
||
description: 'Как разрешится маршрут для конкретного пользователя'
|
||
},
|
||
manageRequestsCard: {
|
||
title: 'Редактирование заявок',
|
||
description: 'Правка маршрута согласования конкретной заявки'
|
||
},
|
||
manageByUserCard: {
|
||
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}» будет удалён безвозвратно. Связанные маршруты могут перестать разрешаться.`
|
||
},
|
||
|
||
requestTypeAccess: {
|
||
breadcrumb: 'Доступ к заявкам',
|
||
subtitle: 'Кто видит все заявки типа, а не только свои',
|
||
title: 'Доступ к заявкам',
|
||
add: 'Выдать доступ',
|
||
emptyList: 'Пока никому не выдан доступ ни к одному типу заявок.',
|
||
emptyFiltered: 'Ничего не найдено по заданным фильтрам.',
|
||
filterRequestTypeLabel: 'Фильтр по типу заявки',
|
||
filterRequestTypeAll: 'Все типы заявок',
|
||
filterEmailLabel: 'Фильтр по сотруднику',
|
||
filterEmailAll: 'Все сотрудники',
|
||
filterEmailPlaceholder: 'Найдите по ФИО или email',
|
||
columnRequestType: 'Тип заявки',
|
||
columnEmail: 'Сотрудник',
|
||
columnSharePointLists: 'Списки SharePoint (выдать доступ вручную)',
|
||
createTitle: 'Выдать доступ',
|
||
requestTypeLabel: 'Тип заявки',
|
||
requestTypePlaceholder: 'Выберите тип заявки',
|
||
emailLabel: 'Сотрудники',
|
||
emailPlaceholder: 'Добавьте сотрудника — найдите по ФИО или email',
|
||
removeEmail: 'Убрать из списка',
|
||
alreadyGranted: 'доступ уже есть',
|
||
alreadyChosen: 'уже в списке',
|
||
noEmailsSelected: 'Пока никого не выбрано.',
|
||
createFailed: (failures: string[]): string =>
|
||
`Не удалось выдать доступ (остались в списке): ${failures.join('; ')}`,
|
||
validationRequestType: 'Выберите тип заявки.',
|
||
validationEmail: 'Выберите хотя бы одного сотрудника.',
|
||
deleteTitle: 'Отозвать доступ?',
|
||
deleteText: (email: string, requestTypeName: string): string =>
|
||
`${email} больше не будет видеть все заявки типа «${requestTypeName}».`
|
||
},
|
||
|
||
routesExport: {
|
||
breadcrumb: 'Выгрузка маршрутов',
|
||
subtitle: 'Маршруты согласования в Excel',
|
||
title: 'Выгрузка маршрутов',
|
||
hint: 'Отметьте типы заявок — каждый попадёт на отдельный лист файла (согласующие и исполнители отдельными таблицами).',
|
||
selectAll: 'Выбрать все',
|
||
export: 'Выгрузить',
|
||
exporting: 'Формирование файла...',
|
||
emptyTypes: 'Типов заявок пока нет.',
|
||
selected: (count: number): string => `Выбрано: ${count}`,
|
||
fileName: 'Маршруты согласования.xlsx',
|
||
failed: (reason: string): string => `Не удалось выгрузить маршруты: ${reason}`
|
||
},
|
||
|
||
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: '«Не задано» — берётся значение типа заявки.',
|
||
autoApproveLabel: 'Автосогласование',
|
||
autoApproveCheckbox: 'Согласовать шаг автоматически, когда истечёт SLA',
|
||
autoApproveHint:
|
||
'Если согласующий не примет решение в срок, шаг будет согласован без него и заявка пойдёт дальше. Проверка идёт раз в час, поэтому согласование произойдёт в течение часа после истечения срока.',
|
||
validationAutoApproveWithoutSla:
|
||
'Автосогласование считается от SLA — задайте SLA шага или SLA типа заявки.',
|
||
conditionLabel: 'Условие видимости шага',
|
||
conditionCheckbox: 'Показывать шаг только при выполнении условия',
|
||
conditionDisabledHint:
|
||
'У типа заявки не указан список SharePoint (list_name) — доступно только служебное поле «Инициатор — руководитель».',
|
||
conditionFieldIsManagerLabel: 'Должность в заявке — руководящая (IsManager)',
|
||
conditionFieldNameLabel: 'Поле списка',
|
||
conditionFieldNamePlaceholder: 'Выберите столбец списка',
|
||
conditionFieldNameLoading: 'Загрузка столбцов списка...',
|
||
conditionFieldNameEmpty: 'В списке нет доступных столбцов',
|
||
conditionFieldNameError: (message: string): string => `Не удалось получить столбцы списка: ${message}`,
|
||
conditionOperationLabel: 'Оператор сравнения',
|
||
conditionValueLabel: 'Значение',
|
||
conditionValuePlaceholder: 'Например, 1000',
|
||
conditionLogicLabel: 'Логика объединения условий',
|
||
conditionAdd: 'Добавить условие',
|
||
conditionRemove: 'Удалить условие',
|
||
validationOrderNo: 'Порядковый номер шага обязателен.',
|
||
validationExecutor: 'Выберите сотрудника-исполнителя.',
|
||
validationPosition: 'Укажите должность.',
|
||
validationCondition: 'В каждом условии заполните поле списка, оператор и значение.',
|
||
validationConditionNumber: (fieldName: string): string =>
|
||
`Поле «${fieldName}» числовое — значение условия должно быть числом.`,
|
||
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: 'Статус заявки',
|
||
listLinkLabel: (listTitle: string): string => `Список: ${listTitle}`,
|
||
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: 'Не удалось сохранить'
|
||
},
|
||
|
||
manageByUser: {
|
||
breadcrumb: 'Заявки по участнику',
|
||
subtitle: 'Массовая замена согласующего/исполнителя',
|
||
title: 'Заявки по участнику',
|
||
employeeLabel: 'Сотрудник',
|
||
employeePlaceholder: 'Выберите сотрудника',
|
||
employeeHint: 'Покажет все его активные заявки (по всем типам и партициям) и все шаблонные шаги, где он назначен лично.',
|
||
replaceEverywhere: 'Заменить везде',
|
||
liveTitle: 'Активные заявки',
|
||
liveEmpty: 'Активных заявок нет.',
|
||
liveLoadError: 'Не удалось загрузить заявки',
|
||
columnRequestType: 'Тип заявки',
|
||
columnRequest: 'Заявка',
|
||
columnPosition: 'Должность',
|
||
columnStatus: 'Статус',
|
||
columnPhase: 'Фаза',
|
||
templatesTitle: 'Шаблоны маршрутов',
|
||
templatesEmpty: 'Шагов с этим исполнителем нет.',
|
||
templatesLoadError: 'Не удалось загрузить шаги маршрутов',
|
||
columnRoute: 'Маршрут',
|
||
columnStepStatus: 'Статус шага',
|
||
columnCondition: 'Условие',
|
||
replaceOne: 'Заменить',
|
||
selectAll: 'Выбрать все',
|
||
deselectAll: 'Снять выделение',
|
||
replaceSelected: (count: number): string => `Заменить выбранные (${count})`,
|
||
targetLabel: 'Заменить на',
|
||
targetPlaceholder: 'Выберите, кем заменить',
|
||
targetHint: 'Кого поставим вместо выбранного сотрудника. Пока не заполнено, кнопки замены выключены.',
|
||
targetClear: 'Не выбран',
|
||
swap: 'Поменять местами',
|
||
progressLabel: 'Заменяем…',
|
||
liveResult: (ok: number, fail: number, deleted: number, skipped: number): string => {
|
||
const parts = [`заменено ${ok}`];
|
||
if (deleted > 0) parts.push(`удалено дублей ${deleted}`);
|
||
if (skipped > 0) parts.push(`пропущено ${skipped}`);
|
||
if (fail > 0) parts.push(`не удалось ${fail}`);
|
||
return `Заявки: ${parts.join(', ')}.`;
|
||
},
|
||
templatesResult: (ok: number, fail: number, deleted: number, skipped: number): string => {
|
||
const parts = [`заменено ${ok}`];
|
||
if (deleted > 0) parts.push(`удалено дублей ${deleted}`);
|
||
if (skipped > 0) parts.push(`пропущено ${skipped}`);
|
||
if (fail > 0) parts.push(`не удалось ${fail}`);
|
||
return `Шаблоны: ${parts.join(', ')}.`;
|
||
},
|
||
duplicateModalTitle: 'Уже назначен на этом же шаге',
|
||
duplicateLiveMessage: (requestLabel: string, oldPosition: string, newEmail: string): string =>
|
||
`В заявке ${requestLabel} сотрудник ${newEmail} уже согласует/исполняет этот же шаг. ` +
|
||
`Заменить строку («${oldPosition || 'без должности'}») тоже на него нельзя — появится дубль. Удалить эту строку?`,
|
||
duplicateTemplateMessage: (routeLabel: string, orderNo: number, newEmail: string): string =>
|
||
`В маршруте «${routeLabel}» сотрудник ${newEmail} уже назначен на шаг с такими же параметрами. ` +
|
||
`Заменить шаг №${orderNo} на него нельзя — сервер вернул конфликт. Удалить этот шаг?`,
|
||
duplicateDelete: 'Да, удалить',
|
||
duplicateDeleteAll: 'Да для всех',
|
||
duplicateSkip: 'Нет, пропустить',
|
||
duplicateSkipAll: 'Нет для всех'
|
||
},
|
||
|
||
// Значения по умолчанию для переиспользуемых контролов выбора.
|
||
select: {
|
||
placeholder: 'Выберите значение',
|
||
searchPlaceholder: 'Поиск...',
|
||
nothingFound: 'Ничего не найдено',
|
||
notSelected: 'Не выбрано',
|
||
missingInDictionary: 'Нет в справочнике',
|
||
loadingDictionary: 'Загрузка справочника...',
|
||
searching: 'Поиск...',
|
||
// Не перечисляем поля: искать можно и по ФИО, и по email, и по должности
|
||
// (см. toEmployeeOption — должность попадает в sublabel, а SearchableSelect
|
||
// ищет и по нему). Перечисление всё равно устарело бы при следующем поле.
|
||
employeeSearchPlaceholder: 'Поиск...',
|
||
employeesEmpty: 'Сотрудники не найдены в справочнике'
|
||
}
|
||
};
|