import React, { useState, useEffect } from 'react'; import { authFetch } from '../../services/apiClient'; import { readCache, saveCache } from '../../hooks/useCachedFetch'; import { REFRESH_EVENTS } from '../../constants/refreshEvents'; import { ShoppingCart, Package, FileText, Banknote, Calendar, Activity, AlertTriangle, ShieldCheck, TrendingUp, Clock, CheckCircle, XCircle, Plus, Receipt, Wrench, X } from 'lucide-react'; import { OfficeRequest, OfficeEquipment, OfficeInventoryItem, OfficeDocument } from '../../types'; interface Props { requests: OfficeRequest[]; } export const OfficeSummary: React.FC = ({ requests }) => { const CACHE_KEY = 'mkd_office_summary_cache'; const cached = readCache<{ equipment: OfficeEquipment[]; inventory: OfficeInventoryItem[]; documents: OfficeDocument[]; monthlyExpenses: number; officeInvoices: any[]; repairRequests: any[]; orders: any[] }>(CACHE_KEY, { equipment: [], inventory: [], documents: [], monthlyExpenses: 0, officeInvoices: [], repairRequests: [], orders: [] }); const hasCache = cached.equipment?.length > 0 || cached.inventory?.length > 0 || cached.documents?.length > 0; const [equipment, setEquipment] = useState(cached.equipment || []); const [inventory, setInventory] = useState(cached.inventory || []); const [documents, setDocuments] = useState(cached.documents || []); const [monthlyExpenses, setMonthlyExpenses] = useState(cached.monthlyExpenses ?? 0); const [officeInvoices, setOfficeInvoices] = useState(cached.officeInvoices || []); const [repairRequests, setRepairRequests] = useState(cached.repairRequests || []); const [orders, setOrders] = useState(cached.orders || []); const [loading, setLoading] = useState(!hasCache); const [showCreateInvoiceModal, setShowCreateInvoiceModal] = useState(false); // Заявок в работе (статусы: new, approved, ordered) const pendingRequests = requests.filter(r => r.status === 'new' || r.status === 'approved' || r.status === 'ordered' ).length; // Мало на складе const lowStockItems = inventory.filter(i => { const quantity = i.quantity || 0; const minThreshold = i.minThreshold || 0; return quantity <= minThreshold; }).length; // Входящие письма (не обработанные и не архивированные) const incomingDocs = documents.filter(d => d.type === 'incoming' && d.status !== 'processed' && d.status !== 'archived' ).length; useEffect(() => { const run = async () => { if (!hasCache) setLoading(true); try { await Promise.all([ fetchEquipment(), fetchInventory(), fetchDocuments(), fetchMonthlyExpenses(), fetchOfficeInvoices(), fetchRepairRequests(), fetchOrders() ]); } finally { setLoading(false); } }; run(); }, []); const fetchAllData = async (showSpinner = true) => { if (showSpinner) setLoading(true); try { await Promise.all([ fetchEquipment(), fetchInventory(), fetchDocuments(), fetchMonthlyExpenses(), fetchOfficeInvoices(), fetchRepairRequests(), fetchOrders() ]); } finally { setLoading(false); } }; useEffect(() => { if (!loading && (equipment.length > 0 || inventory.length > 0 || documents.length > 0)) { saveCache(CACHE_KEY, { equipment, inventory, documents, monthlyExpenses, officeInvoices, repairRequests, orders }); } }, [loading, equipment, inventory, documents, monthlyExpenses, officeInvoices, repairRequests, orders]); useEffect(() => { const onRefresh = () => fetchAllData(false); window.addEventListener(REFRESH_EVENTS.office, onRefresh); return () => window.removeEventListener(REFRESH_EVENTS.office, onRefresh); }, []); useEffect(() => { const interval = setInterval(() => fetchAllData(false), 10 * 1000); return () => clearInterval(interval); }, []); const fetchEquipment = async () => { try { const response = await authFetch('/api/office/equipment'); if (response.ok) { const data = await response.json(); // Нормализуем данные из API const normalizedData = data.map((eq: any) => ({ id: eq.id, name: eq.name || '', type: eq.type || 'other', brand: eq.brand, model: eq.model, serialNumber: eq.serial_number || eq.serialNumber, assignedTo: eq.assigned_to || eq.assignedTo, purchaseDate: eq.purchase_date || eq.purchaseDate, warrantyUntil: eq.warranty_until || eq.warrantyUntil, condition: eq.condition || 'good', notes: eq.notes, nextMaintenanceDate: eq.next_maintenance_date || eq.nextMaintenanceDate, createdAt: eq.created_at || eq.createdAt, updatedAt: eq.updated_at || eq.updatedAt })); setEquipment(normalizedData); } } catch (error) { console.error('Ошибка загрузки оборудования:', error); } }; const fetchInventory = async () => { try { const response = await authFetch('/api/office/inventory'); if (response.ok) { const data = await response.json(); // Нормализуем данные из API const normalizedData = data.map((item: any) => ({ id: item.id, itemName: item.item_name || item.itemName || '', category: item.category || '', quantity: item.quantity || 0, unit: item.unit || 'шт', minThreshold: item.min_threshold || item.minThreshold || 0, location: item.location, notes: item.notes, createdAt: item.created_at || item.createdAt, updatedAt: item.updated_at || item.updatedAt })); setInventory(normalizedData); } } catch (error) { console.error('Ошибка загрузки инвентаря:', error); } }; const fetchDocuments = async () => { try { const response = await authFetch('/api/office/documents'); if (response.ok) { const data = await response.json(); // Нормализуем данные из API const normalizedData = data.map((doc: any) => ({ id: doc.id, regNumber: doc.reg_number || doc.regNumber || '', title: doc.title || '', correspondent: doc.correspondent || '', date: doc.date || '', status: doc.status || 'registered', type: doc.document_type || doc.type || 'incoming', letterType: doc.letter_type || doc.letterType || 'paper', assignedTo: doc.assigned_to || doc.assignedTo, trackingNumber: doc.tracking_number || doc.trackingNumber, fileUrl: doc.file_url || doc.fileUrl, notes: doc.notes, createdBy: doc.created_by || doc.createdBy || '', createdAt: doc.created_at, updatedAt: doc.updated_at })); setDocuments(normalizedData); } } catch (error) { console.error('Ошибка загрузки документов:', error); } }; const fetchMonthlyExpenses = async () => { try { // Получаем заявки на закупки за текущий месяц со статусами ordered или received const now = new Date(); const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); const response = await authFetch('/api/office/supply-requests'); if (response.ok) { const data = await response.json(); // Нормализуем данные const normalizedData = data.map((req: any) => ({ id: req.id, requesterName: req.requester_name || req.requesterName || '', category: req.category || '', itemName: req.item_name || req.itemName || '', quantity: req.quantity || 0, amount: parseFloat(req.amount) || 0, // Явно преобразуем в число status: req.status || 'new', createdAt: req.created_at || req.createdAt })); // Фильтруем заявки за текущий месяц со статусами ordered или received const monthlyRequests = normalizedData.filter((req: OfficeRequest) => { if (req.status !== 'ordered' && req.status !== 'received') return false; if (!req.createdAt) return false; const reqDate = new Date(req.createdAt); return reqDate >= firstDayOfMonth && reqDate <= lastDayOfMonth; }); // Суммируем затраты const total = monthlyRequests.reduce((sum: number, req: OfficeRequest) => { const amount = typeof req.amount === 'number' ? req.amount : parseFloat(String(req.amount)) || 0; return sum + amount; }, 0); setMonthlyExpenses(isNaN(total) ? 0 : total); } } catch (error) { console.error('Ошибка загрузки затрат:', error); setMonthlyExpenses(0); } }; const fetchOfficeInvoices = async () => { try { // Получаем счета на оплату для офиса const response = await authFetch('/api/finance/payment-invoices?purposeType=office&limit=50'); if (response.ok) { const data = await response.json(); // API может возвращать массив или объект с полем invoices const invoices = Array.isArray(data) ? data : (data.invoices || []); setOfficeInvoices(invoices); } } catch (error) { console.error('Ошибка загрузки счетов офиса:', error); } }; const fetchRepairRequests = async () => { try { // Получаем заявки на ремонт с платными const response = await authFetch('/api/office/repair-requests'); if (response.ok) { const data = await response.json(); // Фильтруем только платные ремонты const paidRepairs = data.filter((req: any) => req.is_paid || req.isPaid); setRepairRequests(paidRepairs); } } catch (error) { console.error('Ошибка загрузки заявок на ремонт:', error); } }; const fetchOrders = async () => { try { // Получаем заказы const response = await authFetch('/api/office/orders'); if (response.ok) { const data = await response.json(); setOrders(data); } else { // Если endpoint еще не создан, используем пустой массив setOrders([]); } } catch (error) { console.error('Ошибка загрузки заказов:', error); setOrders([]); } }; return (
{/* KPI Cards */}
{pendingRequests}

Заявок в работе

0 ? 'bg-red-50 text-red-600' : 'bg-emerald-50 text-emerald-600'}`}>
0 ? 'text-red-600' : 'text-slate-800'}`}>{lowStockItems}

