import React, { useEffect, useState } from 'react'; import { backendApi, PermissionTemplateRow, PortalUserRow } from '../../services/apiClient'; import { UserRole } from '../../types'; import { Loader2, Shield, Plus, Pencil, Trash2, CheckSquare, Square } from 'lucide-react'; import { ROLE_NAMES } from '../../constants/roleAccess'; import { SECTION_LABELS, SECTION_SUBS, ALL_SECTION_KEYS, getPermissionLevel, permissionKey, PERMISSION_LEVEL_LABELS, type PermissionLevel, } from '../../constants/permissions'; /** Текущий уровень доступа подраздела по массиву прав (для формы) */ function getSubLevelFromPermissions(permissions: string[], section: string, subId: string): PermissionLevel { 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'; } /** Краткое описание прав для отображения в таблице */ function formatPermissionsShort(permissions: string[]): string { if (!permissions.length) return '—'; if (permissions.includes('all')) return 'Все разделы'; const parts: string[] = []; for (const section of ALL_SECTION_KEYS) { if (permissions.includes(section)) { parts.push(`${SECTION_LABELS[section] || section} (все)`); } else { const subs = SECTION_SUBS[section]; if (subs) { const withLevel = subs .map((s) => { const lvl = getPermissionLevel(permissions, section, s.id); if (lvl === 'none') return null; const label = PERMISSION_LEVEL_LABELS[lvl]; return `${s.label} — ${label}`; }) .filter(Boolean); if (withLevel.length) parts.push(`${SECTION_LABELS[section]} (${withLevel.join('; ')})`); } } } return parts.join('; ') || '—'; } export const PermissionsSection: React.FC = () => { const [templates, setTemplates] = useState([]); const [portalUsers, setPortalUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); const [formName, setFormName] = useState(''); const [formDescription, setFormDescription] = useState(''); const [formPermissions, setFormPermissions] = useState([]); const [formScope, setFormScope] = useState<'all' | 'own_district'>('all'); const [formForPosition, setFormForPosition] = useState(''); const [formSuggestedRole, setFormSuggestedRole] = useState(''); const [saving, setSaving] = useState(false); const [applyToUserId, setApplyToUserId] = useState(null); const load = async () => { setLoading(true); setError(null); try { const [t, u] = await Promise.all([ backendApi.getPermissionTemplates(), backendApi.getPortalUsers(), ]); setTemplates(t); setPortalUsers(u); } catch (e: any) { setError(e?.message || 'Ошибка загрузки'); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const toggleSection = (section: string) => { setFormPermissions((prev) => { const hasSection = prev.includes(section); const subs = SECTION_SUBS[section]; const subKeyPrefixes = subs ? subs.flatMap((s) => { const base = `${section}_${s.id}`; return [base, `${base}:read`, `${base}:edit`, `${base}:own`]; }) : []; if (hasSection) { return prev.filter((p) => p !== section && !subKeyPrefixes.some((prefix) => p === prefix)); } else { const filtered = prev.filter((p) => p !== section && !subKeyPrefixes.some((prefix) => p === prefix)); return filtered.concat([section]); } }); }; const setSubLevel = (section: string, subId: string, level: PermissionLevel) => { setFormPermissions((prev) => { const base = `${section}_${subId}`; const toRemove = prev.filter( (p) => p === base || p === `${base}:read` || p === `${base}:edit` || p === `${base}:own` ); const next = prev.filter((p) => !toRemove.includes(p)); if (level !== 'none') { next.push(permissionKey(section, subId, level)); } return next.filter((p) => p !== section); }); }; const hasSection = (section: string) => formPermissions.includes(section); const getSubLevel = (section: string, subId: string) => getSubLevelFromPermissions(formPermissions, section, subId); const hasSub = (section: string, subId: string) => getSubLevel(section, subId) !== 'none'; const handleCreate = () => { setEditingId(null); setFormName(''); setFormDescription(''); setFormPermissions([]); setFormScope('all'); setFormForPosition(''); setFormSuggestedRole(''); setShowForm(true); }; const handleEdit = (t: PermissionTemplateRow) => { setEditingId(t.id); setFormName(t.name); setFormDescription(t.description || ''); setFormPermissions(t.permissions || []); setFormScope(t.scope === 'own_district' ? 'own_district' : 'all'); setFormForPosition(t.forPosition || ''); setFormSuggestedRole(t.suggestedRole || ''); setShowForm(true); }; const handleSave = async () => { if (!formName.trim()) { alert('Укажите название шаблона'); return; } setSaving(true); try { if (editingId != null) { await backendApi.updatePermissionTemplate(editingId, { name: formName.trim(), description: formDescription.trim() || undefined, permissions: formPermissions, scope: formScope, forPosition: formForPosition.trim() || null, suggestedRole: formSuggestedRole || null, }); } else { await backendApi.createPermissionTemplate({ name: formName.trim(), description: formDescription.trim() || undefined, permissions: formPermissions, scope: formScope, forPosition: formForPosition.trim() || null, suggestedRole: formSuggestedRole || null, }); } setShowForm(false); await load(); } catch (e: any) { alert(e?.message || 'Ошибка сохранения'); } finally { setSaving(false); } }; const handleDelete = async (id: number) => { if (!confirm('Удалить шаблон прав?')) return; setSaving(true); try { await backendApi.deletePermissionTemplate(id); await load(); } catch (e: any) { alert(e?.message || 'Ошибка удаления'); } finally { setSaving(false); } }; const handleApplyToUser = async (templateId: number, portalUserId: number) => { const template = templates.find((t) => t.id === templateId); if (!template) return; setSaving(true); try { await backendApi.updatePortalUser(portalUserId, { permissions: template.permissions.length > 0 ? template.permissions : null, scope: template.scope || 'all', role: template.suggestedRole || undefined, }); setApplyToUserId(null); await load(); } catch (e: any) { alert(e?.message || 'Ошибка применения'); } finally { setSaving(false); } }; if (loading) { return (
); } return (

Шаблоны прав

Сохраните набор прав как шаблон и применяйте его к пользователям или настраивайте права вручную в разделе «Пользователи».

{error && (
{error}
)} {showForm && (

{editingId != null ? 'Редактирование шаблона' : 'Новый шаблон'}

setFormName(e.target.value)} placeholder="Например: Мастер участка" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
setFormDescription(e.target.value)} placeholder="Краткое описание" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />

В сводках и отчётах пользователь увидит данные по всем участкам или только по своему.

setFormForPosition(e.target.value)} placeholder="Например: Мастер участка, Начальник PR отдела" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />

При настройке прав пользователя можно применить шаблон по должности сотрудника.

При применении шаблона к пользователю можно подставить эту роль (права задаются шаблоном).

Отметьте раздел целиком или только нужные подразделы/отчёты.

{ALL_SECTION_KEYS.map((section) => { const subs = SECTION_SUBS[section]; const sectionChecked = hasSection(section); return (
{subs && subs.length > 0 && !sectionChecked && (
{subs.map((sub) => (
{sub.label}
))}
)}
); })}
)}
{templates.length === 0 ? ( ) : ( templates.map((t) => ( )) )}
Название Описание Обзор / Должность Разделы Действия
Нет шаблонов. Создайте первый выше.
{t.name} {t.description || '—'} {t.scope === 'own_district' ? 'Свой участок' : 'Все участки'} {t.forPosition ? ` · ${t.forPosition}` : ''} {t.suggestedRole ? ` · ${ROLE_NAMES[t.suggestedRole] || t.suggestedRole}` : ''} {formatPermissionsShort(t.permissions || [])} {applyToUserId === null ? ( <> ) : applyToUserId === t.id ? (
Применить к:
) : null}
); };