import React, { useState, useMemo, useEffect, useCallback } from 'react'; import { MOCK_BUILDINGS } from '../constants'; import { Invoice, InvoiceStatus, PaymentInvoice, PaymentCalendarEntry } from '../types'; import { Plus } from 'lucide-react'; import { apiClient, authFetch } from '../services/apiClient'; function getCalendarPeriod(interval: 'week' | 'month'): { dateFrom: string; dateTo: string } { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth(); if (interval === 'week') { const day = now.getDay(); const mondayOffset = day === 0 ? -6 : 1 - day; const monday = new Date(now); monday.setDate(now.getDate() + mondayOffset); const sunday = new Date(monday); sunday.setDate(monday.getDate() + 6); return { dateFrom: monday.toISOString().split('T')[0], dateTo: sunday.toISOString().split('T')[0] }; } const first = new Date(year, month, 1); const last = new Date(year, month + 1, 0); return { dateFrom: first.toISOString().split('T')[0], dateTo: last.toISOString().split('T')[0] }; } // Imported modular components import { FinanceSummary } from './finance/FinanceSummary'; import { InvoiceRegistry } from './finance/InvoiceRegistry'; import { PaymentCalendar } from './finance/PaymentCalendar'; import { FinanceReports } from './finance/FinanceReports'; import { ReportUploader } from './finance/ReportUploader'; import { ReportProcessing } from './finance/ReportProcessing'; import { BuildingFinancialSummary } from './finance/BuildingFinancialSummary'; import { ReportsGrid } from './finance/ReportsGrid'; import { ReportDetailView } from './finance/ReportDetailView'; import { DebtorReportDetailView } from './finance/DebtorReportDetailView'; import { ReportTypesGrid } from './finance/ReportTypesGrid'; import { FinancialReport } from '../types'; import { PaymentInvoiceList } from './finance/PaymentInvoiceList'; import { PaymentInvoiceDetail } from './finance/PaymentInvoiceDetail'; import { PaymentInvoiceForm } from './finance/PaymentInvoiceForm'; import { allowedSubsForSection } from '../constants/permissions'; import { useSubPermission } from '../contexts/PermissionsContext'; import type { User } from '../types'; type FinanceTab = 'summary' | 'invoices' | 'calendar' | 'reports'; type ReportsView = 'types' | 'upload' | 'processing' | 'detail' | 'list'; type PaymentInvoicesView = 'list' | 'detail' | 'form'; const FINANCE_TABS: FinanceTab[] = ['summary', 'invoices', 'calendar', 'reports']; const SUBTAB_KEY = 'mkd_subTab_finance'; export type FinanceInvoicePrefill = { purposeType: 'event'; purposeEventId?: string; purposeDescription?: string; totalAmount?: number; } | null; interface FinanceModuleProps { currentUser?: User | null; externalInvoiceId?: number | null; externalInvoicePrefill?: FinanceInvoicePrefill; onInvoiceHandled?: () => void; /** Детальные права: null/пусто = все вкладки по роли */ allowedPermissions?: string[] | null; } export const FinanceModule: React.FC = ({ currentUser, externalInvoiceId, externalInvoicePrefill, onInvoiceHandled, allowedPermissions }) => { const visibleTabs = useMemo(() => { const allowed = allowedSubsForSection(allowedPermissions ?? [], 'finance'); if (allowed === 'all') return FINANCE_TABS; return FINANCE_TABS.filter((t) => allowed.includes(t)); }, [allowedPermissions]); const [activeTab, setActiveTab] = useState(() => { const s = localStorage.getItem(SUBTAB_KEY); const tab = (s && FINANCE_TABS.includes(s as FinanceTab)) ? s as FinanceTab : 'summary'; return tab; }); const { canEdit: canEditCurrentTab, scopeOwn: scopeOwnCurrentTab } = useSubPermission('finance', activeTab); useEffect(() => { if (visibleTabs.length > 0 && !visibleTabs.includes(activeTab)) { setActiveTab(visibleTabs[0]); } }, [visibleTabs, activeTab]); useEffect(() => { if (visibleTabs.includes(activeTab)) localStorage.setItem(SUBTAB_KEY, activeTab); }, [activeTab, visibleTabs]); const [reportsView, setReportsView] = useState('types'); const [currentJobId, setCurrentJobId] = useState(null); const [currentReportId, setCurrentReportId] = useState(null); const [selectedReport, setSelectedReport] = useState(null); const [selectedBuildingId, setSelectedBuildingId] = useState(null); const [selectedReportType, setSelectedReportType] = useState(undefined); const [selectedReportTypeName, setSelectedReportTypeName] = useState(undefined); const [totalReportsCount, setTotalReportsCount] = useState(0); // Новая система счетов на оплату const [paymentInvoicesView, setPaymentInvoicesView] = useState('list'); const [selectedPaymentInvoice, setSelectedPaymentInvoice] = useState(null); const [paymentInvoices, setPaymentInvoices] = useState([]); const currentUserId = currentUser?.id ?? ''; const [formPrefill, setFormPrefill] = useState(null); const [calendarEntries, setCalendarEntries] = useState([]); const [calendarInterval, setCalendarInterval] = useState<'week' | 'month'>('month'); const [invoices, setInvoices] = useState(() => MOCK_BUILDINGS.flatMap(b => b.financials.invoices.map(inv => ({ ...inv, closingDocsReceived: Math.random() > 0.5, status: inv.status as InvoiceStatus }))) ); const fetchCalendarEntries = useCallback(async () => { const { dateFrom, dateTo } = getCalendarPeriod(calendarInterval); try { const res = await apiClient.get<{ entries: PaymentCalendarEntry[] }>( `/finance/payment-calendar/entries?dateFrom=${dateFrom}&dateTo=${dateTo}&limit=500` ); setCalendarEntries(res?.entries ?? []); } catch (err) { console.error('Error fetching payment calendar entries:', err); setCalendarEntries([]); } }, [calendarInterval]); // Загружаем счета и записи календаря при открытии вкладки «Календарь оплат» useEffect(() => { if (activeTab === 'calendar') { fetchPaymentInvoices(); fetchCalendarEntries(); } }, [activeTab, fetchCalendarEntries]); // Загружаем количество отчетов для отображения на главном экране useEffect(() => { if (activeTab === 'reports' && reportsView === 'types') { fetchReportsCount(); } }, [activeTab, reportsView]); const fetchReportsCount = async () => { try { const response = await authFetch('/api/finance/reports'); if (response.ok) { const data = await response.json(); setTotalReportsCount(Array.isArray(data) ? data.length : 0); } } catch (err) { console.error('Error fetching reports count:', err); } }; // Если пришел внешний запрос открыть конкретный счет useEffect(() => { const openFromExternal = async () => { if (!externalInvoiceId) return; try { const invoice = await apiClient.get(`/finance/payment-invoices/${externalInvoiceId}`); setSelectedPaymentInvoice(invoice); setActiveTab('invoices'); setPaymentInvoicesView('detail'); if (onInvoiceHandled) { onInvoiceHandled(); } } catch (err) { console.error('Error opening external invoice:', err); } }; openFromExternal(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [externalInvoiceId]); // Если пришло предзаполнение формы счета (например, по мероприятию) useEffect(() => { if (!externalInvoicePrefill) return; setFormPrefill(externalInvoicePrefill); setPaymentInvoicesView('form'); setSelectedPaymentInvoice(null); setActiveTab('invoices'); if (onInvoiceHandled) { onInvoiceHandled(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [externalInvoicePrefill]); const fetchPaymentInvoices = async () => { try { const response = await apiClient.get<{ invoices: PaymentInvoice[] }>('/finance/payment-invoices?status=scheduled&limit=100'); setPaymentInvoices(response.invoices); } catch (err) { console.error('Error fetching payment invoices:', err); } }; const financialData = useMemo(() => { const totalBalance = MOCK_BUILDINGS.reduce((sum, b) => sum + b.financials.balance, 0); const scheduledAmount = invoices.filter(i => i.status === 'scheduled').reduce((sum, i) => sum + i.amount, 0); const totalDebt = MOCK_BUILDINGS.reduce((sum, b) => sum + b.financials.debt, 0); const missingDocs = invoices.filter(i => i.status === 'paid' && !i.closingDocsReceived).length; return { totalBalance, totalDebt, scheduledAmount, missingDocs }; }, [invoices]); const updateInvoiceStatus = (id: string, newStatus: InvoiceStatus, extra?: Partial) => { setInvoices(prev => prev.map(inv => inv.id === id ? { ...inv, status: newStatus, ...extra } : inv )); }; const handleAddInvoice = () => { setPaymentInvoicesView('form'); setSelectedPaymentInvoice(null); setActiveTab('invoices'); }; const handleSavePaymentInvoice = async (invoiceData: Partial) => { try { if (selectedPaymentInvoice) { // Обновление существующего счета await apiClient.put(`/finance/payment-invoices/${selectedPaymentInvoice.id}`, invoiceData); } else { // Создание нового счета await apiClient.post('/finance/payment-invoices', invoiceData); } setFormPrefill(null); setPaymentInvoicesView('list'); setSelectedPaymentInvoice(null); fetchPaymentInvoices(); } catch (err) { console.error('Error saving payment invoice:', err); throw err; } }; const handleUpdatePaymentInvoiceStatus = async ( id: number, status: 'paid' | 'postponed' | 'cancelled', payload?: { paymentDate?: string; paymentRef?: string; isCash?: boolean; postponedDate?: string; cancelReason?: string } ) => { try { await apiClient.post(`/finance/payment-invoices/${id}/update-payment-status`, { status, paymentDate: payload?.paymentDate, paymentRef: payload?.paymentRef, isCash: payload?.isCash, postponedDate: payload?.postponedDate, cancelReason: payload?.cancelReason }); fetchPaymentInvoices(); } catch (err) { console.error('Error updating payment invoice status:', err); throw err; } }; return (

