Files
mkd/components/finance/ReportDetailView.tsx
2026-02-04 00:17:04 +05:00

860 lines
39 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ReportDetailViewProps> = ({ report, onBack }) => {
const [buildings, setBuildings] = useState<Building[]>([]);
const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState<ViewMode>('all');
const [displayMode, setDisplayMode] = useState<DisplayMode>('grid');
const [selectedBuilding, setSelectedBuilding] = useState<string | null>(null);
const [selectedDistrict, setSelectedDistrict] = useState<string | null>(null);
const [sections, setSections] = useState<Record<string, Building[]>>({});
const [residentReportIds, setResidentReportIds] = useState<Record<string, number>>({});
const [aggregatedData, setAggregatedData] = useState<any>(null);
const [districtAggregatedData, setDistrictAggregatedData] = useState<any>(null);
const [selectedBuildingReport, setSelectedBuildingReport] = useState<any>(null);
const [buildingAggregatedData, setBuildingAggregatedData] = useState<any>(null);
const [loadingAggregated, setLoadingAggregated] = useState(false);
const [selectedPeriodStart, setSelectedPeriodStart] = useState<string>('');
const [selectedPeriodEnd, setSelectedPeriodEnd] = useState<string>('');
const [buildingReportViewMode, setBuildingReportViewMode] = useState<'aggregated' | 'detailed'>('detailed');
const [report76Rows, setReport76Rows] = useState<any[]>([]);
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<string, number> = {};
// Получаем все отчеты собственников
const allReports = await apiClient.get<any[]>(`/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<string, Building[]> = {};
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 = (
<div className={`bg-white rounded-xl border-2 ${
selectedBuilding === building.id
? 'border-primary-600 bg-primary-50'
: 'border-slate-200 hover:border-primary-400'
} p-5 transition-all ${onClick ? 'cursor-pointer hover:shadow-lg' : ''}`}>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2 flex-1 min-w-0">
<Building2 className="w-5 h-5 text-primary-600 flex-shrink-0" />
<h4 className="font-bold text-slate-800 line-clamp-2">{building.address}</h4>
</div>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 flex items-center gap-1">
<TrendingUp className="w-4 h-4 text-emerald-600" />
Доходы:
</span>
<span className="font-bold text-emerald-600">
{formatCurrency(building.total_income)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 flex items-center gap-1">
<TrendingDown className="w-4 h-4 text-red-600" />
Расходы:
</span>
<span className="font-bold text-red-600">
{formatCurrency(building.total_expenses)}
</span>
</div>
<div className="flex items-center justify-between text-sm pt-2 border-t border-slate-200">
<span className="text-slate-700 font-medium flex items-center gap-1">
<DollarSign className="w-4 h-4 text-primary-600" />
Баланс:
</span>
<span className={`font-bold text-lg ${
building.balance >= 0 ? 'text-emerald-600' : 'text-red-600'
}`}>
{formatCurrency(building.balance)}
</span>
</div>
{/* Дополнительная статистика */}
<div className="pt-2 border-t border-slate-100 space-y-1">
{building.reports_count > 0 && (
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Отчетов создано:</span>
<span className="font-medium text-slate-700">{building.reports_count}</span>
</div>
)}
{building.total_expenses > 0 && building.total_income > 0 && (
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Рентабельность:</span>
<span className={`font-medium ${
(building.total_income / building.total_expenses) >= 1 ? 'text-emerald-600' : 'text-red-600'
}`}>
{Math.round((building.total_income / building.total_expenses) * 100)}%
</span>
</div>
)}
</div>
</div>
{building.district_name && (
<div className="flex items-center gap-1 text-xs text-slate-500 mb-3">
<MapPin className="w-3 h-3" />
<span>Участок: {building.district_name}</span>
</div>
)}
{/* Кнопка просмотра отчета собственника (если есть) */}
{report.reportType === 'balance_sheet' && residentReportIds[building.id] && (
<button
onClick={(e) => {
e.stopPropagation();
// Открываем отчет в новой вкладке
const url = `/pr/reports/${residentReportIds[building.id]}`;
window.open(url, '_blank');
}}
className="mt-3 w-full bg-primary-600 text-white px-4 py-2 rounded-lg flex items-center justify-center gap-2 hover:bg-primary-700 transition-colors text-sm font-medium"
title="Открыть отчет собственникам МКД для этого дома"
>
<Eye className="w-4 h-4" />
Отчет собственникам
</button>
)}
</div>
);
if (onClick) {
return (
<div key={building.id} onClick={onClick}>
{CardContent}
</div>
);
}
return <div key={building.id}>{CardContent}</div>;
};
if (loading && report.reportType !== 'balance_sheet_76') {
return (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto mb-4"></div>
<p className="text-slate-600">Загрузка данных...</p>
</div>
);
}
if (report.reportType === 'balance_sheet_76') {
if (loading76Rows) {
return (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto mb-4"></div>
<p className="text-slate-600">Загрузка лицевых счетов...</p>
</div>
);
}
const formatNum = (n: number) => (n == null || Number.isNaN(n) ? '—' : Number(n).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
return (
<div className="space-y-6 animate-fade-in">
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-6">
<button onClick={onBack} className="p-2 hover:bg-slate-100 rounded-lg transition-colors flex-shrink-0">
<ArrowLeft className="w-5 h-5 text-slate-600" />
</button>
<div className="flex-1 min-w-0">
<h2 className="text-xl font-bold text-slate-800 mb-1 break-words">{report.filename}</h2>
<p className="text-sm text-slate-500">Лицевые счета: {report76Rows.length}</p>
</div>
</div>
</div>
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="bg-slate-50 border-b border-slate-200">
<th className="text-left py-3 px-4 font-semibold text-slate-700">Лицевой счёт</th>
<th className="text-right py-3 px-4 font-semibold text-slate-700">Сальдо на начало</th>
<th className="text-right py-3 px-4 font-semibold text-slate-700">Обороты дебет</th>
<th className="text-right py-3 px-4 font-semibold text-slate-700">Обороты кредит</th>
<th className="text-right py-3 px-4 font-semibold text-slate-700">Сальдо на конец (дебет)</th>
<th className="text-right py-3 px-4 font-semibold text-slate-700">Сальдо на конец (кредит)</th>
</tr>
</thead>
<tbody>
{report76Rows.map((row: any) => (
<tr key={row.id ?? row.rowIndex} className="border-b border-slate-100 hover:bg-slate-50">
<td className="py-2 px-4 text-slate-800">{row.accountLabel ?? '—'}</td>
<td className="py-2 px-4 text-right text-slate-700">{formatNum(row.saldoStartDebet)}</td>
<td className="py-2 px-4 text-right text-slate-700">{formatNum(row.turnoverDebet)}</td>
<td className="py-2 px-4 text-right text-slate-700">{formatNum(row.turnoverCredit)}</td>
<td className="py-2 px-4 text-right text-slate-700">{formatNum(row.saldoEndDebet)}</td>
<td className="py-2 px-4 text-right text-slate-700">{formatNum(row.saldoEndCredit)}</td>
</tr>
))}
</tbody>
</table>
</div>
{report76Rows.length === 0 && (
<div className="p-12 text-center text-slate-500">Нет данных по лицевым счетам</div>
)}
</div>
</div>
);
}
return (
<div className="space-y-6 animate-fade-in">
{/* Заголовок и навигация */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-6">
<button
onClick={onBack}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors flex-shrink-0"
>
<ArrowLeft className="w-5 h-5 text-slate-600" />
</button>
<div className="flex-1 min-w-0">
<h2 className="text-xl font-bold text-slate-800 mb-1 break-words">{report.filename}</h2>
<p className="text-sm text-slate-500">
{buildings.length} {buildings.length === 1 ? 'дом' : buildings.length < 5 ? 'дома' : 'домов'}
</p>
</div>
</div>
{/* Выбор периода */}
{report.reportType === 'balance_sheet' && (
<div className="bg-white rounded-xl border border-slate-200 p-4 mb-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<label className="text-sm font-medium text-slate-700 whitespace-nowrap">Период отчета:</label>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 flex-1">
<div className="flex items-center gap-2">
<label className="text-xs text-slate-600 whitespace-nowrap">С:</label>
<input
type="date"
value={selectedPeriodStart}
onChange={(e) => 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"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-xs text-slate-600 whitespace-nowrap">По:</label>
<input
type="date"
value={selectedPeriodEnd}
onChange={(e) => 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"
/>
</div>
</div>
</div>
</div>
)}
{/* Кнопки навигации */}
<div className="grid grid-cols-3 gap-3 mb-4">
<button
onClick={() => {
setViewMode('all');
setSelectedBuilding(null);
}}
className={`p-4 rounded-xl border-2 transition-all ${
viewMode === 'all'
? 'border-primary-600 bg-primary-50 text-primary-700 font-bold'
: 'border-slate-200 hover:border-primary-300 text-slate-700'
}`}
>
<div className="flex flex-col items-center gap-2">
<Grid className="w-6 h-6" />
<span className="text-sm">Все дома</span>
</div>
</button>
<button
onClick={() => {
setViewMode('sections');
setSelectedBuilding(null);
}}
className={`p-4 rounded-xl border-2 transition-all ${
viewMode === 'sections'
? 'border-primary-600 bg-primary-50 text-primary-700 font-bold'
: 'border-slate-200 hover:border-primary-300 text-slate-700'
}`}
>
<div className="flex flex-col items-center gap-2">
<List className="w-6 h-6" />
<span className="text-sm">По участкам</span>
</div>
</button>
<button
onClick={() => {
setViewMode('specific');
setSelectedBuilding(null);
}}
className={`p-4 rounded-xl border-2 transition-all ${
viewMode === 'specific'
? 'border-primary-600 bg-primary-50 text-primary-700 font-bold'
: 'border-slate-200 hover:border-primary-300 text-slate-700'
}`}
>
<div className="flex flex-col items-center gap-2">
<Home className="w-6 h-6" />
<span className="text-sm">Конкретный дом</span>
</div>
</button>
</div>
{/* Переключатель вида отображения */}
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setDisplayMode('grid')}
className={`p-2 rounded-lg transition-all ${
displayMode === 'grid'
? 'bg-primary-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setDisplayMode('list')}
className={`p-2 rounded-lg transition-all ${
displayMode === 'list'
? 'bg-primary-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<List className="w-4 h-4" />
</button>
</div>
</div>
{/* Контент в зависимости от режима */}
{viewMode === 'all' && (
<>
{loadingAggregated ? (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<Loader2 className="w-8 h-8 animate-spin text-primary-600 mx-auto mb-4" />
<p className="text-slate-600">Загрузка агрегированного отчета...</p>
</div>
) : aggregatedData ? (
<AggregatedReportView data={aggregatedData} />
) : (
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-12 text-center">
<p className="text-slate-600">Нет данных для агрегированного отчета</p>
</div>
)}
</>
)}
{viewMode === 'sections' && (
<>
{selectedDistrict ? (
<>
<button
onClick={() => {
setSelectedDistrict(null);
setDistrictAggregatedData(null);
}}
className="mb-4 flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium"
>
<ArrowLeft className="w-4 h-4" />
Назад к списку участков
</button>
{loadingAggregated ? (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<Loader2 className="w-8 h-8 animate-spin text-primary-600 mx-auto mb-4" />
<p className="text-slate-600">Загрузка отчета по участку...</p>
</div>
) : districtAggregatedData ? (
<AggregatedReportView data={districtAggregatedData} />
) : (
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-12 text-center">
<p className="text-slate-600">Нет данных для отчета по участку</p>
</div>
)}
</>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{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 (
<div
key={sectionName}
onClick={() => 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"
>
<div className="flex items-center gap-2 mb-4">
<MapPin className="w-5 h-5 text-primary-600" />
<h3 className="text-lg font-bold text-slate-800">{sectionName}</h3>
</div>
<div className="space-y-2 mb-4">
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600">Домов:</span>
<span className="font-bold text-slate-800">{sectionBuildings.length}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 flex items-center gap-1">
<TrendingUp className="w-4 h-4 text-emerald-600" />
Доходы:
</span>
<span className="font-bold text-emerald-600">{formatCurrency(totalIncome)}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-slate-600 flex items-center gap-1">
<TrendingDown className="w-4 h-4 text-red-600" />
Расходы:
</span>
<span className="font-bold text-red-600">{formatCurrency(totalExpenses)}</span>
</div>
<div className="flex items-center justify-between text-sm pt-2 border-t border-slate-200">
<span className="text-slate-700 font-medium flex items-center gap-1">
<DollarSign className="w-4 h-4 text-primary-600" />
Баланс:
</span>
<span className={`font-bold text-lg ${
totalBalance >= 0 ? 'text-emerald-600' : 'text-red-600'
}`}>
{formatCurrency(totalBalance)}
</span>
</div>
</div>
<div className="mt-4 pt-4 border-t border-slate-200">
<p className="text-xs text-primary-600 font-medium text-center">
Нажмите для просмотра отчета
</p>
</div>
</div>
);
})}
</div>
)}
</>
)}
{viewMode === 'specific' && (
<>
{selectedBuilding && (buildingAggregatedData || selectedBuildingReport) ? (
<>
<div className="flex items-center justify-between mb-4">
<button
onClick={() => {
setSelectedBuilding(null);
setBuildingAggregatedData(null);
setSelectedBuildingReport(null);
}}
className="flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium"
>
<ArrowLeft className="w-4 h-4" />
Назад к списку домов
</button>
<div className="flex gap-2">
<button
onClick={() => setBuildingReportViewMode('detailed')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
buildingReportViewMode === 'detailed'
? 'bg-primary-600 text-white'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
Детальный отчет
</button>
<button
onClick={() => setBuildingReportViewMode('aggregated')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
buildingReportViewMode === 'aggregated'
? 'bg-primary-600 text-white'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
Агрегированный отчет
</button>
</div>
</div>
{loadingAggregated ? (
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-12 text-center">
<Loader2 className="w-8 h-8 animate-spin text-primary-600 mx-auto mb-4" />
<p className="text-slate-600">Загрузка отчета по дому...</p>
</div>
) : buildingReportViewMode === 'aggregated' && buildingAggregatedData ? (
<AggregatedReportView data={buildingAggregatedData} />
) : buildingReportViewMode === 'detailed' && selectedBuildingReport?.content ? (
<ResidentReportView
content={selectedBuildingReport.content}
buildingAddress={selectedBuildingReport.address}
period={selectedBuildingReport.periodStart && selectedBuildingReport.periodEnd
? `${new Date(selectedBuildingReport.periodStart).toLocaleDateString('ru-RU')} - ${new Date(selectedBuildingReport.periodEnd).toLocaleDateString('ru-RU')}`
: selectedBuildingReport.month}
/>
) : (
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-12 text-center">
<p className="text-slate-600">Отчет по дому не найден</p>
</div>
)}
</>
) : (
<div className="space-y-4">
<div className="bg-white rounded-2xl border border-slate-200 shadow-sm p-6">
<h3 className="text-lg font-bold text-slate-800 mb-4">Выберите дом</h3>
<div className={displayMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
: 'space-y-3'
}>
{buildings.map(building =>
renderBuildingCard(building, () => setSelectedBuilding(building.id))
)}
</div>
</div>
</div>
)}
</>
)}
{buildings.length === 0 && (
<div className="bg-white rounded-2xl border border-amber-200 shadow-sm p-12">
<div className="text-center mb-6">
<Building2 className="w-12 h-12 text-amber-400 mx-auto mb-4" />
<p className="text-slate-600 font-medium mb-2">Нет данных по домам для этого отчета</p>
<p className="text-sm text-slate-500">
Возможные причины:
</p>
</div>
<div className="bg-amber-50 rounded-lg p-4 space-y-2 text-sm text-slate-700">
<p> Адреса домов в ведомости не совпадают с адресами в базе данных</p>
<p> Дома еще не добавлены в систему</p>
<p> Ошибка при парсинге ведомости</p>
</div>
{report.errorLog && report.errorLog.notFoundAddresses && (
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-lg p-4">
<p className="text-sm font-medium text-amber-800 mb-2">
Не найдено домов: {report.errorLog.notFoundAddresses.length}
</p>
<p className="text-xs text-amber-700 mb-3">
{report.errorLog.message || 'Данные по этим адресам не были загружены. Создайте дома вручную в системе, затем загрузите отчет повторно.'}
</p>
<div className="text-xs text-amber-700 space-y-1 max-h-40 overflow-y-auto">
{report.errorLog.notFoundAddresses.slice(0, 10).map((addr: string, idx: number) => (
<div key={idx}> {addr}</div>
))}
{report.errorLog.notFoundAddresses.length > 10 && (
<div className="text-amber-600 font-medium">
... и еще {report.errorLog.notFoundAddresses.length - 10} адресов
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
);
};