";
}
function workDetail(r) {
if (!r) {
return '';
}
var hint = r.hints;
var codeErr = !r.codeTru;
var sumMismatch = r.id === "ЗК-0919";
var planIssue = r.plan !== "Да";
var isErr = codeErr || sumMismatch || planIssue;
var errReason = codeErr ? (r.id === "ЗК-0919" ? "Сумма не сходится с ценой в плане" : (r.dispute ? "Код несовпадает с оборудованием в заявке" : "ИИ не подтвердил код")) : (sumMismatch ? "Сумма не сходится с ценой в плане" : "Нет обоснования сверх плана");
var checks =
checkItem(planIssue ? "warn" : "ok", "В плане закупок", planIssue ? "В плане отсутствует, нужно обоснование" : "Строка плана найдена") +
checkItem(codeErr ? "err" : "ok", "Код ТРУ", codeErr ? errReason : "Подтверждён по справочнику") +
checkItem(sumMismatch ? "err" : "ok", "Сумма в пределах лимита", sumMismatch ? "Не сходится с ценой в плане" : (r.src === "1С" ? "Сверено со строкой 1С" : "В письмо от инициатора"));
var okCount = isErr ? 2 : 3;
return '";
}
function kpiCell(k, v) {
return '
' + v + '' + k + "
";
}
function checkItem(tone, title, why) {
var ic = tone === "ok" ? "check" : (tone === "err" ? "x" : "warning");
return '
' +
"
" + esc(title) + (why ? '
' + esc(why) + "
" : "") + "
";
}
function aiPanel(r) {
var hint = r.hints;
var head = '
Подсказка ИИ · код ТРУ
';
if (r.codeTru) {
return '
' + head +
'
Код ' + r.trr + " подтверждён " +
(r.confirmedAt ? new Date(r.confirmedAt).toLocaleDateString("ru-RU") : "(ранее)") + ". Реестр обновлён автоматически.
";
}
if (!hint || !hint.length) return '
Подсказка не найдена
';
var best = hint[0];
var cands = hint.map(function (h, idx) {
return '
' + h.c + "
" +
'
' + esc(h.n) + "
" +
'' + h.conf + "%
";
}).join("");
return '
' + head +
'
Уверенность по первой подсказке' + best.conf + "%
" +
'
' + cands + "
" +
'
' +
'
';
}
function bindWork() {
var trs = document.querySelectorAll("#view .kt-ai-table tbody tr[data-id]");
for (var i = 0; i < trs.length; i++) {
trs[i].addEventListener("click", function () {
S.selected = this.getAttribute("data-id");
saveState(); renderWork();
});
}
var btns = document.querySelectorAll('#view [data-act]');
for (var j = 0; j < btns.length; j++) {
btns[j].addEventListener("click", onWorkAction);
}
var cands = document.querySelectorAll("#view .cand-row[data-cand]");
for (var c = 0; c < cands.length; c++) {
cands[c].addEventListener("click", function () {
var rows = document.querySelectorAll("#view .cand-row[data-cand]");
for (var k = 0; k < rows.length; k++) rows[k].classList.remove("sel");
this.classList.add("sel");
});
}
}
function onWorkAction(ev) {
var act = ev.currentTarget.getAttribute("data-act");
var r = (window.__allRequests || []).find(function (x) { return x.id === S.selected; });
if (!r) return;
if (act === "confirm") {
var code = r.trr;
if (r.hints && r.hints.length) {
var selCand = document.querySelector("#view .cand-row.sel");
var ci = 0;
if (selCand) {
var ciRaw = parseInt(selCand.getAttribute("data-cand"), 10);
ci = !isNaN(ciRaw) ? ciRaw : 0;
if (ci >= r.hints.length) ci = 0;
}
var picked = r.hints[ci];
if (picked) code = picked.c;
}
apiPatch("/api/requests", { id: r.id, codeTru: true, trr: code })
.then(function () {
toast("Код " + code + " подтверждён, реестр обновлён", "ok");
return refresh();
})
.catch(function (e) { toast("Ошибка: " + e.message, "err"); });
} else if (act === "return") {
apiPatch("/api/requests", { id: r.id, st: "rejected", done: true, who: "закупщик" })
.then(function () {
S.selected = nextIn((window.__allRequests || []).filter(function (x) { return x.st === "new" || x.st === "checking"; }), r.id);
saveState();
toast(r.id + " возвращена инициатору", "warn");
return refresh();
})
.catch(function (e) { toast("Ошибка: " + e.message, "err"); });
} else if (act === "dispute") {
modalOpen("Спорный случай",
'
Заявка ' + r.id + " будет передана руководителю для разбора: ИИ предложил код, который не совпадает с версией в заявке. Решение по ней примет руководитель.
" + (rows || emptyRow("Ничего не найдено", "Снимите фильтры — реестр за месяц заполнен.")) + "
" +
'
Заявки по неделям сентября
' +
'
' + bars + "
" +
'
' +
'входящие' +
'высокая нагрузка
' +
"
Что показывает реестр" +
"Пик нагрузки — 2–7 сентября: 10–13 заявок в день совпали с закрытием планового цикла. Возвратов в сентябре немного, большинство возвратов — дубли уже приобретённых услуг.
" +
"
";
document.getElementById("view").innerHTML = html;
bindRegistry();
}
function inputSearch() {
return '';
}
function selStatus() {
var opts = "";
for (var k in STATUS) {
if (!Object.prototype.hasOwnProperty.call(STATUS, k)) continue;
opts += '";
}
return '";
}
function selSrc() {
return '";
}
function bindRegistry() {
var trs = document.querySelectorAll("#view .kt-ai-table tbody tr[data-id]");
for (var i = 0; i < trs.length; i++) {
trs[i].addEventListener("click", function () {
S.selected = this.getAttribute("data-id");
S.screen = "approve"; S.regRole = "legal";
saveState(); render();
});
}
var q = document.querySelector(".reg-q");
if (q) q.addEventListener("input", function () { S.regQuery = this.value; saveState(); renderRegistry(); });
var status = document.querySelector(".reg-status");
if (status) status.addEventListener("change", function () { S.regStatus = this.value; saveState(); renderRegistry(); });
var src = document.querySelector(".reg-src");
if (src) src.addEventListener("change", function () { S.regSrc = this.value; saveState(); renderRegistry(); });
var exp = document.querySelector('#view [data-act="csv"]');
if (exp) exp.addEventListener("click", function () { exportFile("/api/export.csv", "CSV"); });
var expX = document.querySelector('#view [data-act="xlsx"]');
if (expX) expX.addEventListener("click", function () { exportFile("/api/export.xlsx", "Excel"); });
}
function exportFile(url, label) {
window.location.href = url;
toast("Реестр выгружен в " + label, "ok");
}
// ================= СОГЛАСОВАНИЕ =================
function renderApprove() {
var role = S.regRole || "legal";
var list = window.__allRequests || [];
var pool = list.filter(function (r) { return role === "legal" ? r.st === "toLegal" : r.st === "toSign"; });
pool.sort(function (a, b) { return a.in < b.in ? 1 : -1; });
var sel = null;
for (var i = 0; i < pool.length; i++) { if (pool[i].id === S.selected) { sel = pool[i]; break; } }
var isDisputed = role === "boss" && sel && sel.st === "disputed";
if (!sel && pool.length) { sel = pool[0]; }
var rows = pool.map(function (r) {
var sAttr = r.id === S.selected ? ' data-selected="true"' : "";
return '
' +
p("Заявки на закупку поступают из 1С и письмами, около 150 в месяц.") +
p("Процесс включает ручную проверку данных (код ТРУ, план, сумма) и последовательную передачу между участниками.") +
"
" +
'
Входные данные и источники
' +
'
' +
p("Заявки из 1С и электронной почты.") +
p("Реестр в Excel: номер, инициатор, сумма, статус, даты.") +
"
" +
'
Процессы для внедрения ИИ
' +
'
' +
p("Проверка по чек-листу.") +
p("Подсказка кода ТРУ.") +
p("Ведение реестра и напоминания.") +
"
"; }
function steps() {
var steps = [
"Поступление заявки из 1С или по электронной почте",
"Закупщик проверяет код ТРУ, наличие в плане и сумму",
"ИИ подсказывает код ТРУ и ведёт реестр",
"Закупщик возвращает заявку инициатору при ошибке",
"Передача заявки юристу для согласования",
"Руководитель подписывает заявку",
"Закупщик отправляет заявку в 1С и уведомляет инициатора"
];
return '
' + steps.map(function (s, i) {
return '
' + (i + 1) + '
' + esc(s) + "
";
}).join("") + "
";
}
function passSteps() {
var ai = ["ИИ подсказывает код ТРУ", "ИИ ведёт реестр", "ИИ обновляет статусы и даты"];
var hum = ["Закупщик проверяет код ТРУ, план и сумму", "Закупщик решает, вернуть ли заявку", "Юрист согласовывает", "Руководитель подписывает", "Руководитель разбирает спорные случаи"];
return '
Кто делает что
' +
'
' +
'
' +
'
ИИ
' +
ai.map(function (s) { return '
·
' + esc(s) + "
"; }).join("") +
"
" +
'
' +
'
Человек
' +
hum.map(function (s) { return '
·
' + esc(s) + "
"; }).join("") +
"
";
}
function risks() {
return '
Контроль и риски
' +
'
' +
riskItem("Ошибка ИИ в коде ТРУ", "ИИ может ошибаться в коде ТРУ — все коды подтверждает закупщик.") +
riskItem("Дубли", "ИИ может пропустить дубли — закупщик проверяет дубли перед отправкой в 1С.") +
riskItem("Спорные случаи", "Спорные случаи разбирает руководитель, решение фиксируется в реестре.") +
"
" +
'
' +
'
' +
'
~3 000 чэкономия в месяц150 заявок × 20 ч — оценка владельца процесса
' +
'
~150заявок в месяциз 1С и по почте
' +
'
2 ролиюрист и руководительпроверяют после закупщика
' +
"
" +
'
' +
'Результат' +
p("Реестр и статус заявки для закупщика и руководителя: меньше времени на обработку и меньше ошибок в кодах ТРУ.") +
'Ожидаемый эффект' +
p("Быстрая обработка заявок, меньше ошибок в кодах ТРУ.") +
'Экономический эффект' +
p("Около 3 000 ч в месяц (150 заявок × 20 ч) — оценка владельца процесса.") +
'Формат реализации' +
p("Веб-интерфейс на Vibe AI: сначала статичный макет, после согласования — рабочее решение.") +
"
";
}
function riskItem(t, d) {
return '
' + esc(t) + "
" + esc(d) + "
";
}
// ================= НАВИГАЦИЯ + ЦИКЛ ЗАГРУЗКИ =================
var CRUMBS = { work: "Филиал · Закупки · Рабочий стол", registry: "Филиал · Закупки · Реестр", approve: "Филиал · Закупки · Согласование", about: "Филиал · Закупки · О решении" };
function render() {
if (S.screen === "work") renderWork();
else if (S.screen === "registry") renderRegistry();
else if (S.screen === "approve") renderApprove();
else renderAbout();
var items = document.querySelectorAll(".kt-ai-nav-item");
for (var i = 0; i < items.length; i++) {
items[i].setAttribute("data-active", items[i].getAttribute("data-screen") === S.screen ? "true" : "false");
}
var list = window.__allRequests || [];
setCount("work", list.filter(function (r) { return r.st === "new" || r.st === "checking"; }).length);
setCount("approve", list.filter(function (r) { return r.st === "toLegal" || r.st === "toSign" || r.st === "disputed"; }).length);
var crumb = document.getElementById("crumb");
if (crumb) crumb.textContent = CRUMBS[S.screen] || CRUMBS.work;
}
function setCount(id, n) {
document.querySelectorAll('[data-count="' + id + '"]').forEach(function (el) {
el.textContent = n;
});
}
var _loading = false;
function refresh() {
if (_loading) return Promise.resolve();
_loading = true;
return apiGet("/api/requests")
.then(function (data) { window.__allRequests = data; render(); })
.catch(function (e) { toast("Ошибка загрузки: " + e.message, "err"); })
.then(function () { _loading = false; });
}
document.addEventListener("DOMContentLoaded", function () {
S = loadState();
var items = document.querySelectorAll(".kt-ai-nav-item");
for (var i = 0; i < items.length; i++) {
(function (a) {
a.addEventListener("click", function (e) {
e.preventDefault();
S.screen = a.getAttribute("data-screen");
S.selected = "";
saveState(); render();
});
})(items[i]);
}
refresh();
});