114 lines
5.3 KiB
TypeScript
Executable File
114 lines
5.3 KiB
TypeScript
Executable File
import React, { useState, useEffect, useMemo } from 'react';
|
||
import { UserCog, Plug, DatabaseBackup, Upload, Shield, ShieldCheck, Trash2, Building2, Bot, Users, Briefcase } from 'lucide-react';
|
||
import { allowedSubsForSection } from '../constants/permissions';
|
||
import { UsersSection } from './admin/UsersSection';
|
||
import { IntegrationsSection } from './admin/IntegrationsSection';
|
||
import { AISection } from './admin/AISection';
|
||
import { BackupsSection } from './admin/BackupsSection';
|
||
import { DataImportSection } from './admin/DataImportSection';
|
||
import { DataCleanupSection } from './admin/DataCleanupSection';
|
||
import { PermissionsSection } from './admin/PermissionsSection';
|
||
import { SecuritySection } from './admin/SecuritySection';
|
||
import { CompanySection } from './admin/CompanySection';
|
||
import { ResponsibilityZonesSection } from './admin/ResponsibilityZonesSection';
|
||
import { PositionsSection } from './admin/PositionsSection';
|
||
|
||
export type AdminSectionId = 'users' | 'permissions' | 'integrations' | 'ai' | 'company' | 'positions' | 'responsibility-zones' | 'backups' | 'data-import' | 'data-cleanup' | 'security';
|
||
|
||
const ADMIN_SECTIONS: Array<{
|
||
id: AdminSectionId;
|
||
label: string;
|
||
icon: React.ComponentType<{ className?: string }>;
|
||
component: React.ComponentType;
|
||
}> = [
|
||
{ id: 'users', label: 'Пользователи', icon: UserCog, component: UsersSection },
|
||
{ id: 'permissions', label: 'Права и шаблоны', icon: Shield, component: PermissionsSection },
|
||
{ id: 'integrations', label: 'Интеграции', icon: Plug, component: IntegrationsSection },
|
||
{ id: 'ai', label: 'ИИ', icon: Bot, component: AISection },
|
||
{ id: 'company', label: 'О компании', icon: Building2, component: CompanySection },
|
||
{ id: 'positions', label: 'Должности', icon: Briefcase, component: PositionsSection },
|
||
{ id: 'responsibility-zones', label: 'Зоны ответственности', icon: Users, component: ResponsibilityZonesSection },
|
||
{ id: 'backups', label: 'Резервные копии', icon: DatabaseBackup, component: BackupsSection },
|
||
{ id: 'data-import', label: 'Загрузка данных', icon: Upload, component: DataImportSection },
|
||
{ id: 'data-cleanup', label: 'Очистка данных', icon: Trash2, component: DataCleanupSection },
|
||
{ id: 'security', label: 'Безопасность', icon: ShieldCheck, component: SecuritySection },
|
||
];
|
||
|
||
const ADMIN_SUBTAB_KEY = 'mkd_subTab_admin';
|
||
|
||
export type AdminModuleProps = {
|
||
allowedPermissions?: string[] | null;
|
||
};
|
||
|
||
export const AdminModule: React.FC<AdminModuleProps> = ({ allowedPermissions }) => {
|
||
const visibleSectionIds = useMemo(() => {
|
||
const allowed = allowedSubsForSection(allowedPermissions ?? [], 'admin');
|
||
return allowed === 'all' ? ADMIN_SECTIONS.map((s) => s.id) : allowed;
|
||
}, [allowedPermissions]);
|
||
|
||
const visibleSections = useMemo(
|
||
() => ADMIN_SECTIONS.filter((s) => visibleSectionIds.includes(s.id)),
|
||
[visibleSectionIds]
|
||
);
|
||
|
||
const [activeSection, setActiveSection] = useState<AdminSectionId>(() => {
|
||
const s = localStorage.getItem(ADMIN_SUBTAB_KEY);
|
||
const id = s as AdminSectionId;
|
||
const allowed = allowedSubsForSection(allowedPermissions ?? [], 'admin');
|
||
const ids = allowed === 'all' ? ADMIN_SECTIONS.map((x) => x.id) : allowed;
|
||
if (id && ids.includes(id)) return id;
|
||
return (ids[0] as AdminSectionId) ?? 'users';
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (!visibleSectionIds.includes(activeSection)) {
|
||
setActiveSection((visibleSectionIds[0] as AdminSectionId) ?? 'users');
|
||
}
|
||
}, [visibleSectionIds, activeSection]);
|
||
|
||
useEffect(() => {
|
||
localStorage.setItem(ADMIN_SUBTAB_KEY, activeSection);
|
||
}, [activeSection]);
|
||
|
||
const ActiveComponent = ADMIN_SECTIONS.find((s) => s.id === activeSection)?.component ?? UsersSection;
|
||
|
||
if (visibleSections.length === 0) {
|
||
return (
|
||
<div className="animate-fade-in pb-20">
|
||
<p className="text-slate-500 text-sm">Нет доступа к разделам панели управления.</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="animate-fade-in pb-20">
|
||
<div className="mb-6 px-1">
|
||
<h2 className="text-xl font-bold text-slate-800 leading-none">Панель управления</h2>
|
||
<p className="text-[10px] text-slate-400 font-black uppercase tracking-widest mt-1">
|
||
Пользователи, интеграции, данные компании, резервные копии и безопасность
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex p-1 bg-slate-200/50 rounded-2xl mb-6 overflow-x-auto no-scrollbar gap-1">
|
||
{visibleSections.map((section) => (
|
||
<button
|
||
key={section.id}
|
||
onClick={() => setActiveSection(section.id)}
|
||
className={`flex-shrink-0 min-w-[7rem] flex items-center justify-center gap-2 py-2.5 px-4 text-[10px] font-black uppercase tracking-wider whitespace-nowrap rounded-xl transition-all ${
|
||
activeSection === section.id
|
||
? 'bg-white text-primary-600 shadow-sm border border-white'
|
||
: 'text-slate-500 hover:text-slate-700'
|
||
}`}
|
||
>
|
||
<section.icon className="w-3.5 h-3.5" /> {section.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
|
||
<ActiveComponent />
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|