916 lines
53 KiB
TypeScript
Executable File
916 lines
53 KiB
TypeScript
Executable File
|
||
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>
|
||
);
|
||
};
|