Initial commit MKD fixes
This commit is contained in:
915
components/legal/DebtRecoveryPipeline.tsx
Executable file
915
components/legal/DebtRecoveryPipeline.tsx
Executable file
@@ -0,0 +1,915 @@
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user