import React, { useEffect, useMemo, useState } from 'react'; import { Building, Invoice, InvoiceStatus, PaymentInvoice, PaymentInvoiceStatus } from '../../types'; import { Download, Plus, Activity, CheckCircle2, X, Trash2, Calendar, FileCheck, AlertCircle, Clock, Wallet } from 'lucide-react'; import { EditableField } from './EditableField'; import { apiClient } from '../../services/apiClient'; import { PaymentInvoiceDetail } from '../finance/PaymentInvoiceDetail'; export const FinanceView: React.FC<{ building: Building, setBuilding: React.Dispatch>, isEditing: boolean }> = ({ building, setBuilding, isEditing }) => { const invoices = building.financials.invoices || []; // Счета на оплату из финансового модуля (payment_invoices) const [paymentInvoices, setPaymentInvoices] = useState([]); const [loadingPaymentInvoices, setLoadingPaymentInvoices] = useState(false); const [selectedPaymentInvoice, setSelectedPaymentInvoice] = useState(null); const currentUserId = 'user-1'; // TODO: взять из системы авторизации const paymentStatusConfig: Record = { draft: { label: 'Черновик', color: 'text-slate-500', bg: 'bg-slate-100' }, pending_manager_approval: { label: 'На согл. руков.', color: 'text-amber-600', bg: 'bg-amber-50' }, pending_finance_manager_approval: { label: 'На согл. фин. руков.', color: 'text-blue-600', bg: 'bg-blue-50' }, approved: { label: 'Согласован', color: 'text-indigo-600', bg: 'bg-indigo-50' }, scheduled: { label: 'В графике', color: 'text-purple-600', bg: 'bg-purple-50' }, paid: { label: 'Оплачен', color: 'text-emerald-600', bg: 'bg-emerald-50' }, postponed: { label: 'Отложен', color: 'text-orange-600', bg: 'bg-orange-50' }, cancelled: { label: 'Отменен', color: 'text-red-600', bg: 'bg-red-50' }, rejected: { label: 'Отклонен', color: 'text-red-600', bg: 'bg-red-50' }, completed: { label: 'Выполнено', color: 'text-green-600', bg: 'bg-green-50' }, }; useEffect(() => { let cancelled = false; const load = async () => { try { setLoadingPaymentInvoices(true); // Берем только "building" и фильтруем по текущему дому на фронте const resp = await apiClient.get<{ invoices: PaymentInvoice[] }>( `/finance/payment-invoices?purposeType=building&limit=200` ); if (cancelled) return; const filtered = (resp?.invoices || []).filter((inv) => Array.isArray(inv.purposeBuildingIds) && inv.purposeBuildingIds.includes(building.id) ); setPaymentInvoices(filtered); } catch (e) { if (!cancelled) setPaymentInvoices([]); console.warn('[FinanceView] Failed to load payment invoices:', e); } finally { if (!cancelled) setLoadingPaymentInvoices(false); } }; load(); return () => { cancelled = true; }; }, [building.id]); const refreshPaymentInvoices = async () => { try { setLoadingPaymentInvoices(true); const resp = await apiClient.get<{ invoices: PaymentInvoice[] }>( `/finance/payment-invoices?purposeType=building&limit=200` ); const filtered = (resp?.invoices || []).filter((inv) => Array.isArray(inv.purposeBuildingIds) && inv.purposeBuildingIds.includes(building.id) ); setPaymentInvoices(filtered); } catch (e) { console.warn('[FinanceView] Failed to refresh payment invoices:', e); } finally { setLoadingPaymentInvoices(false); } }; const paymentInvoicesTotals = useMemo(() => { const total = paymentInvoices.reduce((sum, i) => sum + (Number(i.totalAmount) || 0), 0); const scheduled = paymentInvoices .filter((i) => i.status === 'scheduled') .reduce((sum, i) => sum + (Number(i.totalAmount) || 0), 0); return { total, scheduled }; }, [paymentInvoices]); // Calculate Totals const scheduledTotal = paymentInvoices.length > 0 ? paymentInvoicesTotals.scheduled : invoices.filter(i => i.status === 'scheduled' || i.status === 'overdue').reduce((sum, i) => sum + i.amount, 0); const balance = building.financials.balance; const hasBalance = balance > 0; const hasScheduled = scheduledTotal > 0; const isDeficit = scheduledTotal > balance; const handleAddInvoice = () => { const newInvoice: Invoice = { id: `inv-${Date.now()}`, buildingId: building.id, address: building.passport.address, contractorName: 'Новый Контрагент', serviceName: 'Услуга по содержанию', amount: 0, date: new Date().toISOString().split('T')[0], status: 'pending_approval', priority: 'medium', closingDocsReceived: false }; setBuilding(prev => ({ ...prev, financials: { ...prev.financials, invoices: [newInvoice, ...prev.financials.invoices] } })); }; /** * Updates an invoice with a specific field or a partial object of updates */ const updateInvoice = (id: string, field: keyof Invoice, value: any, extra?: Partial) => { setBuilding(prev => ({ ...prev, financials: { ...prev.financials, invoices: prev.financials.invoices.map(inv => inv.id === id ? { ...inv, [field]: value, ...extra } : inv) } })); }; const deleteInvoice = (id: string) => { setBuilding(prev => ({ ...prev, financials: { ...prev.financials, invoices: prev.financials.invoices.filter(i => i.id !== id) } })); }; return (
{/* 1. Cash Flow Visual */}

Финансовый резерв

{isDeficit ? 'ДЕФИЦИТ СРЕДСТВ' : 'РЕЗЕРВ OK'}

Доступно

setBuilding({...building, financials: {...building.financials, balance: Number(v)}})} isEditing={isEditing} type="number" className="text-lg font-black text-slate-900 w-24"/>

