/* ============================================================ 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 }; })();