/** * Конфигурация прав доступа: разделы и подразделы (вкладки/отчёты). * Ключ доступа: раздел или раздел_подраздел, с опциональным суффиксом уровня: * - без суффикса или :edit — полный доступ (редактирование) * - :read — только чтение * - :own — только своё (записи на своё имя / от своего имени, редактирование в рамках своего) */ export type PermissionLevel = 'none' | 'read' | 'edit' | 'own'; export const PERMISSION_LEVEL_LABELS: Record, string> = { read: 'Только чтение', edit: 'Редактирование', own: 'Только своё', }; export const SECTION_LABELS: Record = { dashboard: 'Сводка', objects: 'Участки', requests: 'Заявки', pr: 'PR и NPS', finance: 'Финансы', legal: 'Юр. отдел', development: 'Развитие', hr: 'Кадры', office: 'Офис', admin: 'Панель управления', }; /** Подразделы по разделам: id вкладки/отчёта -> подпись */ export const SECTION_SUBS: Record> = { 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> = { 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): 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 } | 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) || 'edit'; return { section, subId, level }; }