{hasBalance ? `После оплат останется ≈ ${(balance - scheduledTotal).toLocaleString('ru-RU')} ₽` : 'На счетах нет средств'}

В графике оплат

{scheduledTotal.toLocaleString()} ₽

{hasScheduled ? 'Платежи в графике есть' : 'Платежей в графике нет'}

Загрузка бюджета на тек. неделю: {Math.round((scheduledTotal / (balance || 1)) * 100)}%

{/* 2. Actions */} {isEditing && ( )} {/* 3. Реестр счетов по дому (единый источник) */}

Счета по дому

Источник: финансовый модуль • {paymentInvoices.length} шт. • Сумма: {paymentInvoicesTotals.total.toLocaleString('ru-RU')} ₽

{!loadingPaymentInvoices && paymentInvoices.length === 0 && (
Счетов нет
)} {paymentInvoices.slice(0, 50).map((inv) => (
setSelectedPaymentInvoice(inv)} >
{paymentStatusConfig[inv.status]?.label || inv.status} {inv.invoiceNumber} {inv.scheduledDate && ( {new Date(inv.scheduledDate).toLocaleDateString('ru-RU')} )}
{inv.contractorName}
{inv.serviceDescription || '—'}
{Number(inv.totalAmount || 0).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ₽
{inv.createdAt ? new Date(inv.createdAt).toLocaleDateString('ru-RU') : ''}
))}
{/* Modal: просмотр/доработка счета */} {selectedPaymentInvoice && (
setSelectedPaymentInvoice(null)} onUpdate={async () => { // Перезагружаем счет (чтобы статус/поля обновились) try { const fresh = await apiClient.get(`/finance/payment-invoices/${selectedPaymentInvoice.id}`); setSelectedPaymentInvoice(fresh); } catch (e) { console.warn('[FinanceView] Failed to refresh selected invoice:', e); } await refreshPaymentInvoices(); }} />
)}
); }; const StatusBadge: React.FC<{ status: InvoiceStatus }> = ({ status }) => { const config: Record = { draft: { label: 'Черновик', color: 'text-slate-500', bg: 'bg-slate-100' }, pending_approval: { label: 'На согл.', color: 'text-amber-600', bg: 'bg-amber-50' }, approved: { label: 'Согласован', color: 'text-blue-600', bg: 'bg-blue-50' }, scheduled: { label: 'В графике', color: 'text-indigo-600', bg: 'bg-indigo-50' }, paid: { label: 'Оплачен', color: 'text-emerald-600', bg: 'bg-emerald-50' }, rejected: { label: 'Отказ', color: 'text-red-600', bg: 'bg-red-50' }, overdue: { label: 'Просрочен', color: 'text-white', bg: 'bg-red-500' }, // FIX: Added missing 'clarification' status to config to satisfy Record type. clarification: { label: 'Уточнение', color: 'text-purple-600', bg: 'bg-purple-50' }, }; const s = config[status] || config.draft; return ( {s.label} ); };