import React, { useState, useEffect } from 'react'; import { ArrowLeft, Building2, Grid, List, Home, MapPin, DollarSign, TrendingUp, TrendingDown, FileText, Loader2, CheckCircle2, Eye } from 'lucide-react'; import { FinancialReport } from '../../types'; import { apiClient, authFetch } from '../../services/apiClient'; import { AggregatedReportView } from './AggregatedReportView'; import { ResidentReportView } from '../pr/ResidentReportView'; interface Building { id: string; address: string; district_id?: string; district_name?: string; total_income: number; total_expenses: number; balance: number; reports_count: number; } interface ReportDetailViewProps { report: FinancialReport; onBack: () => void; } type ViewMode = 'all' | 'sections' | 'specific'; type DisplayMode = 'grid' | 'list'; export const ReportDetailView: React.FC = ({ report, onBack }) => { const [buildings, setBuildings] = useState([]); const [loading, setLoading] = useState(true); const [viewMode, setViewMode] = useState('all'); const [displayMode, setDisplayMode] = useState('grid'); const [selectedBuilding, setSelectedBuilding] = useState(null); const [selectedDistrict, setSelectedDistrict] = useState(null); const [sections, setSections] = useState>({}); const [residentReportIds, setResidentReportIds] = useState>({}); const [aggregatedData, setAggregatedData] = useState(null); const [districtAggregatedData, setDistrictAggregatedData] = useState(null); const [selectedBuildingReport, setSelectedBuildingReport] = useState(null); const [buildingAggregatedData, setBuildingAggregatedData] = useState(null); const [loadingAggregated, setLoadingAggregated] = useState(false); const [selectedPeriodStart, setSelectedPeriodStart] = useState(''); const [selectedPeriodEnd, setSelectedPeriodEnd] = useState(''); const [buildingReportViewMode, setBuildingReportViewMode] = useState<'aggregated' | 'detailed'>('detailed'); const [report76Rows, setReport76Rows] = useState([]); const [loading76Rows, setLoading76Rows] = useState(false); useEffect(() => { if (report.reportType === 'balance_sheet_76') { setLoading(true); fetchReport76Rows(); } else { fetchBuildings(); if (report.reportType === 'balance_sheet') { fetchResidentReportIds(); } } }, [report.id]); useEffect(() => { if (viewMode === 'all' && report.reportType === 'balance_sheet') { fetchAggregatedData(); } else { setAggregatedData(null); } }, [viewMode, report.id, selectedPeriodStart, selectedPeriodEnd]); useEffect(() => { if (selectedDistrict && report.reportType === 'balance_sheet') { fetchDistrictAggregatedData(selectedDistrict); } else { setDistrictAggregatedData(null); } }, [selectedDistrict, report.id, selectedPeriodStart, selectedPeriodEnd]); useEffect(() => { if (selectedBuilding && report.reportType === 'balance_sheet') { if (buildingReportViewMode === 'aggregated') { fetchBuildingAggregatedReport(selectedBuilding); } else { fetchBuildingReport(selectedBuilding); } } else { setBuildingAggregatedData(null); setSelectedBuildingReport(null); } }, [selectedBuilding, selectedPeriodStart, selectedPeriodEnd, buildingReportViewMode]); const fetchResidentReportIds = async () => { try { // Получаем период из ведомости const buildingsData = await authFetch(`/api/finance/buildings?reportId=${report.id}`).then(r => r.json()); if (!buildingsData || buildingsData.length === 0) return; // Берем период из первого дома const firstBuilding = buildingsData[0]; const periodStart = firstBuilding.period_start; const periodEnd = firstBuilding.period_end; const reportIds: Record = {}; // Получаем все отчеты собственников const allReports = await apiClient.get(`/api/pr/reports`); for (const building of buildingsData) { // Ищем отчет для этого дома и периода const matchingReport = allReports.find((r: any) => r.buildingId === building.id && r.periodStart === periodStart && r.periodEnd === periodEnd ); if (matchingReport) { reportIds[building.id] = matchingReport.id; } } setResidentReportIds(reportIds); } catch (err) { console.warn('Не удалось загрузить ID отчетов собственников:', err); } }; useEffect(() => { if (buildings.length > 0) { // Группируем дома по участкам const grouped: Record = {}; buildings.forEach(building => { const section = building.district_name || building.district_id || 'Без участка'; if (!grouped[section]) { grouped[section] = []; } grouped[section].push(building); }); setSections(grouped); } }, [buildings]); const fetchReport76Rows = async () => { try { setLoading76Rows(true); const response = await authFetch(`/api/finance/reports/${report.id}/balance-sheet-76-rows`); if (!response.ok) throw new Error('Не удалось загрузить строки'); const data = await response.json(); setReport76Rows(Array.isArray(data) ? data : []); } catch (err: any) { console.error('Ошибка загрузки строк ОСВ 76:', err); setReport76Rows([]); } finally { setLoading76Rows(false); setLoading(false); } }; const fetchBuildings = async () => { try { setLoading(true); const response = await authFetch(`/api/finance/buildings?reportId=${report.id}`); if (!response.ok) { throw new Error('Не удалось загрузить дома'); } const data = await response.json(); // Преобразуем snake_case из API в camelCase const formattedBuildings: Building[] = data.map((b: any) => { // Используем district_name, если он есть и не равен district_id, иначе используем district_id const districtName = b.district_name && b.district_name !== b.district_id ? b.district_name : (b.district_id || undefined); // Логируем для отладки if (b.district_id && !b.district_name) { console.warn(`[ReportDetailView] Участок ${b.district_id} не найден в таблице districts`); } return { id: b.id, address: b.address, district_id: b.district_id, district_name: districtName, total_income: parseFloat(b.total_income) || 0, total_expenses: parseFloat(b.total_expenses) || 0, balance: parseFloat(b.balance) || 0, reports_count: parseInt(b.reports_count) || 0 }; }); setBuildings(formattedBuildings); } catch (err: any) { console.error('Ошибка загрузки домов:', err); } finally { setLoading(false); } }; const fetchAggregatedData = async () => { try { setLoadingAggregated(true); const params = new URLSearchParams(); // Фильтруем по периоду только если период выбран if (selectedPeriodStart) params.append('periodStart', selectedPeriodStart); if (selectedPeriodEnd) params.append('periodEnd', selectedPeriodEnd); const url = `/api/finance/reports/${report.id}/aggregated${params.toString() ? '?' + params.toString() : ''}`; const response = await fetch(url); if (!response.ok) { // Если ошибка 404, пробуем загрузить без фильтра по периоду if (response.status === 404 && (selectedPeriodStart || selectedPeriodEnd)) { const fallbackUrl = `/api/finance/reports/${report.id}/aggregated`; const fallbackResponse = await fetch(fallbackUrl); if (fallbackResponse.ok) { const data = await fallbackResponse.json(); // Инициализируем период из данных if (data.periodStart && data.periodEnd) { setSelectedPeriodStart(data.periodStart); setSelectedPeriodEnd(data.periodEnd); } setAggregatedData({ ...data, title: 'Отчет по всем домам', subtitle: `Всего домов: ${data.buildingsCount}` }); return; } } throw new Error('Не удалось загрузить агрегированные данные'); } const data = await response.json(); // Инициализируем период из данных, если период еще не установлен if (!selectedPeriodStart && !selectedPeriodEnd && data.periodStart && data.periodEnd) { setSelectedPeriodStart(data.periodStart); setSelectedPeriodEnd(data.periodEnd); } setAggregatedData({ ...data, title: 'Отчет по всем домам', subtitle: `Всего домов: ${data.buildingsCount}` }); } catch (err) { console.error('Ошибка загрузки агрегированных данных:', err); setAggregatedData(null); } finally { setLoadingAggregated(false); } }; const fetchDistrictAggregatedData = async (districtId: string) => { try { setLoadingAggregated(true); const params = new URLSearchParams(); if (selectedPeriodStart) params.append('periodStart', selectedPeriodStart); if (selectedPeriodEnd) params.append('periodEnd', selectedPeriodEnd); const url = `/api/finance/reports/${report.id}/districts/${districtId}/aggregated${params.toString() ? '?' + params.toString() : ''}`; const response = await fetch(url); if (!response.ok) throw new Error('Не удалось загрузить данные по участку'); const data = await response.json(); setDistrictAggregatedData({ ...data, title: `Отчет по участку: ${data.districtName}`, subtitle: `Домов в участке: ${data.buildingsCount}` }); } catch (err) { console.error('Ошибка загрузки данных по участку:', err); } finally { setLoadingAggregated(false); } }; const fetchBuildingReport = async (buildingId: string) => { try { setLoadingAggregated(true); // Сначала пробуем загрузить существующий отчет из resident_reports const reportId = residentReportIds[buildingId]; if (reportId) { try { const response = await fetch(`/api/pr/reports/${reportId}`); if (response.ok) { const reportData = await response.json(); setSelectedBuildingReport(reportData); return; } } catch (err) { console.warn('Не удалось загрузить существующий отчет, генерируем на лету:', err); } } // Если отчета нет или не удалось загрузить, генерируем на лету из данных ведомости const params = new URLSearchParams(); if (selectedPeriodStart) params.append('periodStart', selectedPeriodStart); if (selectedPeriodEnd) params.append('periodEnd', selectedPeriodEnd); const url = `/api/finance/reports/${report.id}/buildings/${buildingId}/report${params.toString() ? '?' + params.toString() : ''}`; const response = await fetch(url); if (!response.ok) { throw new Error('Не удалось загрузить или сгенерировать отчет'); } const reportData = await response.json(); setSelectedBuildingReport(reportData); } catch (err) { console.error('Ошибка загрузки отчета по дому:', err); setSelectedBuildingReport(null); } finally { setLoadingAggregated(false); } }; const fetchBuildingAggregatedReport = async (buildingId: string) => { try { setLoadingAggregated(true); // Загружаем агрегированный отчет по дому const params = new URLSearchParams(); if (selectedPeriodStart) params.append('periodStart', selectedPeriodStart); if (selectedPeriodEnd) params.append('periodEnd', selectedPeriodEnd); const url = `/api/finance/reports/${report.id}/buildings/${buildingId}/aggregated${params.toString() ? '?' + params.toString() : ''}`; const response = await fetch(url); if (!response.ok) { throw new Error('Не удалось загрузить отчет по дому'); } const reportData = await response.json(); setBuildingAggregatedData(reportData); } catch (err) { console.error('Ошибка загрузки агрегированного отчета по дому:', err); setBuildingAggregatedData(null); } finally { setLoadingAggregated(false); } }; const formatCurrency = (amount: number) => { return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB', minimumFractionDigits: 0 }).format(amount); }; const renderBuildingCard = (building: Building, onClick?: () => void) => { const CardContent = (

{building.address}

Доходы: {formatCurrency(building.total_income)}
Расходы: {formatCurrency(building.total_expenses)}
Баланс: = 0 ? 'text-emerald-600' : 'text-red-600' }`}> {formatCurrency(building.balance)}
{/* Дополнительная статистика */}
{building.reports_count > 0 && (
Отчетов создано: {building.reports_count}
)} {building.total_expenses > 0 && building.total_income > 0 && (
Рентабельность: = 1 ? 'text-emerald-600' : 'text-red-600' }`}> {Math.round((building.total_income / building.total_expenses) * 100)}%
)}
{building.district_name && (
Участок: {building.district_name}
)} {/* Кнопка просмотра отчета собственника (если есть) */} {report.reportType === 'balance_sheet' && residentReportIds[building.id] && ( )}
); if (onClick) { return (
{CardContent}
); } return
{CardContent}
; }; if (loading && report.reportType !== 'balance_sheet_76') { return (

Загрузка данных...

); } if (report.reportType === 'balance_sheet_76') { if (loading76Rows) { return (

Загрузка лицевых счетов...

); } const formatNum = (n: number) => (n == null || Number.isNaN(n) ? '—' : Number(n).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })); return (

{report.filename}

Лицевые счета: {report76Rows.length}

{report76Rows.map((row: any) => ( ))}
Лицевой счёт Сальдо на начало Обороты дебет Обороты кредит Сальдо на конец (дебет) Сальдо на конец (кредит)
{row.accountLabel ?? '—'} {formatNum(row.saldoStartDebet)} {formatNum(row.turnoverDebet)} {formatNum(row.turnoverCredit)} {formatNum(row.saldoEndDebet)} {formatNum(row.saldoEndCredit)}
{report76Rows.length === 0 && (
Нет данных по лицевым счетам
)}
); } return (
{/* Заголовок и навигация */}

{report.filename}

{buildings.length} {buildings.length === 1 ? 'дом' : buildings.length < 5 ? 'дома' : 'домов'}

{/* Выбор периода */} {report.reportType === 'balance_sheet' && (
setSelectedPeriodStart(e.target.value)} className="px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
setSelectedPeriodEnd(e.target.value)} className="px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
)} {/* Кнопки навигации */}
{/* Переключатель вида отображения */}
{/* Контент в зависимости от режима */} {viewMode === 'all' && ( <> {loadingAggregated ? (

Загрузка агрегированного отчета...

) : aggregatedData ? ( ) : (

Нет данных для агрегированного отчета

)} )} {viewMode === 'sections' && ( <> {selectedDistrict ? ( <> {loadingAggregated ? (

Загрузка отчета по участку...

) : districtAggregatedData ? ( ) : (

Нет данных для отчета по участку

)} ) : (
{Object.entries(sections).map(([sectionName, sectionBuildings]) => { const districtId = sectionBuildings[0]?.district_id; const totalIncome = sectionBuildings.reduce((sum, b) => sum + b.total_income, 0); const totalExpenses = sectionBuildings.reduce((sum, b) => sum + b.total_expenses, 0); const totalBalance = sectionBuildings.reduce((sum, b) => sum + b.balance, 0); return (
districtId && setSelectedDistrict(districtId)} className="bg-white rounded-2xl border-2 border-slate-200 hover:border-primary-400 p-6 cursor-pointer transition-all hover:shadow-lg" >

{sectionName}

Домов: {sectionBuildings.length}
Доходы: {formatCurrency(totalIncome)}
Расходы: {formatCurrency(totalExpenses)}
Баланс: = 0 ? 'text-emerald-600' : 'text-red-600' }`}> {formatCurrency(totalBalance)}

Нажмите для просмотра отчета

); })}
)} )} {viewMode === 'specific' && ( <> {selectedBuilding && (buildingAggregatedData || selectedBuildingReport) ? ( <>
{loadingAggregated ? (

Загрузка отчета по дому...

) : buildingReportViewMode === 'aggregated' && buildingAggregatedData ? ( ) : buildingReportViewMode === 'detailed' && selectedBuildingReport?.content ? ( ) : (

Отчет по дому не найден

)} ) : (

Выберите дом

{buildings.map(building => renderBuildingCard(building, () => setSelectedBuilding(building.id)) )}
)} )} {buildings.length === 0 && (

Нет данных по домам для этого отчета

Возможные причины:

• Адреса домов в ведомости не совпадают с адресами в базе данных

• Дома еще не добавлены в систему

• Ошибка при парсинге ведомости

{report.errorLog && report.errorLog.notFoundAddresses && (

⚠ Не найдено домов: {report.errorLog.notFoundAddresses.length}

{report.errorLog.message || 'Данные по этим адресам не были загружены. Создайте дома вручную в системе, затем загрузите отчет повторно.'}

{report.errorLog.notFoundAddresses.slice(0, 10).map((addr: string, idx: number) => (
• {addr}
))} {report.errorLog.notFoundAddresses.length > 10 && (
... и еще {report.errorLog.notFoundAddresses.length - 10} адресов
)}
)}
)}
); };