/
Pitonx
/
MyFinance
Обзор
Документация
Войти
/
Pitonx
/
MyFinance
Код
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/components/MainWindow.js
317 строк
13 KB
agent
v15.4.7 — drilldown: categoryId в фильтре, фикс пустого фильтра
14 июн 2026, 20:44
14 июн 2026, 20:44
b619a0e
Код
Авторство
О чём код?
import React, { useState, useMemo, useEffect } from 'react'; import { APP_VERSION, ICONS, SECTIONS, ENTITY_GROUPS, ENTITY_LABELS, ENTITY_ICONS } from '../config/entities'; import { API_BASE_URL } from '../config/apiConfig'; import EntityList from './EntityList'; import Dashboard from './Dashboard'; import AdminDataTransfer from './AdminDataTransfer'; import UserSettings from './UserSettings'; import BankImportWizard from './BankImport/BankImportWizard'; import ImportRulesSettings from './ImportRulesSettings'; import BudgetList from './BudgetList'; import BudgetForm from './BudgetForm'; import BudgetIncomeList from './BudgetIncomeList'; import BudgetIncomeForm from './BudgetIncomeForm'; import ReturnList from './ReturnList'; import TrashList from './TrashList'; import ReturnForm from './ReturnForm'; import ReportsPage from './ReportsPage'; import { useLookupData } from '../hooks/useLookupData'; export default function MainWindow({ user, onLogout }) { const lookups = useLookupData(); const isAdmin = user?.role === 'admin'; const visibleSections = SECTIONS; const [activeSection, setActiveSection] = useState(() => sessionStorage.getItem('activeSection') || 'dashboard'); const [activeEntity, setActiveEntity] = useState(() => sessionStorage.getItem('activeEntity') || 'accounts'); const [operationalDate, setOperationalDate] = useState(user?.operationalDate || null); const [showBudgetForm, setShowBudgetForm] = useState(false); const [budgetEditItem, setBudgetEditItem] = useState(null); const [showBudgetIncomeForm, setShowBudgetIncomeForm] = useState(false); const [budgetIncomeEditItem, setBudgetIncomeEditItem] = useState(null); const [showReturnForm, setShowReturnForm] = useState(false); const [returnEditItem, setReturnEditItem] = useState(null); const [returnDefaultExpenseId, setReturnDefaultExpenseId] = useState(null); // Drill-down из отчётов: фильтры для EntityList const [drilldownFilters, setDrilldownFilters] = useState(null); useEffect(() => { const handler = (e) => { const { category, categoryId, type, dateFrom, dateTo } = e.detail; const entity = type === 'income' ? 'incomes' : 'expenses'; setDrilldownFilters({ category, categoryId, dateFrom, dateTo }); setActiveSection('documents'); setActiveEntity(entity); setSectionEntityMap(prev => ({ ...prev, documents: entity })); }; window.addEventListener('report-drilldown', handler); return () => window.removeEventListener('report-drilldown', handler); }, []); // Сохраняем текущую страницу при смене useEffect(() => { sessionStorage.setItem('activeSection', activeSection); }, [activeSection]); useEffect(() => { sessionStorage.setItem('activeEntity', activeEntity); }, [activeEntity]); // Определяем мобильный режим const [isMobile, setIsMobile] = useState(() => window.matchMedia('(max-width: 768px)').matches); const [backendVersion, setBackendVersion] = useState(''); useEffect(() => { const mq = window.matchMedia('(max-width: 768px)'); const handler = (e) => setIsMobile(e.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); }, []); // Тестовая/продакшн: проверяем и фронт и бэкенд const [backendEnv, setBackendEnv] = useState(''); const isTestEnv = process.env.REACT_APP_ENV === 'test' || backendEnv === 'test'; // Получаем версию бэкенда (once only, guard against Strict Mode double-run) useEffect(() => { if (window._versionChecked) return; window._versionChecked = true; const token = localStorage.getItem('auth_token'); fetch(`${API_BASE_URL}/api/v1/version/`, { headers: token ? { 'Authorization': `Token ${token}` } : {}, }) .then(r => r.ok ? r.json() : null) .then(data => { if (data?.version) setBackendVersion(data.version); if (data?.env) setBackendEnv(data.env); }) .catch(() => {}); }, []); // Запоминаем выбранную сущность для каждой секции const [sectionEntityMap, setSectionEntityMap] = useState({ documents: 'expenses', references: 'accounts', budgets: 'budgets', reports: 'reportDynamics', settings: 'userSettings', }); const sectionEntities = useMemo(() => { const entities = ENTITY_GROUPS[activeSection] || []; if (!isAdmin) return entities.filter(e => e !== 'users'); return entities; }, [activeSection, isAdmin]); const userDisplay = user ? `${user.name || user.username} (${user.role === 'admin' ? 'Администратор' : 'Пользователь'})` : 'Гость'; const handleOpenEntity = (entityId) => { setActiveEntity(entityId); setSectionEntityMap(prev => ({ ...prev, [activeSection]: entityId })); }; const handleSectionClick = (sectionId) => { console.log(`[MainWindow] handleSectionClick: ${sectionId}`); setActiveSection(sectionId); if (sectionId === 'dashboard') return; const entities = ENTITY_GROUPS[sectionId] || []; const filtered = !isAdmin ? entities.filter(e => e !== 'users') : entities; if (filtered.length === 0) return; const savedEntity = sectionEntityMap[sectionId]; const targetEntity = savedEntity && filtered.includes(savedEntity) ? savedEntity : filtered[0]; console.log(`[MainWindow] → setActiveEntity('${targetEntity}')`); setActiveEntity(targetEntity); setSectionEntityMap(prev => ({ ...prev, [sectionId]: targetEntity })); }; const isDocumentsSection = activeSection === 'documents'; const showDesktopToolsPanel = !isDocumentsSection || isMobile; // Маппинг entityId → reportType для секции Отчёты const REPORT_ENTITY_MAP = { reportDynamics: 'dynamics', reportStructure: 'structure', reportByCategory: 'by_category', reportBudgetVsActual: 'budget_vs_actual', reportAccountFlow: 'account_flow', reportLoans: 'loans', reportDeposits: 'deposits', reportTopN: 'top_n', reportPeriodCompare: 'period_compare', reportCashflow: 'cashflow', }; const activeReportType = REPORT_ENTITY_MAP[activeEntity] || 'dynamics'; return ( <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}> {/* System Title Bar */} <div className="system-bar" style={{ display: 'flex', alignItems: 'center', padding: '0 8px', height: 28, background: isTestEnv ? 'linear-gradient(180deg, #ffcdd2 0%, #ef9a9a 100%)' : 'linear-gradient(180deg, #f0f0f0 0%, #e0e0e0 100%)', borderBottom: isTestEnv ? '1px solid #e57373' : '1px solid #c0c0c0', }}> <div style={{ flex: 1 }} /> <span style={{ fontWeight: 600, fontSize: 13, color: isTestEnv ? '#b71c1c' : '#333' }}>Учет финансов</span> <span style={{ fontSize: 11, color: '#1976d2', marginLeft: 8, fontWeight: 500 }}> [ОД: {operationalDate ? new Date(operationalDate).toLocaleDateString('ru-RU') : new Date().toLocaleDateString('ru-RU')}] </span> <span style={{ fontSize: 11, color: isTestEnv ? '#c62828' : '#666', marginLeft: 8, fontWeight: isTestEnv ? 600 : 'normal' }}> {APP_VERSION}{backendVersion ? ` | ${backendVersion}` : ''}{isTestEnv ? ' | ТЕСТОВАЯ ВЕРСИЯ' : ''} </span> <div style={{ flex: 1 }} /> <button className="sys-btn" onClick={onLogout} title="Закрыть" style={{ background: 'none', border: '1px solid #ccc', borderRadius: 3, cursor: 'pointer', fontSize: 14, padding: '2px 8px', display: 'flex', alignItems: 'center' }} > {ICONS.close} </button> </div> {/* Sections Panel (Yellow ribbon) */} <div className="sections-panel"> {visibleSections.map(section => ( <div key={section.id} className={`section-item ${activeSection === section.id ? 'active' : ''}`} onClick={() => handleSectionClick(section.id)} > <span className="section-icon">{ICONS[section.id]}</span> <span className="section-label">{section.label}</span> </div> ))} </div> {/* Functions Panel */} <div className="functions-panel"> {sectionEntities.map(entityId => ( <div key={entityId} className={`function-item ${activeEntity === entityId ? 'active' : ''}`} onClick={() => handleOpenEntity(entityId)} > {ENTITY_ICONS[entityId]} {ENTITY_LABELS[entityId]} </div> ))} </div> {/* Main Content */} <div className="main-layout"> {/* Content Area */} <div className="content-area"> {activeSection === 'dashboard' ? ( <Dashboard /> ) : activeEntity === 'userSettings' ? ( <UserSettings user={user} onOperationalDateChange={setOperationalDate} onNavigate={setActiveEntity} /> ) : activeEntity === 'importRules' ? ( <ImportRulesSettings user={user} /> ) : activeEntity === 'bankImport' ? ( <BankImportWizard user={user} /> ) : activeEntity === 'dataTransfer' ? ( <AdminDataTransfer /> ) : activeEntity === 'budgets' ? ( showBudgetForm ? ( <BudgetForm item={budgetEditItem} onSubmit={() => { setShowBudgetForm(false); setBudgetEditItem(null); window.dispatchEvent(new CustomEvent('budgets-refresh')); }} onCancel={() => { setShowBudgetForm(false); setBudgetEditItem(null); }} isMobile={isMobile} lookups={lookups} /> ) : ( <BudgetList onCreate={() => { setBudgetEditItem(null); setShowBudgetForm(true); }} onEdit={(item) => { setBudgetEditItem(item); setShowBudgetForm(true); }} isMobile={isMobile} lookups={lookups} /> ) ) : activeEntity === 'budgetsIncome' ? ( showBudgetIncomeForm ? ( <BudgetIncomeForm item={budgetIncomeEditItem} onSubmit={() => { setShowBudgetIncomeForm(false); setBudgetIncomeEditItem(null); window.dispatchEvent(new CustomEvent('budgets-income-refresh')); }} onCancel={() => { setShowBudgetIncomeForm(false); setBudgetIncomeEditItem(null); }} isMobile={isMobile} lookups={lookups} /> ) : ( <BudgetIncomeList onCreate={() => { setBudgetIncomeEditItem(null); setShowBudgetIncomeForm(true); }} onEdit={(item) => { setBudgetIncomeEditItem(item); setShowBudgetIncomeForm(true); }} isMobile={isMobile} lookups={lookups} /> ) ) : activeEntity === 'returns' ? ( showReturnForm ? ( <ReturnForm item={returnEditItem} defaultExpenseId={returnDefaultExpenseId} onSubmit={() => { setShowReturnForm(false); setReturnEditItem(null); setReturnDefaultExpenseId(null); }} onCancel={() => { setShowReturnForm(false); setReturnEditItem(null); setReturnDefaultExpenseId(null); }} isMobile={isMobile} lookups={lookups} /> ) : ( <ReturnList onCreate={() => { setReturnEditItem(null); setReturnDefaultExpenseId(null); setShowReturnForm(true); }} onEdit={(item) => { setReturnEditItem(item); setReturnDefaultExpenseId(null); setShowReturnForm(true); }} isMobile={isMobile} lookups={lookups} /> ) ) : activeEntity.startsWith('report') ? ( <ReportsPage isMobile={isMobile} lookups={lookups} activeReport={activeReportType} /> ) : activeEntity.startsWith('trash') ? ( <TrashList isMobile={isMobile} lookups={lookups} filterType={{ trashAll: '', trashExpenses: 'expense', trashIncomes: 'income', trashTransfers: 'transfer', trashLoansGiven: 'loangiven', trashLoansTaken: 'loantaken', trashDeposits: 'deposit', trashReturns: 'return', }[activeEntity] || ''} /> ) : ( <EntityList entity={activeEntity} onClose={() => setActiveEntity('accounts')} operationalDate={operationalDate} drilldownFilters={drilldownFilters} onDrilldownApplied={() => setDrilldownFilters(null)} onCreateReturn={(expenseId) => { setActiveSection('documents'); setActiveEntity('returns'); setSectionEntityMap(prev => ({ ...prev, documents: 'returns' })); setReturnEditItem(null); setReturnDefaultExpenseId(expenseId); setShowReturnForm(true); }} /> )} </div> </div> </div> ); }