import React, { useState, useEffect } from 'react'; import { Scale, FileSignature, Gavel, FileText, ShieldAlert, Calendar, TrendingUp, Loader2, AlertTriangle } from 'lucide-react'; import { authFetch } from '../../services/apiClient'; import { readCache, saveCache } from '../../hooks/useCachedFetch'; import { REFRESH_EVENTS } from '../../constants/refreshEvents'; interface Props { onNavigate: (tab: any) => void; } interface Contract { id: string; status: string; } interface CourtCase { id: string; caseNumber: string; subject: string; amount: number; nextHearingDate?: string; judge?: string; status: string; recoveredAmount?: number; amountAtBailiffs?: number; fsspLastActionDate?: string; debtorName?: string; } interface CounterpartyCheck { id: number; riskLevel: string; checkedDate?: string; checkedAt?: string; // альтернативное поле } const CACHE_KEY = 'mkd_legal_summary_cache'; const CACHE_DEFAULT = { preTrialStats: { total: 0, inProgress: 0, promisedPayment: 0, transferredToCourt: 0 }, contracts: [], courtCases: [], counterpartyChecks: [], efficiency: { percentage: 0, recovered: 0, totalClaimed: 0, totalPenalty: 0 } }; export const LegalSummary: React.FC = ({ onNavigate }) => { const cached = readCache(CACHE_KEY, CACHE_DEFAULT); const hasCache = (cached.contracts?.length ?? 0) > 0 || (cached.courtCases?.length ?? 0) > 0; const [loading, setLoading] = useState(!hasCache); const [preTrialStats, setPreTrialStats] = useState(cached.preTrialStats || CACHE_DEFAULT.preTrialStats); const [contracts, setContracts] = useState(cached.contracts || []); const [courtCases, setCourtCases] = useState(cached.courtCases || []); const [counterpartyChecks, setCounterpartyChecks] = useState(cached.counterpartyChecks || []); const [efficiency, setEfficiency] = useState(cached.efficiency || CACHE_DEFAULT.efficiency); useEffect(() => { loadAllData(); }, []); useEffect(() => { const onRefresh = () => loadAllData(false); window.addEventListener(REFRESH_EVENTS.legal, onRefresh); return () => window.removeEventListener(REFRESH_EVENTS.legal, onRefresh); }, []); useEffect(() => { const interval = setInterval(() => loadAllData(false), 10 * 1000); return () => clearInterval(interval); }, []); const loadAllData = async (showSpinner = true) => { try { if (showSpinner && !hasCache) setLoading(true); // Загружаем все данные параллельно const [preTrialRes, contractsRes, courtCasesRes, counterpartiesRes] = await Promise.all([ authFetch('/api/legal/pre-trial-work'), authFetch('/api/legal/contracts?viewMode=active'), authFetch('/api/legal/court-cases'), authFetch('/api/legal/counterparties?limit=100') ]); // Обрабатываем досудебную работу if (preTrialRes.ok) { const preTrialData = await preTrialRes.json(); setPreTrialStats({ total: preTrialData.length || 0, inProgress: preTrialData.filter((w: any) => w.status === 'in_progress').length, promisedPayment: preTrialData.filter((w: any) => w.status === 'promised_payment').length, transferredToCourt: preTrialData.filter((w: any) => w.status === 'transferred_to_court').length }); } // Обрабатываем договоры if (contractsRes.ok) { const contractsData = await contractsRes.json(); setContracts(contractsData || []); } // Обрабатываем судебные дела if (courtCasesRes.ok) { const courtCasesData = await courtCasesRes.json(); setCourtCases(courtCasesData || []); // Рассчитываем эффективность на основе реальных данных const debtRecoveryCases = (courtCasesData || []).filter((c: any) => c.type === 'debt_recovery' || (c.subject && c.subject.toLowerCase().includes('взыскание')) ); const totalClaimed = debtRecoveryCases.reduce((sum: number, c: any) => sum + (parseFloat(c.amount) || 0), 0); const totalRecovered = debtRecoveryCases.reduce((sum: number, c: any) => sum + (parseFloat(c.recoveredAmount) || 0) + (parseFloat(c.amountAtBailiffs) || 0), 0 ); const totalPenalty = debtRecoveryCases.reduce((sum: number, c: any) => sum + (parseFloat(c.penaltyAmount) || 0), 0); const percentage = totalClaimed > 0 ? Math.round((totalRecovered / totalClaimed) * 100) : 0; setEfficiency({ percentage: Math.min(percentage, 100), recovered: totalRecovered, totalClaimed, totalPenalty }); } // Обрабатываем проверки контрагентов if (counterpartiesRes.ok) { const counterpartiesData = await counterpartiesRes.json(); setCounterpartyChecks(counterpartiesData || []); } } catch (error) { console.error('Error loading legal summary data:', error); } finally { setLoading(false); } }; useEffect(() => { if (!loading && (contracts.length > 0 || courtCases.length > 0)) { saveCache(CACHE_KEY, { preTrialStats, contracts, courtCases, counterpartyChecks, efficiency }); } }, [loading, preTrialStats, contracts, courtCases, counterpartyChecks, efficiency]); // Статистика договоров const activeContracts = contracts.filter(c => c.status === 'active').length; const approvalContracts = contracts.filter(c => ['draft', 'finance_approval', 'counterparty_approval', 'signing'].includes(c.status) ).length; // Статистика судебных дел const activeCourtCases = courtCases.filter(c => c.status !== 'closed').length; // Зависло у приставов: enforcement и более 20 дней без действий const stuckBailiffCases = courtCases.filter(c => { if (c.status !== 'enforcement') return false; const lastDate = c.fsspLastActionDate; if (!lastDate) return true; const days = Math.floor((Date.now() - new Date(lastDate).getTime()) / (1000 * 3600 * 24)); return days > 20; }); const stuckBailiffSum = stuckBailiffCases.reduce((s, c) => s + (c.amountAtBailiffs || 0), 0); // Ближайшие заседания (следующие 30 дней) const upcomingHearings = courtCases .filter(c => c.nextHearingDate && c.status !== 'closed') .map(c => ({ ...c, hearingDate: new Date(c.nextHearingDate!) })) .filter(c => { const today = new Date(); const hearingDate = c.hearingDate; const diffTime = hearingDate.getTime() - today.getTime(); const diffDays = diffTime / (1000 * 60 * 60 * 24); return diffDays >= 0 && diffDays <= 30; }) .sort((a, b) => a.hearingDate.getTime() - b.hearingDate.getTime()) .slice(0, 5); // Статистика проверок контрагентов const highRiskChecks = counterpartyChecks.filter(c => c.riskLevel === 'high').length; const recentChecks = counterpartyChecks.filter(c => { const checkDateStr = c.checkedDate || c.checkedAt; if (!checkDateStr) return false; try { const checkDate = new Date(checkDateStr); if (isNaN(checkDate.getTime())) return false; const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); return checkDate >= thirtyDaysAgo; } catch { return false; } }).length; // Форматирование даты для календаря const formatHearingDate = (dateString: string) => { const date = new Date(dateString); const months = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']; return { day: date.getDate().toString().padStart(2, '0'), month: months[date.getMonth()] }; }; if (loading) { return (
); } return (
{/* Main Stats Grid */}
onNavigate('contracts')} /> onNavigate('courts')} /> 0 ? `${preTrialStats.inProgress} в работе` : undefined} color="text-blue-600" bg="bg-blue-50" onClick={() => onNavigate('preTrial')} /> 0 ? highRiskChecks : recentChecks} subValue={highRiskChecks > 0 ? `${highRiskChecks} высокий риск` : `${recentChecks} проверок за месяц`} color="text-emerald-600" bg="bg-emerald-50" onClick={() => onNavigate('compliance')} />
{/* Court Calendar Preview */}

