Files
mkd/constants/permissions.ts
2026-02-04 00:17:04 +05:00

231 lines
11 KiB
TypeScript
Executable File
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.
/**
* Конфигурация прав доступа: разделы и подразделы (вкладки/отчёты).
* Ключ доступа: раздел или раздел_подраздел, с опциональным суффиксом уровня:
* - без суффикса или :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 };
}