Files
mkd/components/legal/PreTrialWork.tsx

1327 lines
54 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
import React, { useState, useEffect, useMemo } from 'react';
import {
Phone,
Mail,
MapPin,
Calendar,
DollarSign,
Search,
Filter,
Plus,
X,
Clock,
CheckCircle2,
AlertCircle,
Gavel,
FileText,
User,
MessageSquare,
ChevronDown,
ChevronUp
} from 'lucide-react';
import { authFetch, backendApi } from '../../services/apiClient';
import { PreTrialWork as PreTrialWorkType, PreTrialAction, PromisedPayment } from '../../types';
export const PreTrialWork: React.FC = () => {
const [works, setWorks] = useState<PreTrialWorkType[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [assignedToFilter, setAssignedToFilter] = useState<string>('all'); // 'all' | 'mine' | employee name
const [sortBy, setSortBy] = useState<'updatedAt' | 'debtAmount' | 'promisedDate'>('updatedAt');
const [currentUserName, setCurrentUserName] = useState<string | null>(null);
const [employeesList, setEmployeesList] = useState<Array<{ id: string; name: string }>>([]);
const [expandedWorkId, setExpandedWorkId] = useState<number | null>(null);
const [showActionModal, setShowActionModal] = useState(false);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [showTransferModal, setShowTransferModal] = useState(false);
const [selectedWork, setSelectedWork] = useState<PreTrialWorkType | null>(null);
const [showAddDebtorModal, setShowAddDebtorModal] = useState(false);
useEffect(() => {
backendApi.getMe().then(u => setCurrentUserName(u?.name ?? null)).catch(() => setCurrentUserName(null));
backendApi.getEmployeesList().then(list => setEmployeesList(Array.isArray(list) ? list : [])).catch(() => setEmployeesList([]));
}, []);
useEffect(() => {
loadWorks();
}, [statusFilter, search, assignedToFilter]);
const loadWorks = async () => {
try {
setLoading(true);
setError(null);
const params = new URLSearchParams();
if (statusFilter !== 'all') {
params.append('status', statusFilter);
}
if (search) {
params.append('search', search);
}
if (assignedToFilter === 'mine' && currentUserName) {
params.append('assignedTo', currentUserName);
} else if (assignedToFilter && assignedToFilter !== 'all') {
params.append('assignedTo', assignedToFilter);
}
const response = await authFetch(`/api/legal/pre-trial-work?${params}`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
const data = await response.json();
// Убеждаемся, что data - это массив и обрабатываем данные
if (Array.isArray(data)) {
// Фильтруем работы с валидными данными должника
const validWorks = data.filter((work: any) => work && work.debtor);
setWorks(validWorks);
} else {
setWorks([]);
}
} catch (error) {
console.error('Error loading pre-trial work:', error);
setError(error instanceof Error ? error.message : 'Ошибка загрузки данных');
setWorks([]);
} finally {
setLoading(false);
}
};
const handleAddAction = (work: PreTrialWorkType) => {
setSelectedWork(work);
setShowActionModal(true);
};
const handleAddPayment = (work: PreTrialWorkType) => {
setSelectedWork(work);
setShowPaymentModal(true);
};
const handleTransferToCourt = (work: PreTrialWorkType) => {
setSelectedWork(work);
setShowTransferModal(true);
};
const getStatusColor = (status: string) => {
switch (status) {
case 'new': return 'bg-slate-100 text-slate-600';
case 'in_progress': return 'bg-blue-100 text-blue-600';
case 'promised_payment': return 'bg-amber-100 text-amber-600';
case 'transferred_to_court': return 'bg-purple-100 text-purple-600';
case 'resolved': return 'bg-emerald-100 text-emerald-600';
default: return 'bg-slate-100 text-slate-600';
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case 'new': return 'Новое';
case 'in_progress': return 'В работе';
case 'promised_payment': return 'Обещана оплата';
case 'transferred_to_court': return 'Передано в суд';
case 'resolved': return 'Решено';
default: return status;
}
};
const getActionIcon = (type: string) => {
switch (type) {
case 'call': return Phone;
case 'letter': return Mail;
case 'visit': return MapPin;
default: return FileText;
}
};
const getActionLabel = (type: string) => {
switch (type) {
case 'call': return 'Звонок';
case 'letter': return 'Письмо';
case 'visit': return 'Визит';
default: return type;
}
};
const today = useMemo(() => new Date().toISOString().slice(0, 10), []);
const attentionItems = useMemo(() => {
const overdue: Array<{ work: PreTrialWorkType; payment: PromisedPayment }> = [];
const noAssignee: PreTrialWorkType[] = [];
for (const work of works) {
if (!work.debtor) continue;
if (
work.status !== 'transferred_to_court' &&
work.status !== 'resolved' &&
!work.assignedTo
) {
noAssignee.push(work);
}
const payments = work.promisedPayments || [];
for (const p of payments) {
if (!p.isPaid && p.promisedDate < today) {
overdue.push({ work, payment: p });
}
}
}
return { overdue, noAssignee };
}, [works, today]);
const sortedWorks = useMemo(() => {
const list = works.filter((w): w is PreTrialWorkType & { debtor: NonNullable<PreTrialWorkType['debtor']> } => !!w?.debtor);
if (sortBy === 'updatedAt') {
return [...list].sort((a, b) => {
const da = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
const db = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
return db - da;
});
}
if (sortBy === 'debtAmount') {
return [...list].sort((a, b) => (b.debtor?.debtAmount ?? 0) - (a.debtor?.debtAmount ?? 0));
}
if (sortBy === 'promisedDate') {
const getNearest = (w: typeof list[0]) => {
const payments = w.promisedPayments?.filter(p => !p.isPaid) ?? [];
if (payments.length === 0) return '';
return payments.map(p => p.promisedDate).sort()[0] ?? '';
};
return [...list].sort((a, b) => {
const pa = getNearest(a);
const pb = getNearest(b);
if (!pa) return 1;
if (!pb) return -1;
return pa.localeCompare(pb);
});
}
return list;
}, [works, sortBy]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-slate-400">Загрузка...</div>
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] space-y-4">
<div className="text-red-600 font-bold">Ошибка загрузки данных</div>
<div className="text-sm text-slate-600">{error}</div>
<button
onClick={loadWorks}
className="px-4 py-2 bg-primary-600 text-white rounded-xl text-xs font-black uppercase hover:bg-primary-700 transition-colors"
>
Попробовать снова
</button>
</div>
);
}
return (
<div className="space-y-6 animate-fade-in">
{/* Header with Search and Filters */}
<div className="flex flex-col md:flex-row gap-4">
<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-2.5 bg-white border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm"
/>
</div>
<div className="flex flex-wrap gap-2">
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm"
>
<option value="all">Все статусы</option>
<option value="new">Новые</option>
<option value="in_progress">В работе</option>
<option value="promised_payment">Обещана оплата</option>
<option value="transferred_to_court">В суде</option>
<option value="resolved">Решено</option>
</select>
<select
value={assignedToFilter}
onChange={(e) => setAssignedToFilter(e.target.value)}
className="px-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm"
title="Ответственный"
>
<option value="all">Все ответственные</option>
<option value="mine">Мои должники</option>
{employeesList.map((emp) => (
<option key={emp.id} value={emp.name}>{emp.name}</option>
))}
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'updatedAt' | 'debtAmount' | 'promisedDate')}
className="px-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm"
title="Сортировка"
>
<option value="updatedAt">По дате обновления</option>
<option value="debtAmount">По сумме долга</option>
<option value="promisedDate">По сроку обещанной оплаты</option>
</select>
<button className="p-2.5 bg-white border border-slate-200 rounded-xl text-slate-500 hover:bg-slate-50" title="Фильтры">
<Filter className="w-5 h-5" />
</button>
<button
onClick={() => setShowAddDebtorModal(true)}
className="bg-primary-600 text-white px-4 py-2.5 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>
</div>
{/* Statistics */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<p className="text-[10px] font-black text-slate-400 uppercase mb-1">Всего дел</p>
<p className="text-2xl font-black text-slate-800">{works.length}</p>
</div>
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<p className="text-[10px] font-black text-slate-400 uppercase mb-1">В работе</p>
<p className="text-2xl font-black text-blue-600">
{works.filter(w => w.status === 'in_progress').length}
</p>
</div>
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<p className="text-[10px] font-black text-slate-400 uppercase mb-1">Обещана оплата</p>
<p className="text-2xl font-black text-amber-600">
{works.filter(w => w.status === 'promised_payment').length}
</p>
</div>
<div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<p className="text-[10px] font-black text-slate-400 uppercase mb-1">В суде</p>
<p className="text-2xl font-black text-purple-600">
{works.filter(w => w.status === 'transferred_to_court').length}
</p>
</div>
</div>
{/* Требует внимания */}
{(attentionItems.overdue.length > 0 || attentionItems.noAssignee.length > 0) && (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 shadow-sm">
<h3 className="text-sm font-black text-amber-900 uppercase mb-3 flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
Требует внимания
</h3>
<ul className="space-y-2 text-sm">
{attentionItems.overdue.length > 0 && (
<li className="text-amber-800">
<span className="font-bold">Просроченные обещанные платежи:</span>{' '}
{attentionItems.overdue.length}{' '}
{attentionItems.overdue.length === 1 ? 'дело' : 'дел'}
{attentionItems.overdue.slice(0, 3).map(({ work, payment }) => (
<span key={`${work.id}-${payment.id}`} className="block text-xs mt-1 text-amber-700">
{work.debtor?.address}, кв. {work.debtor?.apartment} {new Date(payment.promisedDate).toLocaleDateString('ru-RU')}, {payment.promisedAmount?.toLocaleString()}
</span>
))}
{attentionItems.overdue.length > 3 && (
<span className="block text-xs text-amber-600">и ещё {attentionItems.overdue.length - 3}</span>
)}
</li>
)}
{attentionItems.noAssignee.length > 0 && (
<li className="text-amber-800">
<span className="font-bold">Без ответственного:</span> {attentionItems.noAssignee.length}{' '}
{attentionItems.noAssignee.length === 1 ? 'дело' : 'дел'}
{attentionItems.noAssignee.slice(0, 3).map((w) => (
<span key={w.id} className="block text-xs mt-1 text-amber-700">
{w.debtor?.address}, кв. {w.debtor?.apartment}
</span>
))}
{attentionItems.noAssignee.length > 3 && (
<span className="block text-xs text-amber-600">и ещё {attentionItems.noAssignee.length - 3}</span>
)}
</li>
)}
</ul>
</div>
)}
{/* Works List */}
<div className="space-y-4">
{sortedWorks.length === 0 ? (
<div className="bg-white p-12 rounded-xl border border-slate-200 text-center">
<p className="text-slate-400">Нет дел для отображения</p>
</div>
) : (
sortedWorks
.map((work) => {
if (!work.debtor) return null;
return (
<WorkCard
key={work.id}
work={work}
expanded={expandedWorkId === work.id}
onToggleExpand={() => setExpandedWorkId(expandedWorkId === work.id ? null : work.id)}
onAddAction={() => handleAddAction(work)}
onAddPayment={() => handleAddPayment(work)}
onTransferToCourt={() => handleTransferToCourt(work)}
getStatusColor={getStatusColor}
getStatusLabel={getStatusLabel}
getActionIcon={getActionIcon}
getActionLabel={getActionLabel}
onReload={loadWorks}
employeesList={employeesList}
onAssignChange={(workId, assignedTo) => {
backendApi.updatePreTrialWork(workId, { assignedTo: assignedTo || undefined }).then(() => {
window.dispatchEvent(new CustomEvent('mkd-legal-changed'));
loadWorks();
}).catch(() => alert('Ошибка при смене ответственного'));
}}
/>
);
})
.filter(Boolean) // Убираем null значения
)}
</div>
{/* Action Modal */}
{showActionModal && selectedWork && (
<ActionModal
work={selectedWork}
onClose={() => {
setShowActionModal(false);
setSelectedWork(null);
}}
onSuccess={() => {
window.dispatchEvent(new CustomEvent('mkd-legal-changed'));
loadWorks();
setShowActionModal(false);
setSelectedWork(null);
}}
/>
)}
{/* Payment Modal */}
{showPaymentModal && selectedWork && (
<PaymentModal
work={selectedWork}
onClose={() => {
setShowPaymentModal(false);
setSelectedWork(null);
}}
onSuccess={() => {
window.dispatchEvent(new CustomEvent('mkd-legal-changed'));
loadWorks();
setShowPaymentModal(false);
setSelectedWork(null);
}}
/>
)}
{/* Transfer Modal */}
{showTransferModal && selectedWork && (
<TransferModal
work={selectedWork}
onClose={() => {
setShowTransferModal(false);
setSelectedWork(null);
}}
onSuccess={() => {
window.dispatchEvent(new CustomEvent('mkd-legal-changed'));
loadWorks();
setShowTransferModal(false);
setSelectedWork(null);
}}
/>
)}
{/* Add Debtor Modal */}
{showAddDebtorModal && (
<AddDebtorModal
onClose={() => setShowAddDebtorModal(false)}
onSuccess={() => {
window.dispatchEvent(new CustomEvent('mkd-legal-changed'));
loadWorks();
setShowAddDebtorModal(false);
}}
/>
)}
</div>
);
};
// Add Debtor Modal Component
interface AddDebtorModalProps {
onClose: () => void;
onSuccess: () => void;
}
const AddDebtorModal: React.FC<AddDebtorModalProps> = ({ onClose, onSuccess }) => {
const [loading, setLoading] = useState(false);
const [buildings, setBuildings] = useState<{ id: string; address?: string; districtId?: string }[]>([]);
const [formData, setFormData] = useState({
buildingId: '',
apartment: '',
debtorName: '',
phone: '',
email: '',
address: '',
debtAmount: '',
debtMonths: ''
});
useEffect(() => {
authFetch('/api/buildings')
.then(res => res.ok ? res.json() : [])
.then(data => setBuildings(Array.isArray(data) ? data : []))
.catch(() => setBuildings([]));
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await authFetch('/api/legal/debtors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
buildingId: formData.buildingId || undefined,
apartment: formData.apartment,
debtorName: formData.debtorName || undefined,
phone: formData.phone || undefined,
email: formData.email || undefined,
address: formData.address,
debtAmount: parseFloat(formData.debtAmount),
debtMonths: parseInt(formData.debtMonths)
})
});
if (response.ok) {
onSuccess();
} else {
const error = await response.json();
alert(error.error || 'Ошибка при создании должника');
}
} catch (error) {
console.error('Error creating debtor:', error);
alert('Ошибка при создании должника');
} finally {
setLoading(false);
}
};
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-md w-full">
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-black text-slate-800">Добавить должника</h3>
<button onClick={onClose} className="p-2 text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Здание (объект МКД)</label>
<select
value={formData.buildingId}
onChange={(e) => setFormData({ ...formData, buildingId: 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-primary-500"
>
<option value=""> не выбрано </option>
{buildings.map(b => (
<option key={b.id} value={b.id}>
{(b.passport?.address ?? b.address) || b.id}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Адрес *</label>
<input
type="text"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: 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-primary-500"
required
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Квартира *</label>
<input
type="text"
value={formData.apartment}
onChange={(e) => setFormData({ ...formData, apartment: 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-primary-500"
required
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">ФИО должника</label>
<input
type="text"
value={formData.debtorName}
onChange={(e) => setFormData({ ...formData, debtorName: 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-primary-500"
/>
</div>
<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="tel"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: 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-primary-500"
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Email</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: 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-primary-500"
/>
</div>
</div>
<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.debtAmount}
onChange={(e) => setFormData({ ...formData, debtAmount: 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-primary-500"
min="0"
step="0.01"
required
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Месяцев долга *</label>
<input
type="number"
value={formData.debtMonths}
onChange={(e) => setFormData({ ...formData, debtMonths: 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-primary-500"
min="1"
required
/>
</div>
</div>
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={onClose}
className="flex-1 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>
<button
type="submit"
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"
>
{loading ? 'Сохранение...' : 'Добавить'}
</button>
</div>
</form>
</div>
</div>
);
};
// Одна строка обещанной оплаты с кнопкой «Отметить оплачено»
const PromisedPaymentRow: React.FC<{
payment: PromisedPayment;
onMarkPaid: () => void;
}> = ({ payment, onMarkPaid }) => {
const [loading, setLoading] = useState(false);
const handleMarkPaid = async () => {
setLoading(true);
try {
const res = await authFetch(`/api/legal/promised-payments/${payment.id}/mark-paid`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
actualPaymentDate: new Date().toISOString().slice(0, 10),
actualPaymentAmount: payment.promisedAmount
})
});
if (res.ok) onMarkPaid();
else alert('Ошибка при отметке оплаты');
} catch (e) {
console.error(e);
alert('Ошибка при отметке оплаты');
} finally {
setLoading(false);
}
};
return (
<div
className={`p-3 rounded-xl border-2 ${
payment.isPaid
? 'bg-emerald-50 border-emerald-200'
: new Date(payment.promisedDate) < new Date()
? 'bg-red-50 border-red-200'
: 'bg-amber-50 border-amber-200'
}`}
>
<div className="flex flex-wrap items-center justify-between gap-2 mb-2">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-slate-600" />
<span className="text-sm font-black text-slate-800">
{new Date(payment.promisedDate).toLocaleDateString('ru-RU')}
</span>
</div>
<div className="flex items-center gap-2">
<DollarSign className="w-4 h-4 text-slate-600" />
<span className="text-sm font-black text-slate-800">
{payment.promisedAmount.toLocaleString()}
</span>
</div>
{payment.isPaid ? (
<div className="flex items-center gap-1 text-emerald-600">
<CheckCircle2 className="w-4 h-4" />
<span className="text-xs font-black">Оплачено</span>
</div>
) : new Date(payment.promisedDate) < new Date() ? (
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 text-red-600">
<AlertCircle className="w-4 h-4" />
<span className="text-xs font-black">Просрочено</span>
</div>
<button
type="button"
onClick={handleMarkPaid}
disabled={loading}
className="text-[10px] font-black uppercase px-2.5 py-1.5 rounded-lg bg-emerald-100 text-emerald-700 hover:bg-emerald-200 disabled:opacity-50 transition-colors"
>
{loading ? 'Сохранение...' : 'Отметить оплачено'}
</button>
</div>
) : (
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 text-amber-600">
<Clock className="w-4 h-4" />
<span className="text-xs font-black">Ожидается</span>
</div>
<button
type="button"
onClick={handleMarkPaid}
disabled={loading}
className="text-[10px] font-black uppercase px-2.5 py-1.5 rounded-lg bg-emerald-100 text-emerald-700 hover:bg-emerald-200 disabled:opacity-50 transition-colors"
>
{loading ? 'Сохранение...' : 'Отметить оплачено'}
</button>
</div>
)}
</div>
{payment.actualPaymentDate && (
<p className="text-xs text-slate-600 mt-1">
Фактически оплачено: {new Date(payment.actualPaymentDate).toLocaleDateString('ru-RU')}
{payment.actualPaymentAmount != null && ` - ${payment.actualPaymentAmount.toLocaleString()}`}
</p>
)}
</div>
);
};
interface WorkCardProps {
work: PreTrialWorkType;
expanded: boolean;
onToggleExpand: () => void;
onAddAction: () => void;
onAddPayment: () => void;
onTransferToCourt: () => void;
getStatusColor: (status: string) => string;
getStatusLabel: (status: string) => string;
getActionIcon: (type: string) => any;
getActionLabel: (type: string) => string;
onReload: () => void;
employeesList: Array<{ id: string; name: string }>;
onAssignChange: (workId: number, assignedTo: string | null) => void;
}
const WorkCard: React.FC<WorkCardProps> = ({
work,
expanded,
onToggleExpand,
onAddAction,
onAddPayment,
onTransferToCourt,
getStatusColor,
getStatusLabel,
getActionIcon,
getActionLabel,
onReload,
employeesList,
onAssignChange
}) => {
const debtor = work.debtor;
if (!debtor) {
console.warn('WorkCard: debtor is missing', work);
return null;
}
const actions = work.actions || [];
const payments = work.promisedPayments || [];
return (
<div className="bg-white rounded-[2rem] border border-slate-200 shadow-sm hover:shadow-md transition-all">
<div className="p-5">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex flex-wrap items-center gap-3 mb-2">
<div className={`px-3 py-1 rounded-full text-[10px] font-black uppercase ${getStatusColor(work.status)}`}>
{getStatusLabel(work.status)}
</div>
<div className="flex items-center gap-2">
<User className="w-3.5 h-3.5 text-slate-400 shrink-0" />
<select
value={work.assignedTo || ''}
onChange={(e) => onAssignChange(work.id, e.target.value || null)}
className="text-[10px] font-medium text-slate-600 bg-slate-50 border border-slate-200 rounded-lg px-2 py-1 outline-none focus:ring-1 focus:ring-primary-500 min-w-[120px]"
title="Ответственный"
>
<option value=""> не назначен </option>
{employeesList.map((emp) => (
<option key={emp.id} value={emp.name}>{emp.name}</option>
))}
</select>
</div>
</div>
<h3 className="text-lg font-black text-slate-800 mb-1">
{debtor.address}, кв. {debtor.apartment}
</h3>
{debtor.debtorName && (
<p className="text-sm text-slate-600 mb-2">{debtor.debtorName}</p>
)}
<div className="flex items-center gap-4 text-xs text-slate-500">
<div className="flex items-center gap-1.5">
<DollarSign className="w-4 h-4" />
<span className="font-black text-red-600">{debtor.debtAmount.toLocaleString()} </span>
</div>
<div className="flex items-center gap-1.5">
<Clock className="w-4 h-4" />
<span>{debtor.debtMonths} мес. долга</span>
</div>
{debtor.phone && (
<div className="flex items-center gap-1.5">
<Phone className="w-4 h-4" />
<span>{debtor.phone}</span>
</div>
)}
</div>
</div>
<button
onClick={onToggleExpand}
className="p-2 text-slate-400 hover:text-slate-600 transition-colors"
>
{expanded ? <ChevronUp className="w-5 h-5" /> : <ChevronDown className="w-5 h-5" />}
</button>
</div>
{/* Action Buttons */}
{work.status !== 'transferred_to_court' && work.status !== 'resolved' && (
<div className="flex flex-wrap gap-2 mb-4">
<button
onClick={onAddAction}
className="flex items-center gap-2 px-4 py-2 bg-primary-50 text-primary-600 rounded-xl text-xs font-black uppercase hover:bg-primary-100 transition-colors"
>
<Plus className="w-4 h-4" />
Добавить действие
</button>
<button
onClick={onAddPayment}
className="flex items-center gap-2 px-4 py-2 bg-amber-50 text-amber-600 rounded-xl text-xs font-black uppercase hover:bg-amber-100 transition-colors"
>
<Calendar className="w-4 h-4" />
Обещанная оплата
</button>
<button
onClick={onTransferToCourt}
className="flex items-center gap-2 px-4 py-2 bg-purple-50 text-purple-600 rounded-xl text-xs font-black uppercase hover:bg-purple-100 transition-colors"
>
<Gavel className="w-4 h-4" />
Передать в суд
</button>
</div>
)}
{/* Expanded Content */}
{expanded && (
<div className="space-y-4 pt-4 border-t border-slate-100">
{/* Actions History */}
{actions.length > 0 && (
<div>
<h4 className="text-sm font-black text-slate-700 mb-3 uppercase">История действий</h4>
<div className="space-y-2">
{actions.map((action) => {
const Icon = getActionIcon(action.actionType);
return (
<div key={action.id} className="flex items-start gap-3 p-3 bg-slate-50 rounded-xl">
<div className="p-2 bg-white rounded-lg">
<Icon className="w-4 h-4 text-slate-600" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-black text-slate-800">
{getActionLabel(action.actionType)}
</span>
<span className="text-[10px] text-slate-400">
{new Date(action.actionDate).toLocaleDateString('ru-RU')}
</span>
</div>
<p className="text-xs text-slate-600 mb-1">
Выполнил: {action.performedBy}
</p>
{action.result && (
<p className="text-xs text-slate-700">{action.result}</p>
)}
{action.notes && (
<p className="text-xs text-slate-500 mt-1">{action.notes}</p>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{/* Promised Payments */}
{payments.length > 0 && (
<div>
<h4 className="text-sm font-black text-slate-700 mb-3 uppercase">Обещанные оплаты</h4>
<div className="space-y-2">
{payments.map((payment) => (
<PromisedPaymentRow
key={payment.id}
payment={payment}
onMarkPaid={onReload}
/>
))}
</div>
</div>
)}
{/* Notes */}
{work.notes && (
<div>
<h4 className="text-sm font-black text-slate-700 mb-2 uppercase">Примечания</h4>
<p className="text-xs text-slate-600 bg-slate-50 p-3 rounded-xl">{work.notes}</p>
</div>
)}
</div>
)}
</div>
</div>
);
};
// Action Modal Component
interface ActionModalProps {
work: PreTrialWorkType;
onClose: () => void;
onSuccess: () => void;
}
const ActionModal: React.FC<ActionModalProps> = ({ work, onClose, onSuccess }) => {
const [actionType, setActionType] = useState<'call' | 'letter' | 'visit'>('call');
const [actionDate, setActionDate] = useState(new Date().toISOString().slice(0, 16));
const [performedBy, setPerformedBy] = useState('');
const [result, setResult] = useState('');
const [notes, setNotes] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await authFetch(`/api/legal/pre-trial-work/${work.id}/actions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
actionType,
actionDate: new Date(actionDate).toISOString(),
performedBy,
result,
notes
})
});
if (response.ok) {
onSuccess();
} else {
alert('Ошибка при добавлении действия');
}
} catch (error) {
console.error('Error adding action:', error);
alert('Ошибка при добавлении действия');
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-black text-slate-800">Добавить действие</h3>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Тип действия</label>
<select
value={actionType}
onChange={(e) => setActionType(e.target.value as any)}
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="call">Звонок</option>
<option value="letter">Письмо</option>
<option value="visit">Визит</option>
</select>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Дата и время</label>
<input
type="datetime-local"
value={actionDate}
onChange={(e) => setActionDate(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-primary-500"
required
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Выполнил</label>
<input
type="text"
value={performedBy}
onChange={(e) => setPerformedBy(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-primary-500"
required
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Результат</label>
<textarea
value={result}
onChange={(e) => setResult(e.target.value)}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500"
placeholder="Опишите результат действия..."
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Примечания</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={2}
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500"
/>
</div>
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onClose}
className="flex-1 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>
<button
type="submit"
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"
>
{loading ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
</form>
</div>
</div>
);
};
// Payment Modal Component
interface PaymentModalProps {
work: PreTrialWorkType;
onClose: () => void;
onSuccess: () => void;
}
const PaymentModal: React.FC<PaymentModalProps> = ({ work, onClose, onSuccess }) => {
const [promisedDate, setPromisedDate] = useState('');
const [promisedAmount, setPromisedAmount] = useState('');
const [notes, setNotes] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
setPromisedDate(tomorrow.toISOString().slice(0, 10));
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch(`/api/legal/pre-trial-work/${work.id}/promised-payment`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
promisedDate,
promisedAmount: parseFloat(promisedAmount),
notes
})
});
if (response.ok) {
onSuccess();
} else {
alert('Ошибка при добавлении обещанной оплаты');
}
} catch (error) {
console.error('Error adding promised payment:', error);
alert('Ошибка при добавлении обещанной оплаты');
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-md w-full">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-black text-slate-800">Обещанная оплата</h3>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Дата обещанной оплаты</label>
<input
type="date"
value={promisedDate}
onChange={(e) => setPromisedDate(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-primary-500"
required
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Сумма</label>
<input
type="number"
value={promisedAmount}
onChange={(e) => setPromisedAmount(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-primary-500"
placeholder="0"
min="0"
step="0.01"
required
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">Примечания</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500"
/>
</div>
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onClose}
className="flex-1 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>
<button
type="submit"
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"
>
{loading ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
</form>
</div>
</div>
);
};
// Transfer Modal Component
interface TransferModalProps {
work: PreTrialWorkType;
onClose: () => void;
onSuccess: () => void;
}
const TransferModal: React.FC<TransferModalProps> = ({ work, onClose, onSuccess }) => {
const [courtCaseId, setCourtCaseId] = useState('');
const [courtCaseNumber, setCourtCaseNumber] = useState('');
const [subject, setSubject] = useState('');
const [courtName, setCourtName] = useState('');
const [judge, setJudge] = useState('');
const [loading, setLoading] = useState(false);
// Автоматически заполняем предмет дела на основе данных должника
useEffect(() => {
if (work.debtor && !subject) {
const autoSubject = `Взыскание задолженности по оплате жилищно-коммунальных услуг. ${work.debtor.address || ''}, кв. ${work.debtor.apartment || ''}`;
setSubject(autoSubject);
}
}, [work.debtor, subject]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!confirm('Вы уверены, что хотите передать это дело в суд? Будет автоматически создано судебное дело.')) {
return;
}
setLoading(true);
try {
const response = await authFetch(`/api/legal/pre-trial-work/${work.id}/transfer-to-court`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
courtCaseId: courtCaseId || undefined,
courtCaseNumber: courtCaseNumber || undefined,
subject: subject || undefined,
courtName: courtName || undefined,
judge: judge || undefined
})
});
if (response.ok) {
const result = await response.json();
alert(result.message || 'Дело успешно передано в суд');
onSuccess();
} else {
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
alert(error.error || 'Ошибка при передаче в суд');
}
} catch (error) {
console.error('Error transferring to court:', error);
alert('Ошибка при передаче в суд');
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-md w-full">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-black text-slate-800">Передать в суд</h3>
<button onClick={onClose} className="p-1 text-slate-400 hover:text-slate-600">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3">
<p className="text-xs text-amber-800">
После передачи в суд дело будет перемещено в модуль "Взыскание" для дальнейшей работы.
</p>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">
Номер судебного дела (если уже существует)
</label>
<input
type="text"
value={courtCaseId}
onChange={(e) => setCourtCaseId(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-primary-500 mb-2"
placeholder="А40-12345/2024 (оставьте пустым для автогенерации)"
/>
<p className="text-[10px] text-slate-500 mb-3">
Если оставить пустым, будет автоматически создано новое судебное дело
</p>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">
Номер дела (для нового дела)
</label>
<input
type="text"
value={courtCaseNumber}
onChange={(e) => setCourtCaseNumber(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-primary-500"
placeholder="А40-12345/2024 (автогенерация, если пусто)"
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">
Предмет дела *
</label>
<textarea
value={subject}
onChange={(e) => setSubject(e.target.value)}
rows={3}
className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500"
placeholder="Взыскание задолженности..."
required
/>
</div>
<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={courtName}
onChange={(e) => setCourtName(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-primary-500"
placeholder="Арбитражный суд г. Москвы"
/>
</div>
<div>
<label className="block text-xs font-black text-slate-700 uppercase mb-1">
Судья
</label>
<input
type="text"
value={judge}
onChange={(e) => setJudge(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-primary-500"
placeholder="Иванов И.И."
/>
</div>
</div>
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onClose}
className="flex-1 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>
<button
type="submit"
disabled={loading}
className="flex-1 px-4 py-2.5 bg-purple-600 text-white rounded-xl text-xs font-black uppercase hover:bg-purple-700 transition-colors disabled:opacity-50"
>
{loading ? 'Передача...' : 'Передать в суд'}
</button>
</div>
</form>
</div>
</div>
);
};