Files
mkd/components/ApplicationsModule.tsx
2026-02-04 00:17:04 +05:00

193 lines
8.0 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.
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>
);
};