Мало на складе

{incomingDocs}

Входящие письма

{loading ? '...' : formatCurrency(monthlyExpenses)}

Затраты (мес)

{/* Budget Visualization */}

Лимиты расходов офиса

Остаток 15.2к
{/* График обслуживания */}

График обслуживания

{equipment .filter(eq => eq.nextMaintenanceDate) .sort((a, b) => new Date(a.nextMaintenanceDate!).getTime() - new Date(b.nextMaintenanceDate!).getTime()) .slice(0, 3) .map(eq => { const maintenanceDate = new Date(eq.nextMaintenanceDate!); const today = new Date(); const daysLeft = Math.ceil((maintenanceDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); const isUrgent = daysLeft <= 0; const dateStr = isUrgent ? 'Сегодня' : daysLeft === 1 ? 'Завтра' : maintenanceDate.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }); return (
{eq.name}
{dateStr}
); })} {equipment.filter(eq => eq.nextMaintenanceDate).length === 0 && (

Нет запланированных ТО

)}
{/* Dashboard заявок на закупку */}

Заявки на закупку

{/* Статистика по статусам */}
} label="Новые" value={requests.filter(r => r.status === 'new').length} color="bg-blue-50 text-blue-600" /> } label="Одобренные" value={requests.filter(r => r.status === 'approved').length} color="bg-emerald-50 text-emerald-600" /> } label="Заказано" value={requests.filter(r => r.status === 'ordered').length} color="bg-amber-50 text-amber-600" /> } label="Получено" value={requests.filter(r => r.status === 'received').length} color="bg-violet-50 text-violet-600" />
{/* График распределения по категориям */}