Календарь заседаний

{upcomingHearings.length} ближайших
{upcomingHearings.length > 0 ? ( upcomingHearings.map(c => { const dateInfo = formatHearingDate(c.nextHearingDate!); return (
onNavigate('courts')} >

{dateInfo.day}

{dateInfo.month}

{c.caseNumber}

{c.amount?.toLocaleString() || 0}₽

{c.subject}

{c.judge && (

Судья: {c.judge}

)}
); }) ) : (

Нет ближайших заседаний

)}
{/* Зависло у приставов */} {stuckBailiffCases.length > 0 && (
onNavigate('debt')} className="bg-amber-50 rounded-2xl border-2 border-amber-200 p-4 cursor-pointer hover:border-amber-400 transition-colors" >

Зависло у приставов

{stuckBailiffCases.length} дел

{stuckBailiffSum.toLocaleString()} ₽ на депозите

    {stuckBailiffCases.slice(0, 5).map(c => (
  • {c.debtorName || c.caseNumber}
  • ))}

Перейти во Взыскание →

)} {/* Efficiency Widget */}

Эффективность

{efficiency.percentage}%

Процент взысканной задолженности от общей суммы исков по делам о взыскании долгов.

Взыскано {efficiency.recovered >= 1000000 ? `${(efficiency.recovered / 1000000).toFixed(1)}M ₽` : efficiency.recovered >= 1000 ? `${(efficiency.recovered / 1000).toFixed(1)}K ₽` : `${efficiency.recovered.toLocaleString()} ₽` }
{efficiency.totalClaimed > 0 && (
Из {efficiency.totalClaimed.toLocaleString()} ₽ заявлено {(efficiency.totalPenalty ?? 0) > 0 && ( в т.ч. пени {(efficiency.totalPenalty || 0).toLocaleString()} ₽ )}
)}
); }; const StatCard = ({ icon: Icon, label, value, subValue, color, bg, onClick }: any) => (
{subValue && ( {subValue} )}

{value}

{label}

);