/* Сбор и анализ HR-показателей — статичный интерактивный макет. Всё состояние — в localStorage, данные — правдоподобные примеры. */ (function () { "use strict"; var LS = "hr-analiz-v1"; var Q = "3 квартал 2026"; /* ---------- справочники ---------- */ var COMPANIES = [ { id: "A", name: "АО «СтройИнвест»" }, { id: "B", name: "ООО «ЛогМар»" }, { id: "C", name: "ТОО «АгроПром»" }, { id: "D", name: "АО «СетьРитейл»" }, { id: "E", name: "ТОО «МедСервис»" }, { id: "F", name: "ООО «ТексДом»" } ]; function company(id) { for (var i = 0; i < COMPANIES.length; i++) if (COMPANIES[i].id === id) return COMPANIES[i].name; return id; } /* Показатель, Значение, Тип, Источник, Дата; value — базовое, q3 — факт квартала */ var METRICS = [ { id: "turnover", name: "Текучесть кадров, %", type: "turnover", co: "A", date: "10.07.2026", value: "12", q3: 12 }, { id: "turnover", name: "Текучесть кадров, %", type: "turnover", co: "C", date: "12.07.2026", value: "15", q3: 15, manual: true }, { id: "turnover", name: "Текучесть кадров, %", type: "turnover", co: "D", date: "08.07.2026", value: "9", q3: 9, formErr: true }, { id: "vacancy", name: "Средний срок закрытия вакансии, дн.", type: "vacancy", co: "A", date: "10.07.2026", value: "34", q3: 34 }, { id: "vacancy", name: "Открытые вакансии, шт.", type: "vacancy", co: "B", date: "11.07.2026", value: "18", q3: 18 }, { id: "vacancy", name: "Открытые вакансии, шт.", type: "vacancy", co: "E", date: "09.07.2026", value: "6", q3: 6, manual: true }, { id: "reserve", name: "Доля назначений из резерва, %", type: "reserve", co: "C", date: "12.07.2026", value: "38", q3: 38 }, { id: "reserve", name: "Заявки в резерв, шт.", type: "reserve", co: "D", date: "08.07.2026", value: "12", q3: 12 }, { id: "reserve", name: "Заявки в резерв, шт.", type: "reserve", co: "F", date: "14.07.2026", value: "9", q3: 9 }, { id: "performance", name: "Производительность труда, млн ₸/чел.", type: "performance", co: "A", date: "10.07.2026", value: "1,8", q3: 1.8 }, { id: "performance", name: "Производительность труда, млн ₸/чел.", type: "performance", co: "B", date: "11.07.2026", value: "2,4", q3: 2.4 }, { id: "performance", name: "Производительность труда, млн ₸/чел.", type: "performance", co: "D", date: "08.07.2026", value: "1,1", q3: 1.1 }, { id: "performance", name: "Производительность труда, млн ₸/чел.", type: "performance", co: "F", date: "14.07.2026", value: "5,6", q3: 5.6 } ]; var TYPE_LABELS = { turnover: "Текучесть", vacancy: "Вакансии", reserve: "Резерв", performance: "Производительность" }; var TYPE_CHART_COLOR = { turnover: "--kt-ai-chart-blue", vacancy: "--kt-ai-chart-green", reserve: "--kt-ai-chart-orange", performance: "--kt-ai-chart-pink" }; var FILES_SEED = [ { name: "HR_Q3-2026_A.xlsx", co: "A", rows: 6, status: "ok", time: "10 июл 09:12" }, { name: "HR_Q3-2026_B.xlsx", co: "B", rows: 5, status: "ok", time: "11 июл 10:05" }, { name: "HR_Q3-2026_C.xlsx", co: "C", rows: 4, status: "warn", time: "12 июл 08:47", note: "2 значения без источника" }, { name: "HR_Q3-2026_D.xlsx", co: "D", rows: 6, status: "risk", time: "08 июл 15:31", note: "ошибка формулы в листе «вакансии»" }, { name: "HR_Q3-2026_E.xlsx", co: "E", rows: 3, status: "ok", time: "09 июл 11:20" }, { name: "HR_Q3-2026_F.xlsx", co: "F", rows: 3, status: "ok", time: "14 июл 09:58" } ]; var LOG_SEED = [ { time: "10 июл 09:15", who: "ИИ", type: "Загрузка", result: "ok", text: "АО «СтройИнвест»: 6 показателей загружено, ошибок нет" }, { time: "11 июл 10:07", who: "ИИ", type: "Загрузка", result: "ok", text: "ООО «ЛогМар»: 5 показателей загружено, ошибок нет" }, { time: "12 июл 08:49", who: "ИИ", type: "Загрузка", result: "warn", text: "ТОО «АгроПром»: 4 показатели загружено, 2 значения без источника" }, { time: "08 июл 15:33", who: "ИИ", type: "Загрузка", result: "risk", text: "АО «СетьРитейл»: обнаружена ошибка формулы в листе «вакансии»" }, { time: "09 июл 11:22", who: "ИИ", type: "Загрузка", result: "ok", text: "ТОО «МедСервис»: 3 показателя загружено, ошибок нет" }, { time: "14 июл 10:00", who: "ИИ", type: "Загрузка", result: "ok", text: "ООО «ТексДом»: 3 показателя загружено, ошибок нет" }, { time: "14 июл 11:20", who: "ИИ", type: "Анализ", result: "ok", text: "Сводка по 13 показателям из 6 компаний сформирована" }, { time: "16 июл 14:05", who: "Аналитик", type: "Правка", result: "ok", text: "Текучесть, ТОО «АгроПром»: значение исправлено вручную" } ]; var REPORT_SEED = [ "По итогам 3 квартала 2026 по 6 дочерним компаниям проведена сводка по 13 HR-показателям. Общего роста текучести кадров не зафиксировано: значения остаются в пределах 9–15%, при этом по ТОО «АгроПром» наблюдается устойчивый показатель 15%, что выше усреднённого уровня группы.", "Средний срок закрытия вакансий — 34 дня по АО «СтройИнвест»; по остальным компаниям показатель существенно ниже. Доля назначений из кадрового резерва у ТОО «АгроПром» составляет 38% — максимальная в группе; остальные компании фиксируют 12–22%.", "Производительность труда варьируется от 1,1 до 5,6 млн ₸/чел. в месяц в зависимости от профиля компании. По АО «СетьРитейл» выявлена ошибка в формулах отчёта (лист «вакансии»), значение до исправления не включается в сводку; источник подтверждён экспертом после ручной проверки.", "Рекомендация: перед закрытием квартала запросить у АО «СетьРитейл» исправленную выгрузку и повторно провести анализ. В остальном данные дочерних компаний достаточны для принятия решений по управлению персоналом." ]; /* ---------- состояние ---------- */ function loadState() { var base = { screen: "upload", manual: {}, files: FILES_SEED.slice(), log: LOG_SEED.slice().reverse(), overrides: {}, reportText: REPORT_SEED.join("\n\n"), reportStatus: "review", expertComment: "Проверено: цифры сверены с исходными выгрузками. Вывод по производительности усреднён по профилю — принять к сведению. Остальное согласовано без замечаний.", history: [ { time: "16 июл 14:32", text: "ИИ сформировал черновик заключения", by: "ИИ" }, { time: "16 июл 14:58", text: "Аналитик отредактировал вывод по производительности", by: "Аналитик" }, { time: "17 июл 09:10", text: "Отправлено на проверку эксперту", by: "Аналитик" }, { time: "17 июл 11:24", text: "Эксперт оставил комментарий к разделу о производительности", by: "Эксперт" } ], theme: "light" }; try { var raw = localStorage.getItem(LS); if (raw) { var s = JSON.parse(raw); for (var k in base) if (s && k in s) base[k] = s[k]; } } catch (e) { /* повреждённое состояние — начинаем с нуля */ } return base; } function save() { try { localStorage.setItem(LS, JSON.stringify(S)); } catch (e) { /* квота */ } } var S = loadState(); var EDITING = null; /* ---------- утилиты ---------- */ function $(id) { return document.getElementById(id); } function esc(v) { var d = document.createElement("div"); d.textContent = v == null ? "" : String(v); return d.innerHTML; } function num(n) { try { return new Intl.NumberFormat("ru-RU").format(n); } catch (e) { return String(n); } } function icon(name, size) { return ''; } function toast(msg, tone) { var stack = $("toasts"); if (!stack) return; var el = document.createElement("div"); el.className = "kt-ai-toast"; if (tone) el.setAttribute("data-tone", tone); el.textContent = msg; stack.appendChild(el); setTimeout(function () { el.style.opacity = "0"; el.style.transition = "opacity .3s"; setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 320); }, 3400); } function nowTime() { var d = new Date(); var m = ["янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"]; var h = String(d.getHours()).padStart(2, "0"); var mi = String(d.getMinutes()).padStart(2, "0"); return d.getDate() + " " + m[d.getMonth()] + " " + h + ":" + mi; } function addLog(who, type, result, text) { S.log.unshift({ time: nowTime(), who: who, type: type, result: result, text: text }); if (S.log.length > 40) S.log.pop(); save(); } function metric(m) { var key = m.id + "_" + m.co; var o = S.overrides[key]; if (o) { m.value = o.value; if (o.status) m.checkStatus = o.status; } return m; } function checkStatus(m) { if (m.checkStatus) return m.checkStatus; if (S.manual[m.id + "_" + m.co]) return "manual"; if (m.formErr) return "error"; return "ok"; } function pill(status, label) { return '' + esc(label || String(status)) + ""; } function statusLabel(m) { var st = checkStatus(m); if (st === "manual") return pill("warn", "вручную"); if (st === "error") return pill("risk", "ошибка формулы"); return pill("ok", "проверено"); } /* ---------- каркас экрана ---------- */ function pageHead(title, sub, actions) { return '
' + '

' + esc(title) + "

" + (sub ? '

' + esc(sub) + "

" : "") + '
' + (actions || "") + "
"; } function block(title, cap, actions, body) { return '
' + "

" + esc(title) + "

" + (cap ? "" + cap + "" : "") + '' + (actions || "") + "
" + body + "
"; } function fileStatusPill(f) { if (f.status === "ok") return pill("ok", "успешно"); if (f.status === "warn") return pill("warn", "с замечаниями"); return pill("risk", "ошибка"); } function viewUpload() { var totalRows = 0; var warnings = 0; var errors = 0; S.files.forEach(function (f) { totalRows += f.rows || 0; if (f.status === "warn") warnings++; if (f.status === "risk") errors++; }); var companyOpts = COMPANIES.map(function (c) { return '"; }).join(""); var fileRows = S.files.map(function (f, i) { return '
' + '' + icon("spreadsheet") + "" + '
' + '
' + esc(f.name) + "
" + '
' + esc(company(f.co)) + " · " + (f.rows || 0) + " строк · получен " + esc(f.time) + (f.note ? " · " + esc(f.note) : "") + "
" + fileStatusPill(f) + "
"; }).join(""); var logItems = S.log.map(function (l) { var tone = l.result === "ok" ? "" : (l.result === "warn" ? 'data-status="warn"' : 'data-status="risk"'); return '
' + '' + esc(l.time) + "" + '' + (l.who === "ИИ" ? pill("info", "ИИ") : pill("muted", "Пользователь")) + "" + "
" + "

" + esc(l.text) + '

' + esc(l.type) + "
" + "
"; }).join(""); var kpi = '
Файлов получено' + S.files.length + ' / 6все дочерние компании
' + '
Строк импортировано' + num(totalRows) + 'из Excel-файлов
' + '
Замечаний' + warnings + 'требуют внимания
' + '
Ошибок' + errors + 'ошибка формулы
'; return pageHead("Загрузка данных", "Исходные данные от дочерних компаний, 3 квартал 2026", "") + '
' + kpi + "
" + '
' + '
' + '

Дочерние компании

' + '6 источников · ' + Q + "
" + '
' + '' + icon("upload") + "" + "Перетащите Excel-файл или выберите его" + '.xlsx, до 25 МБ. ИИ распознает столбцы и перенесёт показатели в сводку.' + '
' + '" + '" + "" + "
" + block("Файлы", esc(Q), "", '
' + fileRows + "
") + "
" + block("Журнал действий", "ИИ и пользователь", '", '
' + logItems + "
") + "
" + ""; } /* ---------- экран 2: сводка ---------- */ var CHK = { type: "all", co: "all", only: "all" }; function filteredRows() { return METRICS.map(metric).filter(function (m) { if (CHK.type !== "all" && m.type !== CHK.type) return false; if (CHK.co !== "all" && m.co !== CHK.co) return false; if (CHK.only === "err") return checkStatus(m) === "error"; if (CHK.only === "ok") return checkStatus(m) === "ok" || checkStatus(m) === "manual"; return true; }); } function sumKpis(rows) { var ok = 0, wan = 0, err = 0; rows.forEach(function (m) { var st = checkStatus(m); if (st === "error") err++; else if (st === "manual") wan++; else ok++; }); var kpi = '
Показателей в сводке' + rows.length + "из " + METRICS.length + " всего
" + '
Проверено' + ok + 'ок
' + '
Исправлено вручную' + wan + "аналитиком
" + '
Ошибки формул' + (err ? '' + err + "" : "0") + "требуют правки
"; return kpi; } function sumTable(rows) { var body = ""; rows.forEach(function (m) { var st = checkStatus(m); var val = m.value; var valCell; if (st === "error") { valCell = 'не рассчитано'; } else if (st === "manual") { valCell = '' + esc(val) + ''; } else { valCell = esc(val); } var editing = EDITING && EDITING.id === m.id && EDITING.co === m.co; valCell = editing ? '' : valCell; body += '" + '' + esc(m.name) + '
' + esc(TYPE_LABELS[m.type]) + "
" + '' + valCell + "" + '' + esc(company(m.co)) + "" + '' + esc(m.date) + "" + '' + (st === "error" ? pill("warn", "проверить") : statusLabel(m)) + "" + (editing ? '' : "") + ""; }); return '' + "" + "" + body + "
ПоказательЗначениеИсточникДата актуальностиСтатус проверки
"; } function typeCounts() { var c = { all: METRICS.length, turnover: 0, vacancy: 0, reserve: 0, performance: 0 }; METRICS.forEach(function (m) { c[m.type]++; }); return c; } function viewSummary() { var t = filteredRows(); var coOpts = '' + COMPANIES.map(function (c) { return '"; }).join(""); var cc = typeCounts(); var seg = '
' + '" + '" + '" + '" + '" + "
"; var toolbar = '
' + seg + '
" + '
" + '
'; return pageHead("Сводка показателей", "Показатели дочерних компаний после загрузки, " + Q, '") + '
' + sumKpis(t) + "
" + block("Сводная таблица", "13 показателей · 4 типа", '", '
' + toolbar + '
' + sumTable(t) + "
"); } /* ---------- экран 3: анализ и заключение ---------- */ var QUARTERS = ["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025", "Q1 2026", "Q2 2026"]; var TURN_TREND = [ { label: "Q1 25", value: 10.2 }, { label: "Q2 25", value: 10.8 }, { label: "Q3 25", value: 11.0 }, { label: "Q4 25", value: 11.7 }, { label: "Q1 26", value: 12.1 }, { label: "Q2 26", value: 14.6 } ]; var PERF_TREND = [ { label: "Q1 25", value: 4.7 }, { label: "Q2 25", value: 4.9 }, { label: "Q3 25", value: 4.8 }, { label: "Q4 25", value: 5.2 }, { label: "Q1 26", value: 5.4 }, { label: "Q2 26", value: 5.5 } ]; function barChart(items, unit, colorVar, title) { var max = 0; items.forEach(function (b) { if (b.value > max) max = b.value; }); var bars = items.map(function (b) { var h = Math.round((b.value / max) * 158); if (h < 3) h = 3; return '
' + '
' + esc(b.label) + "
"; }).join(""); return '
' + esc(title) + "
" + '
' + bars + "
"; } function statusChip() { if (S.reportStatus === "draft") return pill("muted", "Черновик"); if (S.reportStatus === "review") return pill("warn", "На проверке"); return pill("ok", "Утверждено"); } function reportParagraphs(text) { return String(text || "").split(/\n{1,}/).filter(function (p) { return p.trim(); }) .map(function (p) { return "

