231 lines
11 KiB
TypeScript
Executable File
231 lines
11 KiB
TypeScript
Executable File
/**
|
||
* Конфигурация прав доступа: разделы и подразделы (вкладки/отчёты).
|
||
* Ключ доступа: раздел или раздел_подраздел, с опциональным суффиксом уровня:
|
||
* - без суффикса или :edit — полный доступ (редактирование)
|
||
* - :read — только чтение
|
||
* - :own — только своё (записи на своё имя / от своего имени, редактирование в рамках своего)
|
||
*/
|
||
|
||
export type PermissionLevel = 'none' | 'read' | 'edit' | 'own';
|
||
|
||
export const PERMISSION_LEVEL_LABELS: Record<Exclude<PermissionLevel, 'none'>, string> = {
|
||
read: 'Только чтение',
|
||
edit: 'Редактирование',
|
||
own: 'Только своё',
|
||
};
|
||
|
||
export const SECTION_LABELS: Record<string, string> = {
|
||
dashboard: 'Сводка',
|
||
objects: 'Участки',
|
||
requests: 'Заявки',
|
||
pr: 'PR и NPS',
|
||
finance: 'Финансы',
|
||
legal: 'Юр. отдел',
|
||
development: 'Развитие',
|
||
hr: 'Кадры',
|
||
office: 'Офис',
|
||
admin: 'Панель управления',
|
||
};
|
||
|
||
/** Подразделы по разделам: id вкладки/отчёта -> подпись */
|
||
export const SECTION_SUBS: Record<string, Array<{ id: string; label: string }>> = {
|
||
dashboard: [
|
||
{ id: 'goals', label: 'Стратегические цели' },
|
||
{ id: 'production', label: 'Панель «Производство»' },
|
||
{ id: 'pr', label: 'Панель «PR и клиентский опыт»' },
|
||
{ id: 'finance', label: 'Панель «Финансы»' },
|
||
{ id: 'development', label: 'Панель «Развитие»' },
|
||
{ id: 'legal', label: 'Панель «Юр. отдел»' },
|
||
{ id: 'hr', label: 'Панель «Кадры»' },
|
||
{ id: 'office', label: 'Панель «Офис»' },
|
||
{ id: 'tasks', label: 'Мои задачи' },
|
||
{ id: 'news', label: 'Новости компании' },
|
||
],
|
||
objects: [
|
||
{ id: 'summary', label: 'Обзор участков' },
|
||
{ id: 'buildings', label: 'Дома' },
|
||
{ id: 'staff', label: 'Штат участка' },
|
||
],
|
||
requests: [
|
||
{ id: 'summary', label: 'Сводка' },
|
||
{ id: 'registry', label: 'Реестр заявок' },
|
||
{ id: 'control', label: 'Контроль' },
|
||
{ id: 'quality', label: 'Качество' },
|
||
{ id: 'mappings', label: 'Привязки' },
|
||
{ id: 'outages', label: 'Отключения' },
|
||
],
|
||
pr: [
|
||
{ id: 'summary', label: 'Сводка' },
|
||
{ id: 'smm', label: 'SMM' },
|
||
{ id: 'events', label: 'Мероприятия' },
|
||
{ id: 'feedback', label: 'Обратная связь' },
|
||
{ id: 'reports', label: 'Отчёты' },
|
||
{ id: 'photos', label: 'Фото работ' },
|
||
{ id: 'nps', label: 'NPS' },
|
||
{ id: 'negative', label: 'Негатив' },
|
||
],
|
||
finance: [
|
||
{ id: 'summary', label: 'Сводка' },
|
||
{ id: 'invoices', label: 'Реестр счетов' },
|
||
{ id: 'calendar', label: 'Календарь оплат' },
|
||
{ id: 'reports', label: 'Отчёты' },
|
||
],
|
||
legal: [
|
||
{ id: 'summary', label: 'Сводка' },
|
||
{ id: 'debt', label: 'Взыскание долгов' },
|
||
{ id: 'contracts', label: 'Договоры' },
|
||
{ id: 'courts', label: 'Судебные дела' },
|
||
{ id: 'preTrial', label: 'Досудебная работа' },
|
||
{ id: 'compliance', label: 'Соответствие' },
|
||
],
|
||
development: [
|
||
{ id: 'summary', label: 'Сводка' },
|
||
{ id: 'pipeline', label: 'Реестр объектов' },
|
||
{ id: 'oss', label: 'ОСС' },
|
||
{ id: 'audit', label: 'Техаудит' },
|
||
{ id: 'marketing', label: 'Поле' },
|
||
],
|
||
hr: [
|
||
{ id: 'summary', label: 'Сводка' },
|
||
{ id: 'employees', label: 'Сотрудники' },
|
||
{ id: 'calendar', label: 'Календарь' },
|
||
{ id: 'vacancies', label: 'Вакансии' },
|
||
{ id: 'hiring', label: 'Подбор' },
|
||
{ id: 'safety', label: 'Охрана труда' },
|
||
],
|
||
office: [
|
||
{ id: 'dashboard', label: 'Сводка' },
|
||
{ id: 'supply', label: 'Закупки' },
|
||
{ id: 'docs', label: 'Документы' },
|
||
{ id: 'facility', label: 'Объекты' },
|
||
{ id: 'repair', label: 'Ремонты' },
|
||
{ id: 'knowledge', label: 'База знаний' },
|
||
{ id: 'meetings', label: 'Встречи' },
|
||
{ id: 'news', label: 'Новости' },
|
||
],
|
||
admin: [
|
||
{ id: 'users', label: 'Пользователи' },
|
||
{ id: 'permissions', label: 'Права и шаблоны' },
|
||
{ id: 'integrations', label: 'Интеграции' },
|
||
{ id: 'ai', label: 'ИИ' },
|
||
{ id: 'company', label: 'О компании' },
|
||
{ id: 'positions', label: 'Должности' },
|
||
{ id: 'responsibility-zones', label: 'Зоны ответственности' },
|
||
{ id: 'backups', label: 'Резервные копии' },
|
||
{ id: 'data-import', label: 'Загрузка данных' },
|
||
{ id: 'data-cleanup', label: 'Очистка данных' },
|
||
{ id: 'security', label: 'Безопасность' },
|
||
],
|
||
};
|
||
|
||
export const ALL_SECTION_KEYS = Object.keys(SECTION_LABELS);
|
||
|
||
/**
|
||
* Группы зон ответственности для отображения (опционально).
|
||
* Один сотрудник может быть ответственным за несколько подразделов в группе.
|
||
* Пример: в Кадрах — «ЗП и обучение» (Сотрудники, Календарь, Охрана труда) и «Найм и адаптация» (Вакансии, Подбор).
|
||
*/
|
||
export const RESPONSIBILITY_GROUPS: Record<string, Array<{ name: string; subIds: string[] }>> = {
|
||
hr: [
|
||
{ name: 'ЗП, настрой и обучение', subIds: ['employees', 'calendar', 'safety'] },
|
||
{ name: 'Найм и адаптация', subIds: ['vacancies', 'hiring'] },
|
||
],
|
||
finance: [
|
||
{ name: 'Счета и оплаты', subIds: ['invoices', 'calendar'] },
|
||
{ name: 'Отчётность', subIds: ['reports'] },
|
||
],
|
||
requests: [
|
||
{ name: 'Реестр и контроль', subIds: ['registry', 'control'] },
|
||
{ name: 'Качество и привязки', subIds: ['quality', 'mappings'] },
|
||
],
|
||
legal: [
|
||
{ name: 'Взыскание и суды', subIds: ['debt', 'courts', 'preTrial'] },
|
||
{ name: 'Договоры и соответствие', subIds: ['contracts', 'compliance'] },
|
||
],
|
||
development: [
|
||
{ name: 'Объекты и ОСС', subIds: ['pipeline', 'oss'] },
|
||
{ name: 'Аудит и маркетинг', subIds: ['audit', 'marketing'] },
|
||
],
|
||
pr: [
|
||
{ name: 'SMM и мероприятия', subIds: ['smm', 'events'] },
|
||
{ name: 'Обратная связь и NPS', subIds: ['feedback', 'nps', 'negative'] },
|
||
],
|
||
};
|
||
|
||
/** Проверяет, есть ли у пользователя хотя бы какое-то право на подраздел (любой уровень) */
|
||
function hasAnyPermissionForSub(permissions: string[], section: string, subId: string): boolean {
|
||
const base = `${section}_${subId}`;
|
||
return (
|
||
permissions.includes(base) ||
|
||
permissions.includes(`${base}:read`) ||
|
||
permissions.includes(`${base}:edit`) ||
|
||
permissions.includes(`${base}:own`)
|
||
);
|
||
}
|
||
|
||
/** Проверка: есть ли у пользователя доступ к подразделу (вкладке/отчёту) */
|
||
export function canAccessSub(permissions: string[], section: string, subId?: string): boolean {
|
||
if (!permissions || permissions.length === 0) return true; // по роли — проверка снаружи
|
||
if (permissions.includes('all')) return true;
|
||
if (permissions.includes(section)) return true;
|
||
if (subId != null) return hasAnyPermissionForSub(permissions, section, subId);
|
||
return false;
|
||
}
|
||
|
||
/** Уровень доступа к подразделу: none | read | edit | own */
|
||
export function getPermissionLevel(permissions: string[], section: string, subId: string): PermissionLevel {
|
||
if (!permissions || permissions.length === 0) return 'edit'; // по роли — полный доступ
|
||
if (permissions.includes('all')) return 'edit';
|
||
if (permissions.includes(section)) return 'edit';
|
||
const base = `${section}_${subId}`;
|
||
if (permissions.includes(`${base}:own`)) return 'own';
|
||
if (permissions.includes(`${base}:edit`) || permissions.includes(base)) return 'edit';
|
||
if (permissions.includes(`${base}:read`)) return 'read';
|
||
return 'none';
|
||
}
|
||
|
||
/** Может ли пользователь редактировать (создавать/изменять/удалять) в подразделе */
|
||
export function canEditSub(permissions: string[], section: string, subId: string): boolean {
|
||
const level = getPermissionLevel(permissions, section, subId);
|
||
return level === 'edit' || level === 'own';
|
||
}
|
||
|
||
/** Может ли пользователь хотя бы просматривать подраздел */
|
||
export function canReadSub(permissions: string[], section: string, subId: string): boolean {
|
||
const level = getPermissionLevel(permissions, section, subId);
|
||
return level !== 'none';
|
||
}
|
||
|
||
/** Ограничен ли доступ «только своё» (фильтровать по автору/исполнителю) */
|
||
export function isScopeOwn(permissions: string[], section: string, subId: string): boolean {
|
||
return getPermissionLevel(permissions, section, subId) === 'own';
|
||
}
|
||
|
||
/** Список разрешённых подразделов для раздела (для отображения вкладок) */
|
||
export function allowedSubsForSection(permissions: string[], section: string): string[] | 'all' {
|
||
if (!permissions || permissions.length === 0) return 'all';
|
||
if (permissions.includes('all') || permissions.includes(section)) return 'all';
|
||
const subs = SECTION_SUBS[section];
|
||
if (!subs) return [];
|
||
const allowed: string[] = [];
|
||
for (const { id } of subs) {
|
||
if (hasAnyPermissionForSub(permissions, section, id)) allowed.push(id);
|
||
}
|
||
return allowed;
|
||
}
|
||
|
||
/** Сформировать ключ права для сохранения (раздел_подраздел:уровень) */
|
||
export function permissionKey(section: string, subId: string, level: Exclude<PermissionLevel, 'none'>): string {
|
||
if (level === 'edit') return `${section}_${subId}`;
|
||
return `${section}_${subId}:${level}`;
|
||
}
|
||
|
||
/** Распарсить уровень из ключа права (section_sub или section_sub:read|edit|own) */
|
||
export function parsePermissionLevel(key: string): { section: string; subId: string; level: Exclude<PermissionLevel, 'none'> } | null {
|
||
const match = key.match(/^([a-z]+)_([a-zA-Z0-9]+)(?::(read|edit|own))?$/);
|
||
if (!match) return null;
|
||
const [, section, subId, suffix] = match;
|
||
const level = (suffix as Exclude<PermissionLevel, 'none'>) || 'edit';
|
||
return { section, subId, level };
|
||
}
|