// Калькулятор ЗП — top-level App // Composes: corner micro-labels, header, upload zone, KPI block, // controls + employees table, summary, sheet modal. const { useState, useMemo, useCallback, useEffect } = React; function App() { // Стартуем с пустого экрана — пользователь выбирает: загрузить файлы или демо. const [loaded, setLoaded] = useState(false); const [mode, setMode] = useState('empty'); // 'empty' | 'demo' | 'real' const [sotrudniki, setSotrudniki] = useState([]); const [tabel, setTabel] = useState([]); const [kpi, setKpi] = useState({ period: '', revPlan: '', revFact: '', writeoff: '' }); const [sotrudnikiInfo, setSotrudnikiInfo] = useState(''); const [tabelInfo, setTabelInfo] = useState(''); // Controls state const [search, setSearch] = useState(''); const [filterDol, setFilterDol] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [sort, setSort] = useState(''); // Sheet modal const [sheetRow, setSheetRow] = useState(null); const computed = useMemo( () => computeAll(sotrudniki, tabel, kpi), [sotrudniki, tabel, kpi] ); const dolzhnosti = useMemo( () => [...new Set(sotrudniki.map(s => s.dolzhnost).filter(Boolean))], [sotrudniki] ); const filteredRows = useMemo(() => { let data = computed.rows.slice(); if (search) { const q = search.toLowerCase(); data = data.filter(r => r.fio.toLowerCase().includes(q)); } if (filterDol) data = data.filter(r => r.dolzhnost === filterDol); if (filterStatus === 'активен') data = data.filter(r => r._active); if (filterStatus === 'неактивен') data = data.filter(r => !r._active); if (sort === 'fio') data.sort((a, b) => a.fio.localeCompare(b.fio, 'ru')); if (sort === 'dolzhnost') data.sort((a, b) => a.dolzhnost.localeCompare(b.dolzhnost, 'ru')); if (sort === 'vyruchka') data.sort((a, b) => (b.vyruchka || 0) - (a.vyruchka || 0)); if (sort === 'nachisleno') data.sort((a, b) => (b.nachisleno || 0) - (a.nachisleno || 0)); if (sort === 'vydacha') data.sort((a, b) => (b.vydacha || 0) - (a.vydacha || 0)); return data; }, [computed.rows, search, filterDol, filterStatus, sort]); const handleLoadDemo = useCallback(() => { setSotrudniki(DEMO_SOTRUDNIKI); setTabel(DEMO_TABEL); setKpi(DEMO_KPI); setSotrudnikiInfo(`Демо · ${DEMO_SOTRUDNIKI.length} сотрудников`); setTabelInfo(`Демо · ${DEMO_TABEL.length} записей`); setLoaded(true); setMode('demo'); }, []); const handleReset = useCallback(() => { setSotrudniki([]); setTabel([]); setKpi({ period: '', revPlan: '', revFact: '', writeoff: '' }); setSotrudnikiInfo(''); setTabelInfo(''); setLoaded(false); setMode('empty'); setSearch(''); setFilterDol(''); setFilterStatus(''); setSort(''); }, []); const handleUploadSotrudniki = useCallback(async (file) => { try { const data = await readXlsxFile(file, 'sotrudniki'); if (!data.length) throw new Error('В файле нет строк с ФИО'); setSotrudniki(data); setSotrudnikiInfo(`✓ Загружено: ${data.length} сотрудников`); setMode('real'); setLoaded(true); } catch (err) { alert('Ошибка чтения справочника: ' + (err.message || err)); } }, []); const handleUploadTabel = useCallback(async (file) => { try { const data = await readXlsxFile(file, 'tabel'); if (!data.length) throw new Error('В файле нет строк с ФИО'); setTabel(data); setTabelInfo(`✓ Загружено: ${data.length} записей`); if (data[0] && data[0].period) { setKpi(prev => ({ ...prev, period: data[0].period })); } setMode('real'); setLoaded(true); } catch (err) { alert('Ошибка чтения табеля: ' + (err.message || err)); } }, []); const handleExportExcel = useCallback(() => { if (!filteredRows.length) { alert('Нет данных для экспорта.'); return; } downloadXlsxSummary(filteredRows, kpi.period); }, [filteredRows, kpi.period]); const handleExportPdf = useCallback(() => { if (!filteredRows.length) { alert('Нет данных для экспорта.'); return; } downloadPdfSummary(filteredRows, kpi.period, computed.totals); }, [filteredRows, kpi.period, computed.totals]); const handleSheetExportExcel = useCallback(() => { if (sheetRow) downloadXlsxSheet(sheetRow, kpi.period); }, [sheetRow, kpi.period]); const handleSheetExportPdf = useCallback(() => { if (sheetRow) downloadPdfSheet(sheetRow, kpi.period); }, [sheetRow, kpi.period]); const handleOpenSheet = useCallback((i) => { setSheetRow(filteredRows[i]); }, [filteredRows]); return ( <>
Калькулятор ждёт справочник сотрудников и табель с выручкой. Или нажмите ниже — посмотрим, как это выглядит.