const COMPANIES = [ { id: 'c1', name: 'КТ-Алматы', template: 'Шаблон 2024-А' }, { id: 'c2', name: 'КТ-Астана', template: 'Шаблон 2023-Б' }, { id: 'c3', name: 'КТ-Шымкент', template: 'Шаблон 2024-А' }, { id: 'c4', name: 'КТ-Караганда', template: 'Шаблон 2022-Г' }, { id: 'c5', name: 'КТ-Актау', template: 'Фирменный' }, { id: 'c6', name: 'КТ-Павлодар', template: 'Шаблон 2023-Б' }, { id: 'c7', name: 'КТ-Семей', template: 'Шаблон 2024-А' }, { id: 'c8', name: 'КТ-Костанай', template: 'Шаблон 2022-Г' }, { id: 'c9', name: 'КТ-Тараз', template: 'Фирменный' }, { id: 'c10', name: 'КТ-Усть-Каменогорск', template: 'Шаблон 2023-Б' }, { id: 'c11', name: 'КТ-Атырау', template: 'Шаблон 2024-А' }, { id: 'c12', name: 'КТ-Петропавловск', template: 'Фирменный' }, { id: 'c13', name: 'КТ-Кызылорда', template: 'Шаблон 2022-Г' }, { id: 'c14', name: 'КТ-Уинт', template: 'Шаблон 2023-Б' }, { id: 'c15', name: 'КТ-Каспий', template: 'Фирменный' } ]; const METRICS = [ { key: 'revenue', name: 'Выручка', unit: 'млн тг' }, { key: 'profit', name: 'Прибыль', unit: 'млн тг' }, { key: 'expenses', name: 'Расходы', unit: 'млн тг' } ]; const METRIC_SOURCE = { revenue: 'Выручка от продаж (млн тг)', profit: 'Чистая прибыль, тыс. тенге', expenses: 'Общие расходы (млн)' }; const STORAGE_KEY = 'svod-fin-otchetov-state-v1'; function now() { const d = new Date(); const p = (n) => String(n).padStart(2, '0'); return p(d.getDate()) + '.' + p(d.getMonth() + 1) + '.' + d.getFullYear(); } function nowTime() { const d = new Date(); const p = (n) => String(n).padStart(2, '0'); return p(d.getHours()) + ':' + p(d.getMinutes()); } function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function randDecimal(min, max, dec) { return (Math.random() * (max - min) + min).toFixed(dec); } function generateDefaultFile(company, idx) { const statuses = ['done', 'done', 'done', 'done', 'done', 'done', 'done', 'done', 'done', 'processing', 'processing', 'waiting', 'waiting', 'waiting', 'waiting']; return { id: 'f' + (idx + 1), company: company.id, companyName: company.name, fileName: 'financial_report_' + company.id + '_' + (2024 + (idx % 3)) + '.xlsx', sentDate: (20 + (idx % 10)) + '.09.2026', template: company.template, status: statuses[idx] || 'waiting' }; } function generateDefaultRows() { const rows = []; const pattern = ['ai', 'ai', 'ai', 'ai-uncertain', 'manual']; COMPANIES.forEach(function (c, ci) { METRICS.forEach(function (m, mi) { const base = mi === 0 ? 4200 : mi === 1 ? 1120 : 3100; const value = Number(randDecimal(base - 400, base + 400, 2)); rows.push({ id: c.id + '-' + m.key, company: c.id, metric: m.key, key: m.key, metricName: m.name, unit: m.unit, value: value, source: c.template + ' / лист «Отчёт» / B' + ((ci + 2) * 7 + mi + 3), status: pattern[(ci + mi) % 5], originalName: m.name }); }); }); return rows; } function defaultState() { const files = COMPANIES.map(generateDefaultFile); const rows = generateDefaultRows(); return { screen: 'upload', files: files, rows: rows, aiLog: [ { time: '09:14', text: 'Найдено 24 значения показателя «Выручка»', src: 'КТ-Алматы, КТ-Астана, …' }, { time: '09:14', text: 'Показатель «Чистая прибыль, тыс. тенге» стандартизирован как «Прибыль»', src: 'КТ-Актау, КТ-Тараз' }, { time: '09:15', text: '4 значения требуют ручного подтверждения', src: 'см. выделенные ячейки' }, { time: '09:16', text: 'Единая сводная таблица заполнена', src: '15 компаний × 3 показателя' } ], recheckLog: [], confirmed: false, chat: [ { role: 'bot', text: 'Здравствуйте! Спросите меня про показатели, компании или статус обработки.' } ] }; } function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); if (parsed && parsed.files && parsed.rows) { return parsed; } } } catch (e) {} return defaultState(); } function saveState() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } let state = loadState(); function el(id) { return document.getElementById(id); } function toast(msg, tone) { const stack = el('toastStack'); if (!stack) return; const t = document.createElement('div'); t.className = 'kt-ai-toast'; if (tone) t.setAttribute('data-tone', tone); t.textContent = msg; stack.appendChild(t); setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 2600); } function companyById(id) { for (let i = 0; i < COMPANIES.length; i++) { if (COMPANIES[i].id === id) return COMPANIES[i]; } return null; } function rowsForCompany(companyId) { const out = []; for (let i = 0; i < state.rows.length; i++) { if (state.rows[i].company === companyId) out.push(state.rows[i]); } return out; } function allRowsForMetric(metricKey) { const out = []; for (let i = 0; i < state.rows.length; i++) { if (state.rows[i].metric === metricKey) out.push(state.rows[i]); } return out; } function sumMetric(metricKey) { const rows = allRowsForMetric(metricKey); let s = 0; for (let i = 0; i < rows.length; i++) s += rows[i].value; return s; } function statusLabel(status) { const map = { waiting: 'Ожидает', processing: 'Обработка', done: 'Готово', ai: 'Найдено ИИ', 'ai-uncertain': 'Требует проверки', manual: 'Ручной режим' }; return map[status] || status; } function statusPillHtml(status) { const map = { waiting: 'muted', processing: 'info', done: 'ok', ai: 'ok', 'ai-uncertain': 'warn', manual: 'muted' }; const tone = map[status] || 'muted'; return '' + statusLabel(status) + ''; } function fileStatusPillHtml(status) { const map = { waiting: 'muted', processing: 'info', done: 'ok' }; const tone = map[status] || 'muted'; return '' + statusLabel(status) + ''; } function navUploadKpi() { const total = state.files.length; let done = 0, processing = 0, waiting = 0; state.files.forEach(function (f) { if (f.status === 'done') done++; else if (f.status === 'processing') processing++; else waiting++; }); const html = '
' + total + 'Всего файлов
' + '
' + done + 'Готово
' + '
' + processing + 'В обработке
' + '
' + waiting + 'Ожидает
'; return html; } function fileRowHtml(f) { const actions = '
' + '' + '' + '
'; return '' + '' + f.companyName + '' + 'Excel ' + f.fileName + '' + '' + f.sentDate + '' + '' + f.template + '' + '' + fileStatusPillHtml(f.status) + '' + '' + actions + '' + ''; } function renderUpload() { const box = el('uploadKpi'); if (box) box.innerHTML = navUploadKpi(); const tbody = el('uploadTableBody'); if (tbody) { tbody.innerHTML = state.files.map(fileRowHtml).join(''); } const count = el('navUploadCount'); if (count) count.textContent = state.files.length; } function summaryKpiHtml() { let ok = 0, warn = 0, manual = 0; state.rows.forEach(function (r) { if (r.status === 'ai') ok++; else if (r.status === 'ai-uncertain') warn++; else if (r.status === 'manual') manual++; }); const rev = sumMetric('revenue'); return '
' + rev.toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + 'Суммарная выручка, млн тг
' + '
' + ok + 'Значений найдено ИИ
' + '
' + warn + 'Требуют проверки
' + '
' + manual + 'В ручном режиме
'; } function fmtValue(r) { const dec = r.unit === 'млн тг' ? 1 : 2; let s = r.value.toFixed(dec); const parts = s.split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '\u00a0'); return parts.join(','); } function rowActionsForSummary(r) { let actions = ''; if (r.status === 'ai-uncertain') { actions = ''; } else if (r.status === 'ai') { actions = ''; } else if (r.status === 'manual') { actions = ''; } return actions; } function summaryRowHtml(r) { const c = companyById(r.company); const cellCls = r.status === 'ai-uncertain' ? 'cell kt-ai-cell-warning' : (r.status === 'manual' ? 'cell kt-ai-cell-manual' : 'cell'); const showRename = r.originalName !== r.metricName; const renameTxt = showRename ? 'Было: ' + r.originalName + ' → ' + r.metricName : r.originalName; return '' + '' + (c ? c.name : r.company) + '' + '
' + '' + r.metricName + '' + (showRename ? '' + renameTxt + '' : '') + '
' + '' + fmtValue(r) + ' ' + r.unit + '' + '' + r.source + '' + '
' + statusPillHtml(r.status) + '
' + rowActionsForSummary(r) + '
' + ''; } function renderSummary() { const box = el('summaryKpi'); if (box) box.innerHTML = summaryKpiHtml(); const tbody = el('summaryTableBody'); if (tbody) tbody.innerHTML = state.rows.map(summaryRowHtml).join(''); const count = el('navSummaryCount'); if (count) count.textContent = state.rows.length; } function renderAiLog() { const list = el('aiLogList'); if (!list) return; list.innerHTML = state.aiLog.map(function (l) { return '
' + '' + l.time + '' + '' + l.text + '' + '' + l.src + '' + '
'; }).join(''); } function renderAll() { renderUpload(); renderSummary(); renderAiLog(); renderVerify(); renderDashboard(); updateNav(); } /* ================= Navigation ================= */ const SCREEN_TITLES = { upload: 'Загрузка отчётов', summary: 'Сводная таблица', verify: 'Проверка и утверждение', dashboard: 'Дашборд руководства', about: 'О решении' }; function showScreen(id) { const screens = document.querySelectorAll('.kt-ai-screen'); screens.forEach(function (s) { s.hidden = true; }); const target = el('screen-' + id); if (target) target.hidden = false; state.screen = id; saveState(); const crumb = el('topbarCrumb'); if (crumb) crumb.textContent = SCREEN_TITLES[id] || id; const navItems = document.querySelectorAll('.kt-ai-nav-item'); navItems.forEach(function (n) { if (n.getAttribute('data-screen') === id) n.setAttribute('data-active', 'true'); else n.removeAttribute('data-active'); }); renderScreen(id); } function renderScreen(id) { if (id === 'upload') renderUpload(); if (id === 'summary') renderSummary(); if (id === 'verify') renderVerify(); if (id === 'dashboard') renderDashboard(); } function updateTopbar() { const actions = el('topbarActions'); if (!actions) return; actions.innerHTML = ''; } function updateNav() { const navItems = document.querySelectorAll('.kt-ai-nav-item'); navItems.forEach(function (n) { const id = n.getAttribute('data-screen'); if (id === state.screen) n.setAttribute('data-active', 'true'); else n.removeAttribute('data-active'); }); } function bindNavigation() { const nav = el('sidebarNav'); if (nav) { nav.addEventListener('click', function (e) { const item = e.target.closest('.kt-ai-nav-item'); if (!item) return; const id = item.getAttribute('data-screen'); if (id) showScreen(id); }); } const btnNew = el('btnNewReport'); if (btnNew) btnNew.addEventListener('click', function () { showScreen('upload'); }); } /* ================= Upload screen ================= */ function bindUpload() { const btnProcessAll = el('btnProcessAll'); if (btnProcessAll) btnProcessAll.addEventListener('click', processAllFiles); const btnDeleteWaiting = el('btnDeleteAllWaiting'); if (btnDeleteWaiting) btnDeleteWaiting.addEventListener('click', function () { const before = state.files.length; state.files = state.files.filter(function (f) { return f.status !== 'waiting'; }); const removed = before - state.files.length; if (removed > 0) { saveState(); renderUpload(); toast('Удалено файлов: ' + removed); } else { toast('Ожидающих файлов нет'); } }); const statusFilter = el('uploadStatusFilter'); if (statusFilter) statusFilter.addEventListener('change', applyUploadFilter); const search = el('uploadSearch'); if (search) search.addEventListener('input', applyUploadFilter); const tbody = el('uploadTableBody'); if (tbody) { tbody.addEventListener('click', function (e) { const btn = e.target.closest('button[data-act]'); if (!btn) return; const act = btn.getAttribute('data-act'); const id = btn.getAttribute('data-id'); if (act === 'file-process') processFile(id); if (act === 'file-delete') deleteFile(id); }); } } function applyUploadFilter() { const status = el('uploadStatusFilter') ? el('uploadStatusFilter').value : 'all'; const search = el('uploadSearch') ? el('uploadSearch').value.toLowerCase() : ''; const rows = state.files.filter(function (f) { const okStatus = status === 'all' || f.status === status; const okSearch = !search || f.companyName.toLowerCase().includes(search) || f.fileName.toLowerCase().includes(search); return okStatus && okSearch; }); // render filtered const tbody = el('uploadTableBody'); if (tbody) tbody.innerHTML = rows.map(fileRowHtml).join(''); } function processAllFiles() { const waiting = state.files.filter(function (f) { return f.status === 'waiting' || f.status === 'processing'; }); if (!waiting.length) { toast('Все файлы уже обработаны'); return; } waiting.forEach(function (f) { f.status = 'processing'; }); saveState(); renderUpload(); toast('Обработка запущена для ' + waiting.length + ' файлов…'); let i = 0; function next() { if (i >= waiting.length) { finishProcessing(waiting); return; } const f = waiting[i]; f.status = 'done'; i++; saveState(); renderUpload(); setTimeout(next, 220); } setTimeout(next, 300); } function processFile(id) { let f = null; state.files.forEach(function (x) { if (x.id === id) f = x; }); if (!f) return; if (f.status === 'done') { toast('Файл уже обработан'); return; } f.status = 'processing'; saveState(); renderUpload(); setTimeout(function () { f.status = 'done'; state.aiLog.unshift({ time: nowTime(), text: 'Показатели компании «' + f.companyName + '» найдены и добавлены в сводную таблицу', src: f.fileName }); saveState(); renderUpload(); renderAiLog(); toast('Файл «' + f.companyName + '» обработан'); }, 600); } function deleteFile(id) { state.files = state.files.filter(function (f) { return f.id !== id; }); saveState(); renderUpload(); toast('Файл удалён'); } function finishProcessing(files) { const names = files.map(function (f) { return f.companyName; }); state.aiLog.unshift({ time: nowTime(), text: 'ИИ завершил обработку файлов', src: names.join(', ') }); saveState(); renderAiLog(); toast('Обработка завершена'); } /* ================= Summary screen ================= */ function bindSummary() { const btnSave = el('btnSaveSummary'); if (btnSave) btnSave.addEventListener('click', function () { saveState(); toast('Изменения сохранены'); }); const btnRerun = el('btnRerunAi'); if (btnRerun) btnRerun.addEventListener('click', function () { state.aiLog.unshift({ time: nowTime(), text: 'Пересчёт соответствий показателей выполнен', src: '15 компаний' }); saveState(); renderSummary(); renderAiLog(); toast('ИИ пересчитал соответствия'); }); const btnClearLog = el('btnClearAiLog'); if (btnClearLog) btnClearLog.addEventListener('click', function () { state.aiLog = []; saveState(); renderAiLog(); }); // filters const companySel = el('summaryCompanyFilter'); if (companySel) { METRICS; // no-op const cur = companySel.value; companySel.innerHTML = '' + COMPANIES.map(function (c) { return ''; }).join(''); companySel.value = cur; companySel.addEventListener('change', applySummaryFilter); } const statusSel = el('summaryStatusFilter'); if (statusSel) statusSel.addEventListener('change', applySummaryFilter); const search = el('summarySearch'); if (search) search.addEventListener('input', applySummaryFilter); const tbody = el('summaryTableBody'); if (tbody) { tbody.addEventListener('click', function (e) { const btn = e.target.closest('button[data-act]'); if (btn) { const act = btn.getAttribute('data-act'); const id = btn.getAttribute('data-id'); if (act === 'confirm-row') confirmRow(id); if (act === 'manual-row') manualRow(id); return; } const cell = e.target.closest('[data-cell]'); if (cell) editCell(cell.getAttribute('data-cell')); }); } } function applySummaryFilter() { const c = el('summaryCompanyFilter') ? el('summaryCompanyFilter').value : 'all'; const s = el('summaryStatusFilter') ? el('summaryStatusFilter').value : 'all'; const q = el('summarySearch') ? el('summarySearch').value.toLowerCase() : ''; const tbody = el('summaryTableBody'); if (!tbody) return; const filtered = state.rows.filter(function (r) { const okC = c === 'all' || r.company === c; const okS = s === 'all' || r.status === s; const okQ = !q || r.metricName.toLowerCase().includes(q) || (companyById(r.company) || { name: '' }).name.toLowerCase().includes(q); return okC && okS && okQ; }); tbody.innerHTML = filtered.map(summaryRowHtml).join(''); } function findRow(id) { for (let i = 0; i < state.rows.length; i++) { if (state.rows[i].id === id) return state.rows[i]; } return null; } function editCell(id) { const r = findRow(id); if (!r) return; const cur = prompt('Введите новое значение (' + r.unit + '):', r.value); if (cur === null) return; const num = parseFloat(String(cur).replace(',', '.')); if (isNaN(num)) { toast('Некорректное значение'); return; } r.value = num; r.status = 'manual'; state.aiLog.unshift({ time: nowTime(), text: 'Значение «' + r.metricName + '» для «' + companyById(r.company).name + '» изменено вручную', src: r.source }); saveState(); applySummaryFilter(); renderAiLog(); toast('Значение обновлено'); } function confirmRow(id) { const r = findRow(id); if (!r) return; r.status = 'manual'; state.aiLog.unshift({ time: nowTime(), text: 'Соответствие «' + r.metricName + '» подтверждено вручную', src: r.source }); saveState(); applySummaryFilter(); renderAiLog(); toast('Показатель подтверждён'); } function manualRow(id) { const r = findRow(id); if (!r) return; r.status = 'manual'; saveState(); applySummaryFilter(); toast('Переведено в ручной режим'); } /* ================= Verify screen ================= */ function renderVerify() { const box = el('verifyKpi'); if (!box) return; // final table body const tbody = el('verifyTableBody'); if (tbody) { tbody.innerHTML = state.rows.map(function (r) { const c = companyById(r.company); const checkStatus = r.status === 'manual' ? 'Ручная проверка' : (r.status === 'ai' ? 'ИИ' : 'ИИ'); const pill = r.status === 'manual' ? 'Ручная проверка' : 'ИИ'; return '' + '' + (c ? c.name : r.company) + '' + '' + r.metricName + '' + '' + fmtValue(r) + ' ' + r.unit + '' + '' + pill + '' + ''; }).join(''); } // company / metric selects const cs = el('recheckCompanySelect'); if (cs) { const cur = cs.value || 'c1'; cs.innerHTML = COMPANIES.map(function (c) { return ''; }).join(''); const exists = state.rows.some(function (r) { return r.company === cur; }); cs.value = exists ? cur : 'c1'; } const ms = el('recheckMetricSelect'); if (ms) { const cid = cs.value; const compRows = rowsForCompany(cid); const metricIds = compRows.map(function (r) { return r.metric; }); const cur = ms.value; ms.innerHTML = METRICS.map(function (m) { const val = compRows.filter(function (r) { return r.metric === m.key; })[0]; return ''; }).join(''); if (metricIds.indexOf(cur) === -1) ms.value = metricIds[0] || 'revenue'; } // KPIs let rechecked = state.recheckLog.length; let passed = state.recheckLog.filter(function (x) { return x.match; }).length; const total = state.rows.length; const confirmedText = state.confirmed ? 'Подтверждена' : 'Не подтверждена'; const confirmedPill = state.confirmed ? 'Готово к выдаче' : 'Ожидает утверждения'; box.innerHTML = '
' + total + 'Записей в итоговой таблице
' + '
' + rechecked + 'Выборочно сверено
' + '
' + passed + 'Совпадений
' + '
' + confirmedText + 'Статус готовности' + confirmedPill + '
'; renderRecheckLog(); } function renderRecheckLog() { const box = el('recheckLog'); if (!box) return; box.innerHTML = state.recheckLog.map(function (x) { const cls = x.match ? 'ok' : 'bad'; const txt = x.match ? 'Совпадение' : 'Отклонение ' + x.diff.toFixed(2); return '
' + x.company + ', ' + x.metric + '' + txt + '
'; }).join(''); } function bindVerify() { const cs = el('recheckCompanySelect'); if (cs) cs.addEventListener('change', function () { const cid = cs.value; const ms = el('recheckMetricSelect'); if (ms) ms.innerHTML = METRICS.map(function (m) { const val = rowsForCompany(cid).filter(function (r) { return r.metric === m.key; })[0]; return ''; }).join(''); }); const btnRecheck = el('btnDoRecheck'); if (btnRecheck) btnRecheck.addEventListener('click', doRecheck); const btnConfirm = el('btnConfirmFinal'); if (btnConfirm) btnConfirm.addEventListener('click', function () { state.confirmed = true; saveState(); renderVerify(); toast('Итоговая таблица подтверждена'); }); const btnBack = el('btnBackToSummary'); if (btnBack) btnBack.addEventListener('click', function () { showScreen('summary'); }); const btnSendBack = el('btnSendBack'); if (btnSendBack) btnSendBack.addEventListener('click', function () { let n = 0; state.rows.forEach(function (r) { if (r.status === 'ai') { r.status = 'ai-uncertain'; n++; } }); state.confirmed = false; state.aiLog.unshift({ time: nowTime(), text: 'Таблица отправлена на доработку: значения возвращены в режим проверки', src: n + ' ячеек' }); saveState(); renderVerify(); renderAiLog(); toast('Таблица отправлена на доработку', 'warn'); }); const btnDl = el('btnDownloadExcel'); if (btnDl) btnDl.addEventListener('click', downloadXlsx); } function doRecheck() { const cid = el('recheckCompanySelect').value; const mk = el('recheckMetricSelect').value; const manual = parseFloat(el('recheckManualValue').value); if (isNaN(manual)) { toast('Введите значение для сверки'); return; } const row = state.rows.filter(function (r) { return r.company === cid && r.metric === mk; })[0]; const c = companyById(cid); const m = METRICS.filter(function (x) { return x.key === mk; })[0]; const diff = manual - row.value; const match = Math.abs(diff) < 0.01; state.recheckLog.unshift({ company: c.name, metric: m.name, diff: diff, match: match }); saveState(); renderVerify(); const res = el('recheckResult'); res.hidden = false; const diffEl = el('recheckDiff'); const statEl = el('recheckStatus'); diffEl.textContent = 'Отклонение: ' + (match ? '0.00' : diff.toFixed(2)) + ' ' + m.unit; if (match) { statEl.innerHTML = 'Совпадение'; toast('Данные совпадают'); } else { statEl.innerHTML = 'Расхождение'; toast('Расхождение ' + diff.toFixed(2), 'warn'); } } /* ================= Excel export ================= */ function csvEscape(v) { const s = String(v); if (/[",\n;]/.test(s)) { return '"' + s.replace(/"/g, '""') + '"'; } return s; } function buildSheetXml(headers, rows) { let xml = '\n' + '' + ''; headers.forEach(function (h) { xml += '' + h + ''; }); rows.forEach(function (r) { xml += ''; r.forEach(function (cell, ci) { const type = typeof cell === 'number' ? 'Number' : 'String'; xml += '' + cell + ''; }); xml += ''; }); xml += '
'; return xml; } function addZipU32(buf, off, v) { buf[off] = v & 0xff; buf[off + 1] = (v >> 8) & 0xff; buf[off + 2] = (v >> 16) & 0xff; buf[off + 3] = (v >> 24) & 0xff; } function addZipU16(buf, off, v) { buf[off] = v & 0xff; buf[off + 1] = (v >> 8) & 0xff; } function crc32(buf) { let c, table = crc32.table; if (!table) { table = crc32.table = []; for (let n = 0; n < 256; n++) { c = n; for (let k = 0; k < 8; k++) { c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); } table[n] = c >>> 0; } } let crc = 0 ^ (-1); for (let i = 0; i < buf.length; i++) { crc = (crc >>> 8) ^ table[(crc ^ buf[i]) & 0xFF]; } return (crc ^ (-1)) >>> 0; } function makeZip(files) { const chunks = []; const central = []; let offset = 0; const enc = new TextEncoder(); files.forEach(function (file) { const nameBytes = enc.encode(file.name); const dataBytes = enc.encode(file.data); const crc = crc32(dataBytes); const local = new ArrayBuffer(30 + nameBytes.length); const lview = new DataView(local); addZipU32(lview, 0, 0x04034b50); addZipU16(lview, 4, 20); addZipU16(lview, 6, 0); addZipU16(lview, 8, 0); addZipU16(lview, 10, 0); addZipU16(lview, 12, 0); addZipU32(lview, 14, crc); addZipU32(lview, 18, dataBytes.length); addZipU32(lview, 22, dataBytes.length); addZipU16(lview, 26, nameBytes.length); addZipU16(lview, 28, 0); const larr = new Uint8Array(local); larr.set(nameBytes, 30); chunks.push(larr, dataBytes); const cen = new ArrayBuffer(46 + nameBytes.length); const cview = new DataView(cen); addZipU32(cview, 0, 0x02014b50); addZipU16(cview, 4, 20); addZipU16(cview, 6, 20); addZipU16(cview, 8, 0); addZipU16(cview, 10, 0); addZipU16(cview, 12, 0); addZipU16(cview, 14, 0); addZipU32(cview, 16, crc); addZipU32(cview, 20, dataBytes.length); addZipU32(cview, 24, dataBytes.length); addZipU16(cview, 28, nameBytes.length); addZipU16(cview, 30, 0); addZipU16(cview, 32, 0); addZipU16(cview, 34, 0); addZipU16(cview, 36, 0); addZipU32(cview, 38, 0); addZipU32(cview, 42, offset); const carr = new Uint8Array(cen); carr.set(nameBytes, 46); central.push(carr); offset += larr.length + dataBytes.length; }); const centralSize = central.reduce(function (s, a) { return s + a.length; }, 0); const eocd = new ArrayBuffer(22); const ev = new DataView(eocd); addZipU32(ev, 0, 0x06054b50); addZipU16(ev, 4, 0); addZipU16(ev, 6, 0); addZipU16(ev, 8, files.length); addZipU16(ev, 10, files.length); addZipU32(ev, 12, centralSize); addZipU32(ev, 16, offset); addZipU16(ev, 20, 0); return new Blob(chunks.concat(central, [new Uint8Array(eocd)]), { type: 'application/vnd.ms-excel' }); } function downloadXlsx() { const headers = ['Компания', 'Показатель', 'Значение', 'Единица', 'Источник', 'Статус проверки']; const rows = state.rows.map(function (r) { const c = companyById(r.company); return [c.name, r.metricName, r.value, r.unit, r.source, statusLabel(r.status)]; }); const sheet = buildSheetXml(headers, rows); const ct = '\r\n' + '' + '' + '' + '' + ''; const rels = '\r\n' + '' + ''; const wb = '\r\n' + ''; const wbRels = '\r\n' + '' + ''; const blob = makeZip([ { name: '[Content_Types].xml', data: ct }, { name: '_rels/.rels', data: rels }, { name: 'xl/workbook.xml', data: wb }, { name: 'xl/_rels/workbook.xml.rels', data: wbRels }, { name: 'xl/worksheets/sheet1.xml', data: sheet } ]); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'svod_finansovyh_otchetov.xlsx'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast('Единый Excel-файл сформирован'); } /* ================= Dashboard screen ================= */ function renderDashboard() { const metric = el('dashChartMetric') ? el('dashChartMetric').value : 'revenue'; const box = el('dashKpi'); if (box) { const rev = sumMetric('revenue'); const prof = sumMetric('profit'); const exp = sumMetric('expenses'); const ratio = rev ? (prof / rev * 100) : 0; box.innerHTML = '
' + rev.toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + 'Суммарная выручка, млн тг
' + '
' + prof.toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + 'Суммарная прибыль, млн тг
' + '
' + exp.toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + 'Суммарные расходы, млн тг
' + '
' + ratio.toLocaleString('ru-RU', { maximumFractionDigits: 1 }) + '%Рентабельность по выручкеприбыль / выручка
'; } renderDashChart(metric); renderChatLog(); } function renderDashChart(metric) { const chart = el('dashChart'); if (!chart) return; const values = COMPANIES.map(function (c) { const r = state.rows.filter(function (x) { return x.company === c.id && x.metric === metric; })[0]; return { name: c.name, value: r ? r.value : 0 }; }); const max = Math.max.apply(null, values.map(function (v) { return v.value; })); const m = METRICS.filter(function (x) { return x.key === metric; })[0]; chart.innerHTML = values.map(function (v) { const h = max ? (v.value / max) * 100 : 0; return '
' + '' + v.name.replace('КТ-', '') + '' + '
'; }).join(''); } function renderChatLog() { const log = el('chatLog'); if (!log) return; log.innerHTML = state.chat.map(function (m) { return '
' + m.text + '
'; }).join(''); log.scrollTop = log.scrollHeight; } function bindDashboard() { const sel = el('dashChartMetric'); if (sel) sel.addEventListener('change', function () { renderDashChart(sel.value); }); const form = el('chatForm'); if (form) form.addEventListener('submit', function (e) { e.preventDefault(); const input = el('chatInput'); const q = input.value.trim(); if (!q) return; input.value = ''; state.chat.push({ role: 'user', text: q }); state.chat.push({ role: 'bot', text: chatReply(q) }); saveState(); renderChatLog(); }); // simple chart tooltip via title fallback document.addEventListener('mouseover', function (e) { const tip = e.target.closest('[data-kt-tip]'); if (tip) { /* handled by design-system if present */ } }); } function chatReply(text) { const q = text.toLowerCase(); const rev = sumMetric('revenue'); const prof = sumMetric('profit'); const exp = sumMetric('expenses'); const done = state.files.filter(function (f) { return f.status === 'done'; }).length; const waiting = state.files.filter(function (f) { return f.status === 'waiting'; }).length; const processing = state.files.filter(function (f) { return f.status === 'processing'; }).length; if (q.includes('выручк')) { const max = allRowsForMetric('revenue').slice().sort(function (a, b) { return b.value - a.value; })[0]; return 'Суммарная выручка — ' + rev.toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + ' млн тг. Максимум у «' + companyById(max.company).name + '»: ' + fmtValue(max) + ' млн тг.'; } if (q.includes('прибыл')) { const max = allRowsForMetric('profit').slice().sort(function (a, b) { return b.value - a.value; })[0]; return 'Суммарная прибыль — ' + prof.toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + ' млн тг. Максимум у «' + companyById(max.company).name + '»: ' + fmtValue(max) + ' млн тг.'; } if (q.includes('расход')) { return 'Суммарные расходы — ' + exp.toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + ' млн тг. Средняя на компанию: ' + (exp / COMPANIES.length).toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + ' млн тг.'; } if (q.includes('файл') || q.includes('статус') || q.includes('обработ')) { return 'Обработано файлов: ' + done + ' из ' + state.files.length + '. В обработке: ' + processing + ', ожидает: ' + waiting + '.'; } if (q.includes('компани')) { return 'В сводной таблице ' + state.rows.length + ' записей по ' + COMPANIES.length + ' дочерним компаниям и 3 показателям.'; } if (q.includes('рентабельност') || q.includes('эффектив')) { const ratio = rev ? (prof / rev * 100) : 0; return 'Рентабельность по выручке — ' + ratio.toLocaleString('ru-RU', { maximumFractionDigits: 1 }) + '% (прибыль ' + prof.toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + ' / выручка ' + rev.toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + ' млн тг).'; } return 'Спросите про выручку, прибыль, расходы, рентабельность, статусы файлов или компании.'; } /* ================= Init ================= */ function bindAll() { bindNavigation(); bindUpload(); bindSummary(); bindVerify(); bindDashboard(); const btnToggle = el('btnToggleTheme'); if (btnToggle) btnToggle.addEventListener('click', function () { const cur = document.documentElement.getAttribute('data-theme') || 'light'; document.documentElement.setAttribute('data-theme', cur === 'light' ? 'dark' : 'light'); }); } function init() { bindAll(); showScreen(state.screen || 'upload'); } document.addEventListener('DOMContentLoaded', init);