193 lines
8.0 KiB
TypeScript
Executable File
193 lines
8.0 KiB
TypeScript
Executable File
|
||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||
import { DomaApplication } from '../types';
|
||
import { backendApi } from '../services/apiClient';
|
||
import { readCache, saveCache } from '../hooks/useCachedFetch';
|
||
import { REFRESH_EVENTS } from '../constants/refreshEvents';
|
||
import {
|
||
LayoutDashboard,
|
||
Inbox,
|
||
ShieldAlert,
|
||
Sparkles,
|
||
Plus,
|
||
RotateCw,
|
||
AlertTriangle,
|
||
Printer,
|
||
Zap
|
||
} from 'lucide-react';
|
||
|
||
// Modular Imports
|
||
import { AppSummary } from './applications/AppSummary';
|
||
import { ApplicationsRegistry } from './applications/ApplicationsRegistry';
|
||
import { DispatcherControl } from './applications/DispatcherControl';
|
||
import { QualityControl } from './applications/QualityControl';
|
||
import { CreateApplicationCard } from './applications/CreateApplicationCard';
|
||
import { OutagesJournal } from './applications/OutagesJournal';
|
||
import { allowedSubsForSection } from '../constants/permissions';
|
||
|
||
type Tab = 'summary' | 'registry' | 'control' | 'quality' | 'mappings' | 'outages';
|
||
|
||
const REQUESTS_TABS: Tab[] = ['summary', 'registry', 'control', 'quality', 'mappings', 'outages'];
|
||
const SUBTAB_KEY = 'mkd_subTab_requests';
|
||
|
||
interface ApplicationsModuleProps {
|
||
integrationEnabled?: boolean;
|
||
allowedPermissions?: string[] | null;
|
||
}
|
||
|
||
export const ApplicationsModule: React.FC<ApplicationsModuleProps> = ({ integrationEnabled = true, allowedPermissions }) => {
|
||
const visibleTabs = useMemo(() => {
|
||
const allowed = allowedSubsForSection(allowedPermissions ?? [], 'requests');
|
||
if (allowed === 'all') return REQUESTS_TABS;
|
||
return REQUESTS_TABS.filter((t) => allowed.includes(t));
|
||
}, [allowedPermissions]);
|
||
|
||
const CACHE_KEY = 'mkd_applications_cache';
|
||
const cached = readCache<DomaApplication[]>(CACHE_KEY, []);
|
||
|
||
const [createCardOpen, setCreateCardOpen] = useState(false);
|
||
const [activeTab, setActiveTab] = useState<Tab>(() => {
|
||
const s = localStorage.getItem(SUBTAB_KEY);
|
||
return (s && REQUESTS_TABS.includes(s as Tab)) ? s as Tab : 'summary';
|
||
});
|
||
const [applications, setApplications] = useState<DomaApplication[]>(cached);
|
||
const [loading, setLoading] = useState(cached.length === 0);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
if (visibleTabs.length > 0 && !visibleTabs.includes(activeTab)) {
|
||
setActiveTab(visibleTabs[0]);
|
||
}
|
||
}, [visibleTabs, activeTab]);
|
||
|
||
const fetchApplications = useCallback(async (showSpinner = true) => {
|
||
if (showSpinner) setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const data = await backendApi.getApplications();
|
||
setApplications(data);
|
||
saveCache(CACHE_KEY, data);
|
||
} catch (err) {
|
||
console.error('[ApplicationsModule] Ошибка загрузки заявок:', err);
|
||
setError('Ошибка загрузки данных заявок с сервера. Проверьте подключение или логи синхронизации Doma AI.');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
fetchApplications(cached.length === 0);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const onRefresh = () => fetchApplications(false);
|
||
window.addEventListener(REFRESH_EVENTS.applications, onRefresh);
|
||
return () => window.removeEventListener(REFRESH_EVENTS.applications, onRefresh);
|
||
}, [fetchApplications]);
|
||
|
||
// Фоновый опрос: чтобы другой пользователь видел новые заявки без перезагрузки
|
||
useEffect(() => {
|
||
const interval = setInterval(() => fetchApplications(false), 10 * 1000);
|
||
return () => clearInterval(interval);
|
||
}, [fetchApplications]);
|
||
|
||
useEffect(() => {
|
||
localStorage.setItem(SUBTAB_KEY, activeTab);
|
||
}, [activeTab]);
|
||
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center py-20 animate-pulse">
|
||
<RotateCw className="w-10 h-10 text-primary-400 animate-spin mb-4" />
|
||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em]">
|
||
{integrationEnabled ? 'Синхронизация с doma.ai...' : 'Загрузка заявок...'}
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="text-center py-20">
|
||
<AlertTriangle className="w-16 h-16 text-red-400 mx-auto mb-4" />
|
||
<h3 className="text-lg font-bold text-slate-700">Ошибка системы</h3>
|
||
<p className="text-sm text-slate-500 mb-6">{error}</p>
|
||
<button onClick={() => fetchApplications(true)} className="bg-primary-600 text-white px-6 py-2 rounded-xl font-bold">Повторить</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="animate-fade-in pb-20">
|
||
<div className="flex justify-between items-center mb-6 px-1">
|
||
<div>
|
||
<h2 className="text-xl font-bold text-slate-800 leading-none">Диспетчерская 24/7</h2>
|
||
<p className="text-[10px] text-slate-400 font-black uppercase tracking-widest mt-1">Центр управления инцидентами</p>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button
|
||
className="p-2.5 bg-white border border-slate-200 text-slate-400 rounded-xl hover:text-primary-600 transition-all shadow-sm"
|
||
title="Обновить реестр"
|
||
onClick={() => window.dispatchEvent(new CustomEvent(REFRESH_EVENTS.applications))}
|
||
>
|
||
<RotateCw className="w-5 h-5"/>
|
||
</button>
|
||
<button
|
||
className="p-2.5 bg-white border border-slate-200 text-slate-400 rounded-xl hover:text-primary-600 transition-all shadow-sm"
|
||
title="Печать отчетов"
|
||
>
|
||
<Printer className="w-5 h-5"/>
|
||
</button>
|
||
<button
|
||
className="bg-primary-600 text-white p-2.5 rounded-xl shadow-lg shadow-primary-500/30 flex items-center gap-2 px-4 text-xs font-black uppercase tracking-wider active:scale-95 transition-all"
|
||
onClick={() => setCreateCardOpen(true)}
|
||
>
|
||
<Plus className="w-4 h-4" /> Создать заявку
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Sheet Selector */}
|
||
<div className="flex p-1 bg-slate-200/50 rounded-2xl mb-6 overflow-x-auto no-scrollbar gap-1">
|
||
{[
|
||
{ id: 'summary', label: 'Сводка', icon: LayoutDashboard },
|
||
{ id: 'registry', label: 'Реестр', icon: Inbox },
|
||
{ id: 'control', label: 'Контроль', icon: ShieldAlert },
|
||
{ id: 'quality', label: 'Качество', icon: Sparkles },
|
||
{ id: 'outages', label: 'Журнал отключений', icon: Zap },
|
||
].filter((tab) => visibleTabs.includes(tab.id)).map((tab) => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id as Tab)}
|
||
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 ${activeTab === tab.id ? 'bg-white text-primary-600 shadow-sm border border-white' : 'text-slate-500 hover:text-slate-700'}`}
|
||
>
|
||
<tab.icon className="w-3.5 h-3.5"/> {tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Dynamic Sheet Content */}
|
||
<div className="min-h-[280px] sm:min-h-[360px] md:min-h-[500px]">
|
||
{activeTab === 'summary' && <AppSummary applications={applications} onNavigate={setActiveTab} />}
|
||
{activeTab === 'registry' && <ApplicationsRegistry applications={applications} onRefresh={fetchApplications} />}
|
||
{activeTab === 'control' && <DispatcherControl applications={applications} />}
|
||
{activeTab === 'quality' && <QualityControl />}
|
||
{activeTab === 'outages' && (
|
||
<div className="mt-2">
|
||
<OutagesJournal />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{createCardOpen && (
|
||
<CreateApplicationCard
|
||
isOpen={createCardOpen}
|
||
onClose={() => setCreateCardOpen(false)}
|
||
onSuccess={fetchApplications}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|