По категориям

{getCategoryStats(requests).map((stat, index) => ( ))}
{/* Последние заявки */}

Последние заявки

{requests .sort((a, b) => { const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0; const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0; return dateB - dateA; }) .slice(0, 5) .map(req => ( ))} {requests.length === 0 && (

Нет заявок

)}
{/* График платежей офиса */}
График платежей офиса
{loading ? (
Загрузка...
) : ( <> {/* Счета из финансов */} {officeInvoices.map((invoice: any) => ( ))} {/* Счета из заказов */} {orders .filter((order: any) => order.invoice_id || order.invoiceId || order.invoice_url || order.invoiceUrl) .map((order: any) => ( ))} {/* Платные ремонты */} {repairRequests .filter((req: any) => (req.is_paid || req.isPaid) && ((req.invoice_id || req.invoiceId) || (req.invoice_url || req.invoiceUrl))) .map((req: any) => ( ))} {officeInvoices.length === 0 && orders.filter((o: any) => o.invoiceId || o.invoiceUrl).length === 0 && repairRequests.filter((r: any) => (r.is_paid || r.isPaid) && (r.invoiceId || r.invoiceUrl)).length === 0 && (
Нет платежей
)} )}
{/* Create Invoice Modal */} {showCreateInvoiceModal && ( setShowCreateInvoiceModal(false)} onSuccess={() => { setShowCreateInvoiceModal(false); fetchOfficeInvoices(); }} /> )}
); }; const formatCurrency = (amount: number): string => { // Проверяем, что amount - валидное число if (isNaN(amount) || !isFinite(amount) || amount < 0) { return '0'; } // Округляем до целого const rounded = Math.round(amount); if (rounded >= 1000000) { return `${(rounded / 1000000).toFixed(1)}млн`; } else if (rounded >= 1000) { return `${(rounded / 1000).toFixed(1)}к`; } return rounded.toString(); }; const LegendItem = ({ color, label, value }: any) => (
{label}
{value}
); const PaymentItem = ({ date, title, amount, status, type, invoiceId, orderId, repairId, isEstimated }: any) => { const dateObj = date ? new Date(date) : new Date(); const day = dateObj.getDate(); const month = dateObj.toLocaleDateString('ru-RU', { month: 'short' }).toUpperCase().slice(0, 3); const getStatusColor = (status: string) => { switch (status) { case 'scheduled': return 'bg-blue-50 text-blue-600 border-blue-100'; case 'paid': return 'bg-emerald-50 text-emerald-600 border-emerald-100'; case 'approved': return 'bg-amber-50 text-amber-600 border-amber-100'; case 'draft': return 'bg-slate-50 text-slate-600 border-slate-100'; default: return 'bg-slate-50 text-slate-600 border-slate-100'; } }; const getTypeIcon = () => { switch (type) { case 'invoice': return ; case 'order': return ; case 'repair': return ; default: return ; } }; return (

{day}

{month}

{getTypeIcon()}

{title}

{isEstimated && ( Предварительно )}
{status === 'scheduled' ? 'Запланировано' : status === 'paid' ? 'Оплачено' : status === 'approved' ? 'Одобрено' : status === 'draft' ? 'Черновик' : status} {formatCurrency(amount)}
); }; const CreateInvoiceModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) => { const [formData, setFormData] = useState({ contractorName: '', serviceDescription: '', totalAmount: '', paymentFormat: 'postpayment' as 'prepayment' | 'postpayment' | 'advance', notes: '' }); const handleSubmit = async () => { try { const response = await authFetch('/api/finance/payment-invoices', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ createdBy: 'Current User', // TODO: получить из контекста purposeType: 'office', purposeDescription: formData.serviceDescription, paymentFormat: formData.paymentFormat, contractorName: formData.contractorName, serviceItems: [{ name: formData.serviceDescription, amount: parseFloat(formData.totalAmount) || 0 }], totalAmount: parseFloat(formData.totalAmount) || 0, itemType: 'service', notes: formData.notes }) }); if (response.ok) { window.dispatchEvent(new CustomEvent('mkd-office-changed')); onSuccess(); alert('Счет успешно создан'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка создания счета: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка создания счета:', error); alert(`Ошибка создания счета: ${error.message || 'Неизвестная ошибка'}`); } }; return (

Создать счет на оплату

setFormData({ ...formData, contractorName: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" placeholder="Название организации" required />
setFormData({ ...formData, serviceDescription: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" placeholder="Например: Закупка канцелярии" required />
setFormData({ ...formData, totalAmount: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" placeholder="0" min="0" step="0.01" required />