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

916 lines
53 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 } from 'react';
import { LegalCourtCase, FsspStage } from '../../types';
import {
HandCoins,
Gavel,
ShieldAlert,
CheckCircle2,
ArrowRight,
Search,
Filter,
TrendingUp,
Landmark,
MessageSquare,
AlertCircle,
ChevronRight,
Zap,
CircleDollarSign,
Receipt,
Clock,
AlertTriangle,
Plus,
Loader2,
X,
FileText,
History,
User,
Calendar,
DollarSign,
Edit,
Save
} from 'lucide-react';
import { authFetch } from '../../services/apiClient';
import { CaseDetailsModal } from './CaseDetailsModal';
interface DebtRecoveryPipelineProps {
onNavigateToPreTrial?: () => void;
}
interface PreTrialWorkItem {
id: number;
status: string;
debtor?: { debtorName?: string; address?: string; debt_amount?: number; apartment?: string };
debtorId?: number;
}
export const DebtRecoveryPipeline: React.FC<DebtRecoveryPipelineProps> = ({ onNavigateToPreTrial }) => {
const [search, setSearch] = useState('');
const [viewMode, setViewMode] = useState<'pipeline' | 'fssp_active' | 'pipeline_board'>('pipeline');
const [debtCases, setDebtCases] = useState<LegalCourtCase[]>([]);
const [preTrialWorks, setPreTrialWorks] = useState<PreTrialWorkItem[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCase, setSelectedCase] = useState<LegalCourtCase | null>(null);
const [showCaseModal, setShowCaseModal] = useState(false);
const [showBailiffModal, setShowBailiffModal] = useState(false);
const loadCases = async () => {
try {
setLoading(true);
const query = new URLSearchParams({ type: 'debt_recovery' });
if (search.trim()) query.set('search', search.trim());
const response = await authFetch(`/api/legal/court-cases?${query.toString()}`);
if (response.ok) {
const data = await response.json();
// Фильтруем дела типа debt_recovery или содержащие "взыскание" в предмете
const filtered = Array.isArray(data)
? data.filter((c: LegalCourtCase) =>
c.type === 'debt_recovery' ||
(c.subject && c.subject.toLowerCase().includes('взыскание')) ||
(c.subject && c.subject.toLowerCase().includes('задолженност'))
)
: [];
setDebtCases(filtered);
} else {
setDebtCases([]);
}
} catch (error) {
console.error('Error loading court cases:', error);
setDebtCases([]);
} finally {
setLoading(false);
}
};
const loadPreTrialWorks = async () => {
try {
const response = await authFetch('/api/legal/pre-trial-work');
if (response.ok) {
const data = await response.json();
const list = Array.isArray(data) ? data : [];
const notTransferred = list.filter((w: PreTrialWorkItem) => w.status !== 'transferred_to_court' && w.status !== 'resolved');
setPreTrialWorks(notTransferred);
} else {
setPreTrialWorks([]);
}
} catch (e) {
console.error('Error loading pre-trial work:', e);
setPreTrialWorks([]);
}
};
useEffect(() => {
if (search === '') {
loadCases();
return;
}
const t = setTimeout(() => loadCases(), 300);
return () => clearTimeout(t);
}, [search]);
useEffect(() => {
if (viewMode === 'pipeline_board') {
loadPreTrialWorks();
}
}, [viewMode]);
const totalClaimed = debtCases.length > 0 ? debtCases.reduce((sum, c) => sum + (c.amount || 0), 0) : 0;
const totalRecovered = debtCases.length > 0 ? debtCases.reduce((sum, c) => sum + (c.recoveredAmount || 0), 0) : 0;
const moneyAtBailiffs = debtCases.length > 0 ? debtCases.reduce((sum, c) => sum + (c.amountAtBailiffs || 0), 0) : 0;
const inTransit = totalClaimed > 0 ? totalClaimed - totalRecovered - moneyAtBailiffs : 0;
return (
<div className="space-y-6 animate-fade-in">
{/* Financial Dashboard for Legal - ENHANCED */}
<div className="bg-slate-900 rounded-[2.5rem] p-8 text-white shadow-xl relative overflow-hidden">
<HandCoins className="absolute -top-4 -right-4 w-48 h-48 opacity-10 rotate-12 text-emerald-400" />
<div className="relative z-10">
<div className="flex justify-between items-start mb-10">
<div>
<h3 className="text-2xl font-black">Возврат задолженности</h3>
<p className="text-slate-400 text-xs font-bold uppercase tracking-widest mt-1">Прямой приход денежных средств</p>
</div>
<div className="flex gap-4">
<div className="text-right">
<p className="text-[10px] font-black text-emerald-400 uppercase tracking-widest">Взыскано (УК)</p>
<p className="text-2xl font-black text-emerald-400">{totalRecovered.toLocaleString()} </p>
</div>
<div className="text-right border-l border-white/10 pl-4">
<p className="text-[10px] font-black text-amber-400 uppercase tracking-widest">У приставов</p>
<p className="text-2xl font-black text-amber-400">{moneyAtBailiffs.toLocaleString()} </p>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div>
<p className="text-slate-400 text-[10px] font-black uppercase tracking-tighter mb-1">Сумма в работе</p>
<p className="text-xl font-black text-white">{totalClaimed.toLocaleString()} </p>
</div>
<div className="flex flex-col justify-end">
<div className="flex justify-between text-[9px] font-black uppercase text-slate-500 mb-1">
<span>Прогресс сбора</span>
<span>{Math.round(((totalRecovered + moneyAtBailiffs)/totalClaimed)*100)}%</span>
</div>
<div className="h-1.5 w-full bg-white/10 rounded-full overflow-hidden flex">
<div className="h-full bg-emerald-500" style={{ width: `${(totalRecovered/totalClaimed)*100}%` }} />
<div className="h-full bg-amber-500" style={{ width: `${(moneyAtBailiffs/totalClaimed)*100}%` }} />
</div>
</div>
<div className="text-right">
<p className="text-slate-400 text-[10px] font-black uppercase tracking-tighter mb-1">Остаток (Листы/Суд)</p>
<p className="text-xl font-black text-slate-300">{inTransit.toLocaleString()} </p>
</div>
</div>
</div>
</div>
{/* View Toggle */}
<div className="flex p-1 bg-slate-200/50 rounded-2xl w-full md:w-fit gap-1">
<button
onClick={() => setViewMode('pipeline')}
className={`flex-1 md:flex-none px-6 py-2.5 rounded-xl text-[10px] font-black uppercase tracking-wider transition-all ${viewMode === 'pipeline' ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500'}`}
>
Все дела
</button>
<button
onClick={() => setViewMode('pipeline_board')}
className={`flex-1 md:flex-none px-6 py-2.5 rounded-xl text-[10px] font-black uppercase tracking-wider transition-all ${viewMode === 'pipeline_board' ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500'}`}
>
Пайплайн
</button>
<button
onClick={() => setViewMode('fssp_active')}
className={`flex-1 md:flex-none px-6 py-2.5 rounded-xl text-[10px] font-black uppercase tracking-wider transition-all ${viewMode === 'fssp_active' ? 'bg-white text-amber-600 shadow-sm' : 'text-slate-500'}`}
>
Контроль приставов
</button>
</div>
{viewMode === 'pipeline_board' ? (
<div className="overflow-x-auto pb-4">
<div className="flex gap-4 min-w-max">
<PipelineColumn title="Досудебная" count={preTrialWorks.length} color="bg-slate-100 border-slate-200">
{preTrialWorks.map(w => (
<div key={w.id} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<p className="font-bold text-slate-800 text-sm truncate">{w.debtor?.debtorName || 'Должник'}</p>
<p className="text-[10px] text-slate-500 uppercase mt-0.5 truncate">{w.debtor?.address}</p>
<p className="text-xs font-black text-slate-700 mt-2">{(w.debtor?.debt_amount || 0).toLocaleString()} </p>
<button type="button" onClick={() => onNavigateToPreTrial?.()} className="mt-2 w-full py-1.5 text-[10px] font-black uppercase bg-slate-100 text-slate-600 rounded-lg hover:bg-slate-200">В досудебную</button>
</div>
))}
</PipelineColumn>
<PipelineColumn title="В суде" count={debtCases.filter(c => c.status === 'pre_trial' || c.status === 'litigation').length} color="bg-blue-50 border-blue-200">
{debtCases.filter(c => c.status === 'pre_trial' || c.status === 'litigation').map(c => (
<div key={c.id} onClick={() => { setSelectedCase(c); setShowCaseModal(true); }} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm cursor-pointer hover:border-primary-300">
<p className="font-bold text-slate-800 text-sm truncate">{c.debtorName}</p>
<p className="text-[10px] text-slate-500 uppercase mt-0.5 truncate">{c.address}</p>
<p className="text-xs font-black text-slate-700 mt-2">{c.amount.toLocaleString()} </p>
</div>
))}
</PipelineColumn>
<PipelineColumn title="Решение получено" count={debtCases.filter(c => c.status === 'decision_received').length} color="bg-indigo-50 border-indigo-200">
{debtCases.filter(c => c.status === 'decision_received').map(c => (
<div key={c.id} onClick={() => { setSelectedCase(c); setShowCaseModal(true); }} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm cursor-pointer hover:border-primary-300">
<p className="font-bold text-slate-800 text-sm truncate">{c.debtorName}</p>
<p className="text-[10px] text-slate-500 uppercase mt-0.5 truncate">{c.address}</p>
<p className="text-xs font-black text-slate-700 mt-2">{c.amount.toLocaleString()} </p>
</div>
))}
</PipelineColumn>
<PipelineColumn title="У приставов" count={debtCases.filter(c => c.status === 'enforcement').length} color="bg-amber-50 border-amber-200">
{debtCases.filter(c => c.status === 'enforcement').map(c => (
<div key={c.id} onClick={() => { setSelectedCase(c); setShowBailiffModal(true); }} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm cursor-pointer hover:border-amber-300">
<p className="font-bold text-slate-800 text-sm truncate">{c.debtorName}</p>
<p className="text-[10px] text-slate-500 uppercase mt-0.5 truncate">{c.address}</p>
<p className="text-xs font-black text-amber-600 mt-2">{(c.amountAtBailiffs || 0).toLocaleString()} у ФССП</p>
</div>
))}
</PipelineColumn>
<PipelineColumn title="Закрыто" count={debtCases.filter(c => c.status === 'closed').length} color="bg-emerald-50 border-emerald-200">
{debtCases.filter(c => c.status === 'closed').map(c => (
<div key={c.id} onClick={() => { setSelectedCase(c); setShowCaseModal(true); }} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm cursor-pointer hover:border-emerald-300">
<p className="font-bold text-slate-800 text-sm truncate">{c.debtorName}</p>
<p className="text-[10px] text-slate-500 uppercase mt-0.5 truncate">{c.address}</p>
<p className="text-xs font-black text-emerald-600 mt-2">Взыскано {(c.recoveredAmount || 0).toLocaleString()} </p>
</div>
))}
</PipelineColumn>
</div>
</div>
) : viewMode === 'pipeline' ? (
<div className="space-y-4">
<div className="flex gap-4 px-1">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Поиск должника или квартиры..."
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full pl-9 pr-4 py-3 bg-white border border-slate-200 rounded-2xl text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm"
/>
</div>
<button className="p-3 bg-white border border-slate-200 rounded-2xl text-slate-500"><Filter className="w-5 h-5"/></button>
<button
onClick={() => onNavigateToPreTrial ? onNavigateToPreTrial() : (window.location.hash = '#preTrial')}
className="bg-primary-600 text-white px-4 py-3 rounded-xl shadow-lg shadow-primary-500/30 flex items-center gap-2 text-xs font-black uppercase tracking-wider active:scale-95 transition-all"
>
<Plus className="w-4 h-4" /> Новое дело
</button>
</div>
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="text-slate-400">Загрузка...</div>
</div>
) : (
<div className="space-y-3">
{debtCases.map(caseItem => (
<DebtCaseCard
key={caseItem.id}
item={caseItem}
onClick={() => {
setSelectedCase(caseItem);
setShowCaseModal(true);
}}
/>
))}
{debtCases.length === 0 && (
<div className="py-20 text-center text-slate-400">
<p className="font-bold uppercase tracking-widest text-xs">Дела не найдены</p>
</div>
)}
</div>
)}
</div>
) : (
<div className="space-y-4">
{/* FSSP Detailed View */}
<div className="bg-amber-50 border border-amber-100 p-4 rounded-2xl flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-amber-600 shrink-0 mt-0.5"/>
<div>
<p className="text-xs font-black text-amber-800 uppercase tracking-tight">Зависло у приставов: {moneyAtBailiffs.toLocaleString()} </p>
<p className="text-[10px] text-amber-700 mt-1">Деньги взысканы ФССП, но не поступили на счет УК. Требуется сверка с депозитом отдела.</p>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="text-slate-400">Загрузка...</div>
</div>
) : (
<div className="space-y-3">
{debtCases.filter(c => c.status === 'enforcement').map(caseItem => (
<BailiffActionCard
key={caseItem.id}
item={caseItem}
onClick={() => {
setSelectedCase(caseItem);
setShowBailiffModal(true);
}}
/>
))}
{debtCases.filter(c => c.status === 'enforcement').length === 0 && (
<div className="py-20 text-center text-slate-400">
<p className="font-bold uppercase tracking-widest text-xs">Нет дел у приставов</p>
</div>
)}
</div>
)}
</div>
)}
{/* Case Details Modal */}
{showCaseModal && selectedCase && (
<CaseDetailsModal
courtCase={selectedCase}
onClose={() => {
setShowCaseModal(false);
setSelectedCase(null);
}}
onUpdate={(updatedCase) => {
setDebtCases(debtCases.map(c => c.id === updatedCase.id ? updatedCase : c));
loadCases();
}}
/>
)}
{/* Bailiff Interaction Modal */}
{showBailiffModal && selectedCase && (
<BailiffInteractionModalComponent
courtCase={selectedCase}
onClose={() => {
setShowBailiffModal(false);
setSelectedCase(null);
}}
onUpdate={(updatedCase) => {
setDebtCases(debtCases.map(c => c.id === updatedCase.id ? updatedCase : c));
loadCases();
}}
/>
)}
</div>
);
};
const PipelineColumn: React.FC<{ title: string; count: number; color: string; children: React.ReactNode }> = ({ title, count, color, children }) => (
<div className={`w-64 flex-shrink-0 rounded-2xl border-2 p-4 ${color}`}>
<div className="flex justify-between items-center mb-3">
<h4 className="text-xs font-black text-slate-700 uppercase tracking-wider">{title}</h4>
<span className="text-[10px] font-black text-slate-500 bg-white/80 px-2 py-0.5 rounded-full">{count}</span>
</div>
<div className="space-y-2 max-h-[70vh] overflow-y-auto">{children}</div>
</div>
);
const DebtCaseCard: React.FC<{ item: LegalCourtCase; onClick: () => void }> = ({ item, onClick }) => {
const isEnforcement = item.status === 'enforcement';
const isLitigation = item.status === 'litigation';
return (
<div
onClick={onClick}
className="bg-white p-5 rounded-[2.5rem] border border-slate-200 shadow-sm hover:shadow-md hover:border-primary-300 transition-all flex flex-col md:flex-row gap-6 group cursor-pointer"
>
<div className="flex items-center gap-4 flex-1">
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center ${isEnforcement ? 'bg-amber-50 text-amber-600' : isLitigation ? 'bg-blue-50 text-blue-600' : 'bg-slate-50 text-slate-400'}`}>
{isEnforcement ? <HandCoins className="w-7 h-7"/> : <Gavel className="w-7 h-7"/>}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase tracking-tighter ${isEnforcement ? 'bg-amber-100 text-amber-600' : isLitigation ? 'bg-blue-100 text-blue-600' : 'bg-slate-100 text-slate-600'}`}>
{item.status === 'pre_trial' ? 'Претензия' : item.status === 'litigation' ? 'В суде' : item.status === 'enforcement' ? 'У приставов' : 'Закрыто'}
</span>
<span className="text-[10px] text-slate-400 font-bold uppercase">{item.caseNumber}</span>
</div>
<h4 className="font-black text-slate-800 text-base group-hover:text-primary-600 transition-colors truncate">{item.debtorName}</h4>
<p className="text-[10px] text-slate-500 font-bold uppercase mt-1 flex items-center gap-1">
<Landmark className="w-3 h-3"/> {item.address}
</p>
</div>
</div>
<div className="flex items-center justify-between md:justify-end gap-8 border-t md:border-t-0 border-slate-50 pt-4 md:pt-0">
<div className="text-right">
<p className="text-[9px] text-slate-400 font-black uppercase mb-1">Сумма долга</p>
<p className="text-sm font-black text-slate-900">{item.amount.toLocaleString()} </p>
{item.recoveredAmount && item.recoveredAmount > 0 && (
<p className="text-[10px] font-black text-emerald-600 mt-0.5 flex items-center justify-end gap-1">
<TrendingUp className="w-3 h-3"/> {item.recoveredAmount.toLocaleString()}
</p>
)}
</div>
<div className="text-right min-w-[120px]">
{isEnforcement ? (
<div>
<p className="text-[9px] text-amber-600 font-black uppercase mb-1">Статус ФССП</p>
<p className="text-[11px] font-bold text-slate-700">{item.fsspStatus}</p>
</div>
) : (
<div>
<p className="text-[9px] text-slate-400 font-black uppercase mb-1">Заседание</p>
<p className="text-[11px] font-bold text-slate-700">{item.nextHearingDate || 'Не назначено'}</p>
</div>
)}
</div>
<div className="flex gap-1">
<button
onClick={(e) => e.stopPropagation()}
className="p-2.5 bg-slate-50 text-slate-400 rounded-xl hover:text-primary-600 hover:bg-primary-50 transition-colors"
>
<MessageSquare className="w-5 h-5"/>
</button>
<button
onClick={(e) => {
e.stopPropagation();
onClick();
}}
className="p-2.5 bg-slate-50 text-slate-400 rounded-xl hover:text-primary-600 transition-colors"
>
<ChevronRight className="w-5 h-5"/>
</button>
</div>
</div>
</div>
);
};
const FSSP_STAGE_ORDER: FsspStage[] = ['writ_submitted', 'ip_initiated', 'bank_requests', 'money_on_deposit', 'transferred_to_uk'];
const FSSP_STAGE_LABELS: Record<FsspStage, string> = {
writ_submitted: 'ИЛ предъявлен',
ip_initiated: 'ИП возбуждено',
bank_requests: 'Запросы в банки',
money_on_deposit: 'Деньги на депозите',
transferred_to_uk: 'Перевод в УК'
};
const BailiffActionCard: React.FC<{ item: LegalCourtCase; onClick: () => void }> = ({ item, onClick }) => {
const lastAction = item.fsspLastActionDate ? new Date(item.fsspLastActionDate) : new Date();
const daysSinceLastAction = Math.floor((new Date().getTime() - lastAction.getTime()) / (1000 * 3600 * 24));
const isStuck = daysSinceLastAction > 20;
const currentStageIndex = item.fsspStage ? FSSP_STAGE_ORDER.indexOf(item.fsspStage) : -1;
return (
<div
onClick={onClick}
className="bg-white p-6 rounded-[2.5rem] border border-slate-200 shadow-sm hover:border-amber-400 transition-all group cursor-pointer"
>
<div className="flex flex-col md:flex-row justify-between gap-6">
<div className="flex items-center gap-4 flex-1">
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center ${isStuck ? 'bg-red-50 text-red-600' : 'bg-amber-50 text-amber-600'}`}>
<CircleDollarSign className="w-8 h-8"/>
</div>
<div>
<h4 className="font-black text-slate-800 text-base">{item.debtorName}</h4>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter mt-0.5">{item.address}</p>
<div className="flex items-center gap-3 mt-3 flex-wrap">
{item.enforcementNumber && (
<div className="flex items-center gap-1 text-[10px] font-black text-slate-600 uppercase bg-slate-50 px-2 py-0.5 rounded border border-slate-100">
ИП: {item.enforcementNumber}
</div>
)}
<div className="flex items-center gap-1 text-[10px] font-black text-slate-500 uppercase bg-slate-50 px-2 py-0.5 rounded border border-slate-100">
<Landmark className="w-3 h-3"/> Пристав: {item.bailiffName || '—'}
</div>
<div className={`flex items-center gap-1 text-[10px] font-black uppercase px-2 py-0.5 rounded border ${isStuck ? 'bg-red-50 text-red-600 border-red-100' : 'bg-emerald-50 text-emerald-600 border-emerald-100'}`}>
<Clock className="w-3 h-3"/> {daysSinceLastAction} дн. без действий
</div>
</div>
</div>
</div>
<div className="flex items-center justify-between md:justify-end gap-10">
<div className="text-right">
<p className="text-[9px] text-slate-400 font-black uppercase mb-1">Сумма у ФССП</p>
<p className="text-lg font-black text-amber-600">{item.amountAtBailiffs?.toLocaleString()} </p>
<p className="text-[9px] text-slate-400 mt-1 font-bold">Из {item.amount.toLocaleString()} присужденных</p>
</div>
<div className="flex flex-col gap-2">
<button
onClick={(e) => { e.stopPropagation(); onClick(); }}
className="bg-amber-500 text-white px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-amber-500/20 active:scale-95 transition-all flex items-center gap-2"
>
<Receipt className="w-3.5 h-3.5"/> Ходатайство
</button>
<button
onClick={(e) => { e.stopPropagation(); onClick(); }}
className="bg-slate-900 text-white px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest active:scale-95 transition-all flex items-center gap-2"
>
<CheckCircle2 className="w-3.5 h-3.5"/> Сверка прихода
</button>
</div>
</div>
</div>
<div className="mt-6 pt-6 border-t border-slate-50 flex items-center gap-4">
<span className="text-[8px] font-black text-slate-400 uppercase tracking-widest shrink-0">Цепочка взыскания:</span>
<div className="flex items-center gap-1 flex-1 min-w-0">
{FSSP_STAGE_ORDER.map((stage, idx) => {
const done = currentStageIndex > idx || (currentStageIndex === idx && stage === 'transferred_to_uk');
const current = currentStageIndex === idx && stage !== 'transferred_to_uk';
return (
<div
key={stage}
className={`h-1.5 rounded-full flex-1 min-w-0 ${done ? 'bg-emerald-500' : current ? 'bg-amber-400 animate-pulse' : 'bg-slate-100'}`}
title={FSSP_STAGE_LABELS[stage]}
/>
);
})}
</div>
</div>
</div>
);
};
// Bailiff Interaction Modal Component (inline version)
interface BailiffInteractionModalComponentProps {
courtCase: LegalCourtCase;
onClose: () => void;
onUpdate: (updatedCase: LegalCourtCase) => void;
}
const BailiffInteractionModalComponent: React.FC<BailiffInteractionModalComponentProps> = ({ courtCase, onUpdate, onClose }) => {
const [loading, setLoading] = useState(false);
const [comments, setComments] = useState<any[]>([]);
const [newComment, setNewComment] = useState('');
const [commentAuthor, setCommentAuthor] = useState('');
const [formData, setFormData] = useState({
fsspStatus: courtCase.fsspStatus || '',
bailiffName: courtCase.bailiffName || '',
fsspLastActionDate: courtCase.fsspLastActionDate || '',
amountAtBailiffs: courtCase.amountAtBailiffs || 0,
recoveredAmount: courtCase.recoveredAmount || 0,
enforcementNumber: courtCase.enforcementNumber || '',
enforcementStartDate: courtCase.enforcementStartDate || '',
fsspStage: courtCase.fsspStage || ''
});
const [newPetition, setNewPetition] = useState({
type: 'request',
text: '',
date: new Date().toISOString().split('T')[0]
});
useEffect(() => {
loadComments();
}, [courtCase.id]);
const loadComments = async () => {
try {
const response = await authFetch(`/api/legal/court-cases/${courtCase.id}/comments`);
if (response.ok) {
const data = await response.json();
// Фильтруем комментарии, связанные с приставами
const bailiffComments = (data || []).filter((c: any) =>
c.comment.toLowerCase().includes('ходатайство') ||
c.comment.toLowerCase().includes('пристав') ||
c.comment.toLowerCase().includes('фссп')
);
setComments(bailiffComments);
}
} catch (error) {
console.error('Error loading comments:', error);
}
};
const handleSave = async () => {
setLoading(true);
try {
const response = await authFetch(`/api/legal/court-cases/${courtCase.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...formData,
changedBy: commentAuthor || 'System'
})
});
if (response.ok) {
const updated = await response.json();
onUpdate(updated);
alert('Данные обновлены');
} else {
alert('Ошибка при сохранении');
}
} catch (error) {
console.error('Error saving bailiff data:', error);
alert('Ошибка при сохранении');
} finally {
setLoading(false);
}
};
const handleReconciliation = async () => {
if (!confirm(`Подтвердите сверку прихода. Сумма ${formData.amountAtBailiffs} ₽ будет перенесена из "У приставов" в "Взыскано".`)) {
return;
}
setLoading(true);
try {
const newRecoveredAmount = (courtCase.recoveredAmount || 0) + (formData.amountAtBailiffs || 0);
const response = await authFetch(`/api/legal/court-cases/${courtCase.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recoveredAmount: newRecoveredAmount,
amountAtBailiffs: 0,
changedBy: commentAuthor || 'System'
})
});
if (response.ok) {
const updated = await response.json();
onUpdate(updated);
setFormData({
...formData,
recoveredAmount: newRecoveredAmount,
amountAtBailiffs: 0
});
// Добавляем комментарий о сверке
if (commentAuthor) {
await authFetch(`/api/legal/court-cases/${courtCase.id}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
author: commentAuthor,
comment: `Сверка прихода выполнена. Перенесено ${formData.amountAtBailiffs} ₽ из ФССП в взысканные средства.`
})
});
await loadComments();
}
alert('Сверка прихода выполнена');
} else {
alert('Ошибка при сверке прихода');
}
} catch (error) {
console.error('Error reconciling payment:', error);
alert('Ошибка при сверке прихода');
} finally {
setLoading(false);
}
};
const handleAddPetition = async () => {
if (!newPetition.text.trim() || !commentAuthor.trim()) {
alert('Заполните текст ходатайства и автора');
return;
}
setLoading(true);
try {
const response = await authFetch(`/api/legal/court-cases/${courtCase.id}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
author: commentAuthor,
comment: `[ХОДАТАЙСТВО] ${newPetition.text}`
})
});
if (response.ok) {
setNewPetition({ ...newPetition, text: '' });
await loadComments();
alert('Ходатайство добавлено');
} else {
alert('Ошибка при добавлении ходатайства');
}
} catch (error) {
console.error('Error adding petition:', error);
alert('Ошибка при добавлении ходатайства');
} finally {
setLoading(false);
}
};
const lastAction = courtCase.fsspLastActionDate ? new Date(courtCase.fsspLastActionDate) : null;
const daysSinceLastAction = lastAction
? Math.floor((new Date().getTime() - lastAction.getTime()) / (1000 * 3600 * 24))
: null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-y-auto">
<div className="bg-white rounded-2xl p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-6">
<div>
<h3 className="text-xl font-black text-slate-800">Взаимодействие с приставами</h3>
<p className="text-sm text-slate-500 mt-1">{courtCase.caseNumber}</p>
</div>
<button onClick={onClose} className="p-2 text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-6">
{/* Информация о приставах */}
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<h4 className="text-sm font-black text-amber-800 uppercase mb-4 flex items-center gap-2">
<CircleDollarSign className="w-5 h-5" /> Информация о приставах
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Номер ИП</label>
<input
type="text"
value={formData.enforcementNumber}
onChange={(e) => setFormData({ ...formData, enforcementNumber: e.target.value })}
placeholder="Номер исполнительного производства"
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white"
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Дата возбуждения ИП</label>
<input
type="date"
value={formData.enforcementStartDate}
onChange={(e) => setFormData({ ...formData, enforcementStartDate: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white"
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Этап ФССП</label>
<select
value={formData.fsspStage}
onChange={(e) => setFormData({ ...formData, fsspStage: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white"
>
<option value=""> не выбран </option>
{FSSP_STAGE_ORDER.map(stage => (
<option key={stage} value={stage}>{FSSP_STAGE_LABELS[stage]}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Статус ФССП (текст)</label>
<input
type="text"
value={formData.fsspStatus}
onChange={(e) => setFormData({ ...formData, fsspStatus: e.target.value })}
placeholder="Например: Деньги на депозите"
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white"
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">ФИО пристава</label>
<input
type="text"
value={formData.bailiffName}
onChange={(e) => setFormData({ ...formData, bailiffName: e.target.value })}
placeholder="Иванов И.И."
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white"
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Дата последнего действия</label>
<input
type="date"
value={formData.fsspLastActionDate}
onChange={(e) => setFormData({ ...formData, fsspLastActionDate: e.target.value })}
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white"
/>
{daysSinceLastAction !== null && (
<p className={`text-xs mt-1 font-bold ${daysSinceLastAction > 20 ? 'text-red-600' : 'text-slate-600'}`}>
{daysSinceLastAction} дней без действий
</p>
)}
</div>
</div>
</div>
{/* Финансовые данные */}
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
<h4 className="text-sm font-black text-slate-800 uppercase mb-4 flex items-center gap-2">
<DollarSign className="w-5 h-5" /> Финансовые данные
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Сумма у приставов</label>
<input
type="number"
value={formData.amountAtBailiffs}
onChange={(e) => setFormData({ ...formData, amountAtBailiffs: parseFloat(e.target.value) || 0 })}
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white"
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Взыскано (УК)</label>
<input
type="number"
value={formData.recoveredAmount}
onChange={(e) => setFormData({ ...formData, recoveredAmount: parseFloat(e.target.value) || 0 })}
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white"
/>
</div>
</div>
<button
onClick={handleReconciliation}
disabled={loading || !formData.amountAtBailiffs || formData.amountAtBailiffs <= 0}
className="mt-4 w-full px-4 py-2.5 bg-slate-900 text-white rounded-xl text-xs font-black uppercase hover:bg-slate-800 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
>
<CheckCircle2 className="w-4 h-4" /> Сверка прихода
</button>
</div>
{/* Ходатайства */}
<div className="border border-slate-200 rounded-xl p-4">
<h4 className="text-sm font-black text-slate-800 uppercase mb-4 flex items-center gap-2">
<Receipt className="w-5 h-5" /> Ходатайства
</h4>
<div className="space-y-3 mb-4 max-h-48 overflow-y-auto">
{comments.filter(c => c.comment.toLowerCase().includes('ходатайство')).length > 0 ? (
comments.filter(c => c.comment.toLowerCase().includes('ходатайство')).map((comment: any) => (
<div key={comment.id} className="bg-amber-50 p-3 rounded-xl border border-amber-200">
<div className="flex justify-between items-start mb-1">
<p className="text-xs font-black text-amber-800">{comment.author}</p>
<p className="text-xs text-slate-400">
{new Date(comment.createdAt).toLocaleDateString('ru-RU')}
</p>
</div>
<p className="text-sm text-slate-700">{comment.comment.replace('[ХОДАТАЙСТВО]', '').trim()}</p>
</div>
))
) : (
<div className="text-center py-8 text-slate-400">
<Receipt className="w-8 h-8 mx-auto mb-2 opacity-20" />
<p className="text-xs font-bold uppercase">Ходатайств пока нет</p>
</div>
)}
</div>
<div className="border-t border-slate-200 pt-4">
<div className="space-y-3">
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Автор</label>
<input
type="text"
value={commentAuthor}
onChange={(e) => setCommentAuthor(e.target.value)}
placeholder="Ваше имя"
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500"
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Текст ходатайства</label>
<textarea
value={newPetition.text}
onChange={(e) => setNewPetition({ ...newPetition, text: e.target.value })}
rows={3}
placeholder="Введите текст ходатайства..."
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500"
/>
</div>
<button
onClick={handleAddPetition}
disabled={loading || !newPetition.text.trim() || !commentAuthor.trim()}
className="w-full px-4 py-2 bg-amber-500 text-white rounded-xl text-xs font-black uppercase hover:bg-amber-600 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
>
<Receipt className="w-4 h-4" /> {loading ? 'Добавление...' : 'Создать ходатайство'}
</button>
</div>
</div>
</div>
{/* История взаимодействий */}
<div className="border border-slate-200 rounded-xl p-4">
<h4 className="text-sm font-black text-slate-800 uppercase mb-4 flex items-center gap-2">
<History className="w-5 h-5" /> История взаимодействий
</h4>
<div className="space-y-2 max-h-64 overflow-y-auto">
{comments.length > 0 ? (
comments.map((comment: any) => (
<div key={comment.id} className="bg-slate-50 p-3 rounded-xl border border-slate-200">
<div className="flex justify-between items-start mb-1">
<p className="text-xs font-black text-slate-800">{comment.author}</p>
<p className="text-xs text-slate-400">
{new Date(comment.createdAt).toLocaleDateString('ru-RU')}
</p>
</div>
<p className="text-sm text-slate-700">{comment.comment}</p>
</div>
))
) : (
<div className="text-center py-8 text-slate-400">
<History className="w-8 h-8 mx-auto mb-2 opacity-20" />
<p className="text-xs font-bold uppercase">История пуста</p>
</div>
)}
</div>
</div>
{/* Кнопки действий */}
<div className="flex gap-3 pt-4 border-t border-slate-200">
<button
onClick={handleSave}
disabled={loading}
className="flex-1 px-4 py-2.5 bg-primary-600 text-white rounded-xl text-xs font-black uppercase hover:bg-primary-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
>
<Save className="w-4 h-4" /> {loading ? 'Сохранение...' : 'Сохранить изменения'}
</button>
<button
onClick={onClose}
className="px-4 py-2.5 bg-slate-100 text-slate-700 rounded-xl text-xs font-black uppercase hover:bg-slate-200 transition-colors"
>
Закрыть
</button>
</div>
</div>
</div>
</div>
);
};