" + esc(p) + "

"; }).join(""); } function viewAnalysis() { var kpi = '
Текучесть (сводка)12,7%из 3 источников, Q2 → +2,3 п.п.
' + '
Закрытие вакансий34 дн.максимум группы · АО «СтройИнвест»
' + '
Назначения из резерва38%ТОО «АгроПром» · максимум
' + '
Производительность5,5 млн ₸на человека, сводка Q2
'; return pageHead("Анализ и заключение", "Результаты анализа и черновик отчёта, " + Q, '
' + statusChip() + (S.reportStatus === "draft" ? '" : "") + '" + "
") + '
' + kpi + "
" + '
' + barChart(TURN_TREND, "Текучесть, %", "--kt-ai-chart-blue", "Текучесть кадров по кварталам") + barChart(PERF_TREND, "млн ₸/чел.", "--kt-ai-chart-pink", "Производительность труда по кварталам") + "
" + '
' + (S.reportStatus === "draft" ? '

Заключение

сформировано ИИ · редактируется вручную
' + '
" : '

Итоговое заключение · ' + esc(Q) + "

" + reportParagraphs(S.reportText) + "
") + '
' + block("Источники", "6 дочерних компаний", "", COMPANIES.map(function (c) { return '
' + '' + icon("spreadsheet") + "" + '
' + esc(c.name) + "
" + pill("ok", "получено") + "
"; }).join("")) + "
"; } /* ---------- экран 4: проверка экспертом ---------- */ function expertTable() { var rows = ["turnover", "vacancy", "reserve", "performance"].map(function (type) { var ms = METRICS.map(metric).filter(function (m) { return m.type === type && checkStatus(m) !== "error"; }); var vals = ms.map(function (m) { return m.q3; }); var avg = vals.length ? (vals.reduce(function (a, b) { return a + b; }, 0) / vals.length).toFixed(1) : "—"; return "" + esc(TYPE_LABELS[type]) + "" + ms.map(function (m) { return '' + esc(m.value) + ""; }).join("") + '' + esc(avg) + ""; }).join(""); return '' + "" + COMPANIES.map(function (c) { return '"; }).join("") + '' + rows + "
Тип показателя' + c.id + "Сводка
"; } function historyList() { return '
' + S.history.map(function (h) { return '
' + esc(h.time) + "" + '

