Files
mkd/constants/permissions.ts

231 lines
11 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
/**
* Конфигурация прав доступа: разделы и подразделы (вкладки/отчёты).
* Ключ доступа: раздел или раздел_подраздел, с опциональным суффиксом уровня:
* - без суффикса или :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 };
}