521 lines
21 KiB
TypeScript
521 lines
21 KiB
TypeScript
|
|
|
|||
|
|
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<FinanceModuleProps> = ({ 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<FinanceTab>(() => {
|
|||
|
|
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<ReportsView>('types');
|
|||
|
|
const [currentJobId, setCurrentJobId] = useState<string | null>(null);
|
|||
|
|
const [currentReportId, setCurrentReportId] = useState<number | null>(null);
|
|||
|
|
const [selectedReport, setSelectedReport] = useState<FinancialReport | null>(null);
|
|||
|
|
const [selectedBuildingId, setSelectedBuildingId] = useState<string | null>(null);
|
|||
|
|
const [selectedReportType, setSelectedReportType] = useState<string | undefined>(undefined);
|
|||
|
|
const [selectedReportTypeName, setSelectedReportTypeName] = useState<string | undefined>(undefined);
|
|||
|
|
const [totalReportsCount, setTotalReportsCount] = useState<number>(0);
|
|||
|
|
|
|||
|
|
// Новая система счетов на оплату
|
|||
|
|
const [paymentInvoicesView, setPaymentInvoicesView] = useState<PaymentInvoicesView>('list');
|
|||
|
|
const [selectedPaymentInvoice, setSelectedPaymentInvoice] = useState<PaymentInvoice | null>(null);
|
|||
|
|
const [paymentInvoices, setPaymentInvoices] = useState<PaymentInvoice[]>([]);
|
|||
|
|
const currentUserId = currentUser?.id ?? '';
|
|||
|
|
const [formPrefill, setFormPrefill] = useState<FinanceInvoicePrefill>(null);
|
|||
|
|
const [calendarEntries, setCalendarEntries] = useState<PaymentCalendarEntry[]>([]);
|
|||
|
|
const [calendarInterval, setCalendarInterval] = useState<'week' | 'month'>('month');
|
|||
|
|
|
|||
|
|
const [invoices, setInvoices] = useState<Invoice[]>(() =>
|
|||
|
|
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<PaymentInvoice>(`/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<Invoice>) => {
|
|||
|
|
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<PaymentInvoice>) => {
|
|||
|
|
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 (
|
|||
|
|
<div className="animate-fade-in pb-20 space-y-6">
|
|||
|
|
<div className="flex flex-wrap justify-between items-center gap-4">
|
|||
|
|
<h2 className="text-xl font-bold text-slate-800">Финансовый отдел</h2>
|
|||
|
|
{canEditCurrentTab && activeTab === 'invoices' && (
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
<button onClick={handleAddInvoice} className="bg-primary-600 text-white p-2 rounded-xl shadow-lg shadow-primary-500/30 flex items-center gap-2 px-3 text-xs font-bold active:scale-95 transition-transform">
|
|||
|
|
<Plus className="w-4 h-4" /> Новый счет
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Tabs */}
|
|||
|
|
<div className="flex p-1 bg-slate-200/50 rounded-xl overflow-x-auto no-scrollbar">
|
|||
|
|
{[
|
|||
|
|
{id: 'summary', label: 'Сводка'},
|
|||
|
|
{id: 'invoices', label: 'Реестр счетов'},
|
|||
|
|
{id: 'calendar', label: 'Календарь оплат'},
|
|||
|
|
{id: 'reports', label: 'Отчеты'}
|
|||
|
|
].filter((tab) => visibleTabs.includes(tab.id as FinanceTab)).map((tab) => (
|
|||
|
|
<button
|
|||
|
|
key={tab.id}
|
|||
|
|
onClick={() => setActiveTab(tab.id as FinanceTab)}
|
|||
|
|
className={`flex-shrink-0 min-w-[7rem] px-4 py-2 text-xs font-bold whitespace-nowrap rounded-lg transition-all ${activeTab === tab.id ? 'bg-white text-primary-600 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`}
|
|||
|
|
>
|
|||
|
|
{tab.label}
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Sheet Content Rendering */}
|
|||
|
|
<div className="min-h-[280px] sm:min-h-[360px] md:min-h-[500px]">
|
|||
|
|
{activeTab === 'summary' && <FinanceSummary data={financialData} />}
|
|||
|
|
{activeTab === 'invoices' && (
|
|||
|
|
<>
|
|||
|
|
{paymentInvoicesView === 'form' && (
|
|||
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
|
|||
|
|
<PaymentInvoiceForm
|
|||
|
|
invoice={selectedPaymentInvoice || undefined}
|
|||
|
|
initialPrefill={formPrefill ?? undefined}
|
|||
|
|
currentUserId={currentUserId}
|
|||
|
|
onSave={handleSavePaymentInvoice}
|
|||
|
|
onCancel={() => {
|
|||
|
|
setFormPrefill(null);
|
|||
|
|
setPaymentInvoicesView('list');
|
|||
|
|
setSelectedPaymentInvoice(null);
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
{paymentInvoicesView === 'detail' && selectedPaymentInvoice && (
|
|||
|
|
<PaymentInvoiceDetail
|
|||
|
|
invoice={selectedPaymentInvoice}
|
|||
|
|
currentUserId={currentUserId}
|
|||
|
|
onBack={() => {
|
|||
|
|
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' && (
|
|||
|
|
<PaymentInvoiceList
|
|||
|
|
onInvoiceClick={(invoice) => {
|
|||
|
|
setSelectedPaymentInvoice(invoice);
|
|||
|
|
setPaymentInvoicesView('detail');
|
|||
|
|
}}
|
|||
|
|
onCreateNew={handleAddInvoice}
|
|||
|
|
currentUserId={currentUserId}
|
|||
|
|
canEdit={canEditCurrentTab}
|
|||
|
|
scopeOwn={scopeOwnCurrentTab}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
{activeTab === 'calendar' && (
|
|||
|
|
<PaymentCalendar
|
|||
|
|
invoices={invoices}
|
|||
|
|
paymentInvoices={paymentInvoices}
|
|||
|
|
calendarEntries={calendarEntries}
|
|||
|
|
onUpdateStatus={updateInvoiceStatus}
|
|||
|
|
onUpdatePaymentInvoiceStatus={handleUpdatePaymentInvoiceStatus}
|
|||
|
|
onRefreshCalendar={() => {
|
|||
|
|
fetchPaymentInvoices();
|
|||
|
|
fetchCalendarEntries();
|
|||
|
|
}}
|
|||
|
|
currentBalance={financialData.totalBalance}
|
|||
|
|
currentUserId={currentUserId}
|
|||
|
|
interval={calendarInterval}
|
|||
|
|
onIntervalChange={setCalendarInterval}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
{activeTab === 'reports' && (
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
{/* Детальный просмотр отчета */}
|
|||
|
|
{reportsView === 'detail' && selectedReport && selectedReport.reportType === 'debtors' && (
|
|||
|
|
<DebtorReportDetailView
|
|||
|
|
report={selectedReport}
|
|||
|
|
onBack={() => {
|
|||
|
|
setReportsView('list');
|
|||
|
|
setSelectedReport(null);
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
{reportsView === 'detail' && selectedReport && selectedReport.reportType !== 'debtors' && (
|
|||
|
|
<ReportDetailView
|
|||
|
|
report={selectedReport}
|
|||
|
|
onBack={() => {
|
|||
|
|
setReportsView('list');
|
|||
|
|
setSelectedReport(null);
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Список загруженных отчетов */}
|
|||
|
|
{reportsView === 'list' && (
|
|||
|
|
<ReportsGrid
|
|||
|
|
onReportClick={(report) => {
|
|||
|
|
setSelectedReport(report);
|
|||
|
|
setReportsView('detail');
|
|||
|
|
}}
|
|||
|
|
reportTypeFilter={selectedReportType}
|
|||
|
|
onBack={() => {
|
|||
|
|
setReportsView('types');
|
|||
|
|
setSelectedReportType(undefined);
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Загрузка данных (старый вариант без предустановленного типа) */}
|
|||
|
|
{reportsView === 'upload' && !selectedReportType && (
|
|||
|
|
<ReportUploader
|
|||
|
|
onUploadSuccess={(reportId, jobId) => {
|
|||
|
|
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 && (
|
|||
|
|
<ReportProcessing
|
|||
|
|
jobId={currentJobId}
|
|||
|
|
reportId={currentReportId || undefined}
|
|||
|
|
onComplete={() => {
|
|||
|
|
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 && (
|
|||
|
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
|
|||
|
|
<p className="text-slate-500">Нет активных процессов обработки</p>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setReportsView('types')}
|
|||
|
|
className="mt-4 text-primary-600 hover:text-primary-700 font-medium"
|
|||
|
|
>
|
|||
|
|
Вернуться к типам отчетов
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Типы отчетов (основной экран) */}
|
|||
|
|
{reportsView === 'types' && (
|
|||
|
|
<ReportTypesGrid
|
|||
|
|
onReportTypeClick={(reportType) => {
|
|||
|
|
// При клике на квадратик открываем модальное окно загрузки с предустановленным типом
|
|||
|
|
setSelectedReportType(reportType.reportType);
|
|||
|
|
setSelectedReportTypeName(reportType.name);
|
|||
|
|
setReportsView('upload');
|
|||
|
|
}}
|
|||
|
|
onViewReportsClick={() => {
|
|||
|
|
setReportsView('list');
|
|||
|
|
setSelectedReportType(undefined);
|
|||
|
|
}}
|
|||
|
|
totalReportsCount={totalReportsCount}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Модальное окно загрузки с предустановленным типом */}
|
|||
|
|
{reportsView === 'upload' && selectedReportType && (
|
|||
|
|
<div className="relative">
|
|||
|
|
<ReportUploader
|
|||
|
|
presetReportType={selectedReportType as any}
|
|||
|
|
reportTypeName={selectedReportTypeName}
|
|||
|
|
onUploadSuccess={(reportId, jobId) => {
|
|||
|
|
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);
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|