' + esc(h.text) + "

" + '' + esc(h.by) + "
"; }).join("") + "
"; } function viewExpert() { var commentText = S.expertComment ? '

' + esc(S.expertComment) + "

" : ""; var reviewState = S.reportStatus === "approved" ? pill("ok", "Утверждено") : (S.reportStatus === "rejected" ? pill("risk", "Отклонено") : pill("warn", "На проверке")); var side = block("Комментарий эксперта", "", "", commentText + (S.reportStatus === "rejected" ? '
Заключение возвращено аналитику. После правок и повторной отправки проверьте вывод ещё раз.
' : "") + '" + '
' + (S.reportStatus === "approved" ? '
Заключение утверждено экспертом. Доступно для использования в решениях.
' : '" + '") + "
") + block("История изменений", "", "", historyList()); return pageHead("Проверка экспертом", "Финальная валидация перед использованием, " + Q, '
' + reviewState + '
") + block("Итоговое заключение", "только чтение", "", '

Итоговое заключение · ' + esc(Q) + " · автор: Аналитик

" + reportParagraphs(S.reportText) + "
") + block("Сводная таблица цифр", "по типам показателей", "", '
' + expertTable() + "
") + '
' + side + "
" + '
' + '
Что показывает сводка' + 'Цифры по 4 типам HR-показателей сопоставимы между компаниями. Текучесть остаётся в рабочих пределах 9–15%, назначение из резерва — сильная сторона группы, производительность варьируется по профилю бизнеса.' + 'Значение «СетьРитейл» по производительности исключено из сводки до исправления формулы — эксперт подтверждает источник после ручной проверки выгрузки.
'; } /* экран 5: о решении */ function viewAbout() { var steps = [ "Аналитик собирает данные с дочерних компаний (ИИ)", "Аналитик сводит данные в Excel (ИИ)", "Аналитик проводит анализ показателей (ИИ)", "Аналитик пишет текстовое заключение (ИИ)", "Эксперт проверяет итоговое заключение" ]; var aiWorks = [ "Сбор и валидация данных из Excel — экран «Загрузка данных»", "Сводка данных и проверка формул — экран «Сводка показателей»", "Проведение анализа и генерация текстового заключения — экран «Анализ и заключение»" ]; var humans = [ "Аналитик: загружает исходные данные, проверяет сводку, редактирует заключение", "Эффект: ускоряет процесс, повышает качество аналитики", "Аналитик: время на один цикл — 2 недели при частоте раз в квартал. Оценка — около 20 часов в месяц" ]; var risks = [ "Ошибка ИИ при неправильном заполнении данных работниками", "Ошибка ИИ при неверно выставленных формулах в Excel", "Сроки и исключения по процессу определяет эксперт" ]; var measures = [ "Проверка результата человеком перед использованием", "Журнал действий ИИ и возможность вернуть ручной режим" ]; var stepsItems = steps.map(function (s, i) { return "
  • " + '' + (i + 1) + "" + "" + esc(s) + "
  • "; }).join(""); function ul(list) { return ""; } return pageHead("О решении", "Автоматизировать сбор, сводку и анализ HR-показателей для подготовки итогового заключения") + '
    ' + '

    Паспорт

    ' + '
    Наименование
    Сбор и анализ HR-показателей
    ' + "
    Исполнитель
    владелец процесса – сборка на платформе Vibe AI
    " + "
    Пользователь
    Аналитик, Руководитель
    " + "
    Дата внедрения
    —
    " + "

    Предпосылки

    " + '" + "

    Входные данные и источники

    " + '" + "
    " + "

    Шаги процесса

    " + '
      ' + stepsItems + "
    " + "

    Результат

    " + '" + '

    Ожидаемый эффект

    ' + '' + "
    " + "

    Что делает ИИ

    " + ul(aiWorks) + '

    Что проверяет и решает человек

    ' + ul(humans) + "
    " + '

    Контроль и риски

    ' + ul(risks) + '

    Меры по снижению

    ' + ul(measures) + '

    Формат реализации

    ' + '" + "
    " + "
    "; } /* ---------- навигация и рендер ---------- */ var SCREENS = [ { id: "upload", title: "Загрузка данных", sub: "исходные данные от дочерних компаний", icon: "upload", view: viewUpload }, { id: "summary", title: "Сводка показателей", sub: "таблица, фильтры, проверки", icon: "table", view: viewSummary }, { id: "analysis", title: "Анализ и заключение", sub: "результаты анализа и отчёт", icon: "sparkles", view: viewAnalysis }, { id: "expert", title: "Проверка экспертом", sub: "валидация и утверждение", icon: "checkCircle", view: viewExpert }, { id: "about", title: "О решении", sub: "паспорт и контекст процесса", icon: "info", view: viewAbout } ]; function currentTitle() { for (var i = 0; i < SCREENS.length; i++) if (SCREENS[i].id === S.screen) return SCREENS[i].title; return SCREENS[0].title; } function renderSidebar() { var nav = $("sbNav"); if (!nav) return; var html = []; SCREENS.forEach(function (s, i) { html.push('' + icon(s.icon) + '' + esc(s.title) + "" + esc(s.sub) + "" + '' + (i + 1) + ""); }); nav.innerHTML = html.join(""); } function renderTopbar() { var t = $("topTitle"); if (!t) return; t.innerHTML = '' + esc(currentTitle()) + ""; } function render() { renderSidebar(); renderTopbar(); var view = $("view"); if (!view) return; for (var i = 0; i < SCREENS.length; i++) { if (SCREENS[i].id === S.screen) { view.innerHTML = SCREENS[i].view(); break; } } syncSummaryControls(); syncSelects(); save(); window.scrollTo(0, 0); } function navigate(id) { if (id === S.screen) return; S.screen = id; EDITING = null; render(); } function syncSummaryControls() { var fCompany = $("fCompany"); if (fCompany) fCompany.value = CHK.co; var fOnly = $("fOnly"); if (fOnly) fOnly.value = CHK.only; var note = $("fNote"); if (S.screen === "summary" && note) { var errN = METRICS.filter(function (m) { return checkStatus(m) === "error"; }).length; note.textContent = errN ? ("Ошибок формул: " + errN) : ""; } } function syncSelects() { var rc = $("srcCompany"); if (rc) rc.selectedIndex = 0; } /* --- события (делегирование) --- */ function onSegType(e) { var b = e.target.closest(".seg"); if (!b) return; CHK.type = b.getAttribute("data-v"); render(); } function onFileListClick(e) { var row = e.target.closest("[data-file]"); if (!row) return; var idx = parseInt(row.getAttribute("data-file"), 10); if (isNaN(idx)) return; var f = S.files[idx]; if (!f) return; if (f.status === "risk") { toast("Файл содержит ошибку формулы: " + f.note, "risk"); } else if (f.status === "warn") { toast("Загрузка с замечаниями: " + f.note, "warn"); } else { toast("Файл загружен без ошибок", "ok"); } addLog("Пользователь", "Просмотр файла", "ok", company(f.co) + ": файл открыт для просмотра"); save(); render(); } function onTableClick(e) { var row = e.target.closest("tr[data-row]"); if (!row) return; var btn = e.target.closest("[data-act='error']"); if (btn) return; var key = row.getAttribute("data-row"); var parts = key ? key.split("|") : []; var id = parts[0], co = parts[1]; if (EDITING && EDITING.id === id && EDITING.co === co) return; EDITING = { id: id, co: co }; render(); } function onKeydown(e) { if (e.key !== "Enter") return; var inp = e.target; if (!inp || !inp.getAttribute || !inp.getAttribute("data-edit")) return; var key = inp.getAttribute("data-edit"); var parts = key.split("|"); var id = parts[0], co = parts[1]; var val = inp.value.trim(); var key2 = id + "_" + co; S.overrides[key2] = { value: val, status: "ok" }; S.manual[key2] = true; EDITING = null; addLog("Пользователь", "Правка значения", "ok", "Значение исправлено вручную"); toast("Значение сохранено", "ok"); save(); render(); } function onInput(e) { if (e.target && e.target.id === "reportText") S.reportText = e.target.value; } function onDropzoneDragOver(e) { e.preventDefault(); e.dataTransfer && e.dataTransfer.dropEffect && (e.dataTransfer.dropEffect = "copy"); } function onFileInputChange(e) { var input = e.target; if (!input || !input.files || !input.files.length) return; var file = input.files[0]; var srcSel = $("srcCompany"); var srcCo = srcSel ? srcSel.value : "A"; var ext = (file.name || "").toLowerCase(); var isExcel = ext.indexOf(".xlsx") !== -1 || ext.indexOf(".xls") !== -1; if (!isExcel) { toast("Нужен Excel-файл (.xlsx или .xls)", "risk"); return; } var fname = "HR_Q3-2026_" + srcCo + ".xlsx"; var existing = null; for (var i = 0; i < S.files.length; i++) { if (S.files[i].co === srcCo) { existing = S.files[i]; break; } } if (existing) { existing.rows = 5 + Math.floor(Math.random() * 5); existing.time = nowTime(); existing.status = "ok"; existing.note = ""; } else { S.files.push({ name: fname, co: srcCo, rows: 5 + Math.floor(Math.random() * 5), status: "ok", time: nowTime(), note: "" }); } var name = company(srcCo); addLog("ИИ", "Загрузка", "ok", name + ": файл получен, показатели добавлены в сводку"); toast("Файл загружен: " + name, "ok"); save(); render(); } function manualAction() { addLog("Пользователь", "Ручной режим", "ok", "Сбор и анализ возвращены в ручной режим"); toast("Ручной режим включён", "warn"); save(); render(); } function openLogAction() { addLog("Пользователь", "Просмотр журнала", "ok", "Журнал действий открыт"); S.screen = "upload"; save(); render(); } function runAnalysisAction() { addLog("ИИ", "Анализ", "ok", "Анализ заведомо выполнен: сформирован черновик заключения"); toast("Анализ запущен, черновик заключения сформирован", "ok"); S.screen = "analysis"; save(); render(); } function sendReviewAction() { if (S.reportStatus !== "draft") return; S.reportStatus = "review"; S.history.push({ time: nowTime(), text: "Отправлено на проверку эксперту", by: "Аналитик" }); addLog("Пользователь", "Отправка на проверку", "ok", "Заключение отправлено эксперту"); toast("Заключение отправлено на проверку", "ok"); save(); render(); } function downloadAction() { addLog("Пользователь", "Скачивание отчёта", "ok", "Отчёт сохранён в PDF"); toast("Отчёт сохранён в PDF", "ok"); save(); render(); } function approveAction() { if (S.reportStatus === "approved") return; var cm = $("expertComment"); if (cm && cm.value && cm.value !== S.expertComment) { S.expertComment = cm.value; S.history.push({ time: nowTime(), text: "Эксперт оставил комментарий", by: "Эксперт" }); } S.reportStatus = "approved"; S.history.push({ time: nowTime(), text: "Заключение утверждено", by: "Эксперт" }); addLog("Эксперт", "Утверждение", "ok", "Итоговое заключение утверждено"); toast("Заключение утверждено", "ok"); save(); render(); } function rejectAction() { if (S.reportStatus === "approved") return; var cm = $("expertComment"); var c = cm && cm.value ? cm.value.trim() : ""; if (!c) { toast("Введите комментарий к отклонению", "warn"); return; } S.expertComment = c; S.reportStatus = "rejected"; S.history.push({ time: nowTime(), text: "Заключение отклонено с комментарием", by: "Эксперт" }); addLog("Эксперт", "Отклонение", "risk", "Заключение возвращено аналитику"); toast("Заключение отклонено, возвращено аналитику", "warn"); save(); render(); } function toAnalysisAction() { S.screen = "analysis"; save(); render(); } function toExpertAction() { S.screen = "expert"; save(); render(); } function resetDemo() { try { localStorage.removeItem(LS); } catch (e) {} S = loadState(); toast("Данные сброшены", "ok"); render(); } function onNavScreen(e) { var a = e.target.closest("[data-screen]"); if (!a) return; e.preventDefault(); navigate(a.getAttribute("data-screen")); } function onActClick(e) { var b = e.target.closest("[data-act]"); if (!b) return; var act = b.getAttribute("data-act"); var saveKey = b.getAttribute("data-save"); var input = b.closest("tr[data-row]"); if (act === "save-edit" && saveKey && input) { var val = (input.querySelector("[data-edit]") || {}).value || ""; var parts = saveKey.split("|"); var key2 = parts[0] + "_" + parts[1]; S.overrides[key2] = { value: val.trim(), status: "ok" }; S.manual[key2] = true; EDITING = null; addLog("Пользователь", "Правка значения", "ok", "Значение исправлено вручную"); save(); render(); return; } if (act === "upload-open") { var fi = $("fileInput"); if (fi) fi.click(); return; } if (act === "upload-file") { toast("Выберите Excel-файл для загрузки", "info"); return; } if (act === "manual-mode") { manualAction(); return; } if (act === "open-log") { openLogAction(); return; } if (act === "run-analysis") { runAnalysisAction(); return; } if (act === "send-review") { sendReviewAction(); return; } if (act === "download") { downloadAction(); return; } if (act === "approve") { approveAction(); return; } if (act === "reject") { rejectAction(); return; } if (act === "to-analysis") { toAnalysisAction(); return; } if (act === "to-expert") { toExpertAction(); return; } } function setupEvents() { document.addEventListener("click", function (e) { if (e.target.closest(".kt-ai-segmented [data-v]")) { onSegType(e); return; } if (e.target.closest("#fileList [data-file]")) { onFileListClick(e); return; } if (e.target.closest("#sumTableWrap tr[data-row]")) { onTableClick(e); return; } if (e.target.closest("[data-act]")) { onActClick(e); return; } if (e.target.closest("[data-screen]")) { onNavScreen(e); return; } }); document.addEventListener("keydown", onKeydown); document.addEventListener("input", onInput); var dz = $("dropzone"); function bindDropzone() { var el = $("dropzone"); if (!el || el._dzBound) return; el._dzBound = true; el.addEventListener("dragover", onDropzoneDragOver); el.addEventListener("drop", function (e) { e.preventDefault(); toast("Выберите Excel-файл для загрузки", "info"); }); } setInterval(bindDropzone, 200); document.addEventListener("change", function (e) { var t = e.target; if (!t) return; if (t.id === "fileInput") { onFileInputChange(e); return; } if (t.id === "fCompany") { CHK.co = t.value; render(); return; } if (t.id === "fOnly") { CHK.only = t.value; render(); return; } if (t.id === "expertComment") { S.expertComment = t.value; save(); return; } }); /* --- мобильное меню --- */ var burger = $("burger"), sidebar = $("sidebar"), scrim = $("scrim"), drawer = $("drawer"); function openDrawer(open) { if (scrim) scrim.classList.toggle("on", open); if (sidebar) sidebar.style.display = open ? "block" : ""; } if (burger) burger.addEventListener("click", function () { openDrawer(true); }); if (scrim) scrim.addEventListener("click", function () { openDrawer(false); }); if (drawer) drawer.addEventListener("click", function (e) { var a = e.target.closest("[data-screen]"); if (!a) return; e.preventDefault(); openDrawer(false); navigate(a.getAttribute("data-screen")); }); /* --- тема --- */ var tg = $("themeToggle"), ti = $("themeIcon"); if (tg) tg.addEventListener("click", function () { var dark = S.theme === "light"; S.theme = dark ? "dark" : "light"; document.documentElement.setAttribute("data-theme", S.theme); if (ti) ti.setAttribute("href", dark ? "#sun" : "#moon"); save(); }); document.documentElement.setAttribute("data-theme", S.theme); } function init() { var burger = $("burger"); if (burger) { function toggleBurger() { burger.hidden = window.innerWidth > 831; } window.addEventListener("resize", toggleBurger); toggleBurger(); } render(); setupEvents(); } document.addEventListener("DOMContentLoaded", init); })();