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

1327 lines
54 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, 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>
);
};