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([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [search, setSearch] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [assignedToFilter, setAssignedToFilter] = useState('all'); // 'all' | 'mine' | employee name const [sortBy, setSortBy] = useState<'updatedAt' | 'debtAmount' | 'promisedDate'>('updatedAt'); const [currentUserName, setCurrentUserName] = useState(null); const [employeesList, setEmployeesList] = useState>([]); const [expandedWorkId, setExpandedWorkId] = useState(null); const [showActionModal, setShowActionModal] = useState(false); const [showPaymentModal, setShowPaymentModal] = useState(false); const [showTransferModal, setShowTransferModal] = useState(false); const [selectedWork, setSelectedWork] = useState(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 } => !!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 (
Загрузка...
); } if (error) { return (
Ошибка загрузки данных
{error}
); } return (
{/* Header with Search and Filters */}
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" />
{/* Statistics */}

Всего дел

{works.length}

В работе

{works.filter(w => w.status === 'in_progress').length}

Обещана оплата

{works.filter(w => w.status === 'promised_payment').length}

В суде

{works.filter(w => w.status === 'transferred_to_court').length}

{/* Требует внимания */} {(attentionItems.overdue.length > 0 || attentionItems.noAssignee.length > 0) && (

Требует внимания

    {attentionItems.overdue.length > 0 && (
  • Просроченные обещанные платежи:{' '} {attentionItems.overdue.length}{' '} {attentionItems.overdue.length === 1 ? 'дело' : 'дел'} {attentionItems.overdue.slice(0, 3).map(({ work, payment }) => ( {work.debtor?.address}, кв. {work.debtor?.apartment} — {new Date(payment.promisedDate).toLocaleDateString('ru-RU')}, {payment.promisedAmount?.toLocaleString()} ₽ ))} {attentionItems.overdue.length > 3 && ( и ещё {attentionItems.overdue.length - 3} )}
  • )} {attentionItems.noAssignee.length > 0 && (
  • Без ответственного: {attentionItems.noAssignee.length}{' '} {attentionItems.noAssignee.length === 1 ? 'дело' : 'дел'} {attentionItems.noAssignee.slice(0, 3).map((w) => ( {w.debtor?.address}, кв. {w.debtor?.apartment} ))} {attentionItems.noAssignee.length > 3 && ( и ещё {attentionItems.noAssignee.length - 3} )}
  • )}
)} {/* Works List */}
{sortedWorks.length === 0 ? (

Нет дел для отображения

) : ( sortedWorks .map((work) => { if (!work.debtor) return null; return ( 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 значения )}
{/* Action Modal */} {showActionModal && selectedWork && ( { setShowActionModal(false); setSelectedWork(null); }} onSuccess={() => { window.dispatchEvent(new CustomEvent('mkd-legal-changed')); loadWorks(); setShowActionModal(false); setSelectedWork(null); }} /> )} {/* Payment Modal */} {showPaymentModal && selectedWork && ( { setShowPaymentModal(false); setSelectedWork(null); }} onSuccess={() => { window.dispatchEvent(new CustomEvent('mkd-legal-changed')); loadWorks(); setShowPaymentModal(false); setSelectedWork(null); }} /> )} {/* Transfer Modal */} {showTransferModal && selectedWork && ( { setShowTransferModal(false); setSelectedWork(null); }} onSuccess={() => { window.dispatchEvent(new CustomEvent('mkd-legal-changed')); loadWorks(); setShowTransferModal(false); setSelectedWork(null); }} /> )} {/* Add Debtor Modal */} {showAddDebtorModal && ( setShowAddDebtorModal(false)} onSuccess={() => { window.dispatchEvent(new CustomEvent('mkd-legal-changed')); loadWorks(); setShowAddDebtorModal(false); }} /> )}
); }; // Add Debtor Modal Component interface AddDebtorModalProps { onClose: () => void; onSuccess: () => void; } const AddDebtorModal: React.FC = ({ 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 (

Добавить должника

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 />
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 />
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" />
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" />
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" />
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 />
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 />
); }; // Одна строка обещанной оплаты с кнопкой «Отметить оплачено» 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 (
{new Date(payment.promisedDate).toLocaleDateString('ru-RU')}
{payment.promisedAmount.toLocaleString()} ₽
{payment.isPaid ? (
Оплачено
) : new Date(payment.promisedDate) < new Date() ? (
Просрочено
) : (
Ожидается
)}
{payment.actualPaymentDate && (

Фактически оплачено: {new Date(payment.actualPaymentDate).toLocaleDateString('ru-RU')} {payment.actualPaymentAmount != null && ` - ${payment.actualPaymentAmount.toLocaleString()} ₽`}

)}
); }; 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 = ({ 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 (
{getStatusLabel(work.status)}

{debtor.address}, кв. {debtor.apartment}

{debtor.debtorName && (

{debtor.debtorName}

)}
{debtor.debtAmount.toLocaleString()} ₽
{debtor.debtMonths} мес. долга
{debtor.phone && (
{debtor.phone}
)}
{/* Action Buttons */} {work.status !== 'transferred_to_court' && work.status !== 'resolved' && (
)} {/* Expanded Content */} {expanded && (
{/* Actions History */} {actions.length > 0 && (

История действий

{actions.map((action) => { const Icon = getActionIcon(action.actionType); return (
{getActionLabel(action.actionType)} {new Date(action.actionDate).toLocaleDateString('ru-RU')}

Выполнил: {action.performedBy}

{action.result && (

{action.result}

)} {action.notes && (

{action.notes}

)}
); })}
)} {/* Promised Payments */} {payments.length > 0 && (

Обещанные оплаты

{payments.map((payment) => ( ))}
)} {/* Notes */} {work.notes && (

Примечания

{work.notes}

)}
)}
); }; // Action Modal Component interface ActionModalProps { work: PreTrialWorkType; onClose: () => void; onSuccess: () => void; } const ActionModal: React.FC = ({ 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 (

Добавить действие

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 />
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 />