Финансовый отдел

{canEditCurrentTab && activeTab === 'invoices' && (
)}
{/* Tabs */}
{[ {id: 'summary', label: 'Сводка'}, {id: 'invoices', label: 'Реестр счетов'}, {id: 'calendar', label: 'Календарь оплат'}, {id: 'reports', label: 'Отчеты'} ].filter((tab) => visibleTabs.includes(tab.id as FinanceTab)).map((tab) => ( ))}
{/* Sheet Content Rendering */}
{activeTab === 'summary' && } {activeTab === 'invoices' && ( <> {paymentInvoicesView === 'form' && (
{ setFormPrefill(null); setPaymentInvoicesView('list'); setSelectedPaymentInvoice(null); }} />
)} {paymentInvoicesView === 'detail' && selectedPaymentInvoice && ( { setPaymentInvoicesView('list'); setSelectedPaymentInvoice(null); }} onUpdate={() => { fetchPaymentInvoices(); // Перезагружаем детали счета apiClient.get(`/finance/payment-invoices/${selectedPaymentInvoice.id}`) .then(inv => setSelectedPaymentInvoice(inv)) .catch(err => console.error('Error fetching invoice details:', err)); }} /> )} {paymentInvoicesView === 'list' && ( { setSelectedPaymentInvoice(invoice); setPaymentInvoicesView('detail'); }} onCreateNew={handleAddInvoice} currentUserId={currentUserId} canEdit={canEditCurrentTab} scopeOwn={scopeOwnCurrentTab} /> )} )} {activeTab === 'calendar' && ( { fetchPaymentInvoices(); fetchCalendarEntries(); }} currentBalance={financialData.totalBalance} currentUserId={currentUserId} interval={calendarInterval} onIntervalChange={setCalendarInterval} /> )} {activeTab === 'reports' && (
{/* Детальный просмотр отчета */} {reportsView === 'detail' && selectedReport && selectedReport.reportType === 'debtors' && ( { setReportsView('list'); setSelectedReport(null); }} /> )} {reportsView === 'detail' && selectedReport && selectedReport.reportType !== 'debtors' && ( { setReportsView('list'); setSelectedReport(null); }} /> )} {/* Список загруженных отчетов */} {reportsView === 'list' && ( { setSelectedReport(report); setReportsView('detail'); }} reportTypeFilter={selectedReportType} onBack={() => { setReportsView('types'); setSelectedReportType(undefined); }} /> )} {/* Загрузка данных (старый вариант без предустановленного типа) */} {reportsView === 'upload' && !selectedReportType && ( { setCurrentReportId(reportId); setCurrentJobId(jobId ?? null); window.dispatchEvent(new CustomEvent('mkd-finance-reports-changed')); if (jobId) { setReportsView('processing'); } else { fetchReportsCount(); setReportsView('list'); } }} onUploadError={(error) => { console.error('Ошибка загрузки:', error); }} onClose={() => { setReportsView('types'); setSelectedReportType(undefined); }} /> )} {/* Обработка */} {reportsView === 'processing' && currentJobId && ( { window.dispatchEvent(new CustomEvent('mkd-finance-reports-changed')); fetchReportsCount(); setReportsView('types'); setCurrentJobId(null); setCurrentReportId(null); setSelectedReportType(undefined); }} onViewReports={() => { // Обновляем счетчик отчетов перед переходом к списку fetchReportsCount(); setReportsView('list'); setCurrentJobId(null); setCurrentReportId(null); setSelectedReportType(undefined); }} /> )} {reportsView === 'processing' && !currentJobId && (

Нет активных процессов обработки

)} {/* Типы отчетов (основной экран) */} {reportsView === 'types' && ( { // При клике на квадратик открываем модальное окно загрузки с предустановленным типом setSelectedReportType(reportType.reportType); setSelectedReportTypeName(reportType.name); setReportsView('upload'); }} onViewReportsClick={() => { setReportsView('list'); setSelectedReportType(undefined); }} totalReportsCount={totalReportsCount} /> )} {/* Модальное окно загрузки с предустановленным типом */} {reportsView === 'upload' && selectedReportType && (
{ setCurrentReportId(reportId); setCurrentJobId(jobId ?? null); window.dispatchEvent(new CustomEvent('mkd-finance-reports-changed')); fetchReportsCount(); if (jobId) { setReportsView('processing'); } else { setReportsView('list'); setSelectedReportType(undefined); } }} onUploadError={(error) => { console.error('Ошибка загрузки:', error); }} onViewReports={() => { setReportsView('list'); setSelectedReportType(undefined); setSelectedReportTypeName(undefined); }} onClose={() => { setReportsView('types'); setSelectedReportType(undefined); setSelectedReportTypeName(undefined); }} />
)}
)}
); };