/* ============================================================
AssemblerAnalytics — аналитика занятости сборщиков
#/admin/assembler-analytics
Данные: «Таблица занятости сборщиков.xlsx» → backend parser
============================================================ */
const AssemblerAnalytics = (function () {
"use strict";
function escHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
}
function el(html) {
const t = document.createElement("template");
t.innerHTML = html.trim();
return t.content.firstChild;
}
function fmtMoney(n) {
return Math.round(n || 0).toLocaleString("ru-RU") + " ₽";
}
function fmtMonth(ym) {
// "2026-05" → "Май 2026"
try {
const d = new Date(ym + "-01");
return d.toLocaleDateString("ru-RU", { month: "long", year: "numeric" });
} catch { return ym; }
}
async function _api(path, body = {}) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 30000); // парсинг Excel — долгий
try {
const res = await fetch(`${BACKEND_URL}/api/${path}`, {
method: "POST", signal: ctrl.signal,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
initData: (typeof Platform !== "undefined" ? Platform.initData : (window.tg?.initData || "")),
initDataUnsafe: (typeof Platform !== "undefined" ? Platform.initDataUnsafe : null),
...body,
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (e) {
if (e.name === "AbortError") throw new Error("Таймаут — файл большой, попробуй ещё раз");
throw e;
} finally { clearTimeout(t); }
}
/* ── Главный экран ─────────────────────────────────────────── */
async function mount(container) {
container.innerHTML = "";
document.body.classList.remove("has-bottom-nav");
document.getElementById("bottom-nav")?.remove();
const h = el(`
`);
h.querySelector(".podbor-back").addEventListener("click", () => {
haptic && haptic("impact");
history.back();
});
const screen = el(`
`);
container.appendChild(h);
container.appendChild(screen);
const yearEl = el(`
`);
container.insertBefore(yearEl, screen);
const load = async (year) => {
screen.innerHTML = `Парсим Excel… может занять 10–20 сек
`;
try {
const data = await _api("assembler_analytics", { year });
if (data.error) {
screen.innerHTML = `${escHtml(data.error)}
`;
return;
}
_render(screen, data, year);
} catch (e) {
screen.innerHTML = `Ошибка: ${escHtml(e.message)}
`;
}
};
yearEl.querySelector("#yearSelect").addEventListener("change", function () {
load(this.value);
});
h.querySelector("#reloadBtn").addEventListener("click", () => {
haptic && haptic("impact");
load(yearEl.querySelector("#yearSelect").value);
});
load("2026");
}
/* ── Рендер данных ─────────────────────────────────────────── */
function _render(screen, data, year) {
screen.innerHTML = "";
const parsedAt = data.parsed_at ? new Date(data.parsed_at).toLocaleString("ru-RU") : "—";
screen.appendChild(el(`
Обновлено: ${escHtml(parsedAt)} · Записей: ${escHtml(String(data.total_records || 0))}
`));
// === Итоги по месяцам ===
const byMonth = data.by_month || {};
const months = Object.keys(byMonth).sort();
if (months.length) {
screen.appendChild(el(`📅 По месяцам
`));
const monthWrap = el(``);
const table = el(`
| Месяц |
Заказов |
Сумма сборок |
Сборщиков |
`);
const tbody = table.querySelector("#monthTbody");
let grandTotal = 0, grandOrders = 0;
for (const ym of months.reverse()) {
const m = byMonth[ym];
grandTotal += m.total_amount || 0;
grandOrders += m.order_count || 0;
const tr = el(`
| ${escHtml(fmtMonth(ym))} |
${escHtml(String(m.order_count || 0))} |
${escHtml(fmtMoney(m.total_amount))} |
${escHtml(String((m.assemblers || []).length))} |
`);
tbody.appendChild(tr);
}
// Итого строка
tbody.appendChild(el(`
| ИТОГО |
${grandOrders} |
${escHtml(fmtMoney(grandTotal))} |
|
`));
monthWrap.appendChild(table);
screen.appendChild(monthWrap);
}
// === Рейтинг сборщиков ===
const assemblers = (data.assemblers || []);
if (assemblers.length) {
screen.appendChild(el(`
👷 Сборщики · ${assemblers.length}
`));
const maxAmt = Math.max(...assemblers.map(a => a.total_amount)) || 1;
assemblers.forEach((a, idx) => {
const barPct = Math.round((a.total_amount / maxAmt) * 100);
const avgPerOrder = a.total_orders ? Math.round(a.total_amount / a.total_orders) : 0;
// Раскладка по месяцам: последние 6
const monthKeys = Object.keys(a.months || {}).sort().slice(-6);
const monthCells = monthKeys.map(ym => {
const mm = a.months[ym];
return `
${ym.slice(5)}
${Math.round((mm.total_amount||0)/1000)}к
${mm.orders} зак.
`;
}).join("");
const card = el(`
${idx + 1}. ${escHtml(a.name)}
${a.total_orders} заказов · ср. ${escHtml(fmtMoney(avgPerOrder))} / заказ
${escHtml(fmtMoney(a.total_amount))}
${monthCells ? `
${monthCells}
` : ""}
`);
screen.appendChild(card);
});
}
if (!months.length && !assemblers.length) {
screen.innerHTML = `
Нет данных за выбранный период.
Попробуй выбрать другой год.
`;
}
screen.appendChild(el(``));
}
return { mount };
})();