Initial commit: site monorepo with API, web, and infra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
влад
2026-07-16 10:11:54 +03:00
co-authored by Cursor
commit 016910ffb7
447 changed files with 73972 additions and 0 deletions
@@ -0,0 +1,196 @@
/**
* Вкладка «Сравнение» на /reports.
*/
(function (global) {
const COPY = () => global.WespAnalyticsCopy || {};
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function getDateParams() {
const from = document.getElementById("date-from")?.value || "";
const to = document.getElementById("date-to")?.value || "";
return { date_from: from, date_to: to };
}
function buildUrl() {
const params = global.WespReportsFilters?.buildAnalyticsParams?.() || new URLSearchParams(getDateParams());
return `/api/analytics/plan-fact?${params.toString()}`;
}
function barWidth(value, max) {
if (!max || max <= 0) return 0;
return Math.min(100, (Number(value) / max) * 100);
}
function faultBadge(fault, executionLabel) {
const c = COPY().comparison || {};
if (fault === "execution") {
return `<span class="analytics-pf-fault analytics-pf-fault--execution">${escapeHtml(
executionLabel || c.noteExecution || ""
)}</span>`;
}
if (fault === "excluded" || fault === "zootech_ok") {
return `<span class="analytics-pf-fault analytics-pf-fault--excluded">${escapeHtml(c.noteExcluded || "")}</span>`;
}
if (fault === "adjusted" || fault === "zootech") {
return `<span class="analytics-pf-fault analytics-pf-fault--adjusted">${escapeHtml(c.noteAdjusted || "")}</span>`;
}
return "";
}
function legendHtml() {
const c = COPY().comparison || {};
return (
`<div class="analytics-pf-legend">` +
`<span><i class="analytics-pf-legend__dot analytics-pf-legend__dot--base"></i>${escapeHtml(c.legendBase)}</span>` +
`<span><i class="analytics-pf-legend__dot analytics-pf-legend__dot--plan"></i>${escapeHtml(c.legendPlan)}</span>` +
`<span><i class="analytics-pf-legend__dot analytics-pf-legend__dot--actual"></i>${escapeHtml(c.legendActual)}</span>` +
`</div>`
);
}
function renderMetricRows(rows, maxKg, options) {
const executionLabel = options?.executionLabel;
return rows
.map((c) => {
const notes = (c.notes || [])
.map((n) => {
const note = typeof n === "string" ? { text: n, kind: "execution" } : n;
const kind = note.kind === "zootech" ? "zootech" : "execution";
const cls =
kind === "zootech"
? "analytics-pf-note analytics-pf-note--zootech"
: "analytics-pf-note analytics-pf-note--execution";
return `<li class="${cls}">${escapeHtml(note.text)}</li>`;
})
.join("");
const faultBadgeHtml = faultBadge(c.fault, executionLabel);
const compCls =
c.skippedToday && (c.fault === "excluded" || c.fault === "zootech_ok")
? "analytics-pf-comp analytics-pf-comp--excluded"
: options?.rowClass || "analytics-pf-comp";
return (
`<div class="${compCls}">` +
`<div class="analytics-pf-comp__head">` +
`<div class="analytics-pf-comp__name">${escapeHtml(c.name)}</div>` +
faultBadgeHtml +
`</div>` +
`<div class="analytics-pf-bars">` +
`<div class="analytics-pf-bar analytics-pf-bar--base" title="${escapeHtml(COPY().comparison?.legendBase)}"><span style="width:${barWidth(c.baseKg, maxKg)}%"></span><em>${Number(c.baseKg).toFixed(1)}</em></div>` +
`<div class="analytics-pf-bar analytics-pf-bar--plan" title="${escapeHtml(COPY().comparison?.legendPlan)}"><span style="width:${barWidth(c.planTodayKg, maxKg)}%"></span><em>${Number(c.planTodayKg).toFixed(1)}</em></div>` +
`<div class="analytics-pf-bar analytics-pf-bar--actual" title="${escapeHtml(COPY().comparison?.legendActual)}"><span style="width:${barWidth(c.actualKg, maxKg)}%"></span><em>${Number(c.actualKg).toFixed(1)}</em></div>` +
`</div>` +
(notes ? `<ul class="analytics-pf-notes">${notes}</ul>` : "") +
`</div>`
);
})
.join("");
}
function sectionHtml(title, body) {
if (!body) return "";
return (
`<section class="analytics-pf-section">` +
`<h3 class="analytics-pf-section__title">${escapeHtml(title)}</h3>` +
body +
`</section>`
);
}
function renderItem(item) {
const comps = item.components || [];
const groups = item.unloadingGroups || [];
const mixer = item.mixerRemainder || null;
const compMax = Math.max(...comps.flatMap((c) => [c.baseKg, c.planTodayKg, c.actualKg]), 1);
const unloadMax = Math.max(...groups.flatMap((c) => [c.baseKg, c.planTodayKg, c.actualKg]), 1);
const compHtml = renderMetricRows(comps, compMax, {
executionLabel: COPY().comparison?.noteExecution,
});
const unloadHtml = renderMetricRows(groups, unloadMax, {
executionLabel: COPY().comparison?.noteUnloadingExecution,
});
const mixerHtml = mixer
? (() => {
const kg = Number(mixer.actualKg);
const sign = kg > 0 ? "+" : "";
const cls =
kg < 0
? "analytics-pf-mixer-line analytics-pf-mixer-line--negative"
: "analytics-pf-mixer-line";
return (
`<p class="${cls}">${escapeHtml(
COPY().comparison?.mixerRemainderLabel || "Остаток в миксере"
)}: <strong>${sign}${kg.toFixed(1)}</strong> кг</p>`
);
})()
: "";
const when = item.startTime ? escapeHtml(item.startTime.slice(0, 16).replace("T", " ")) : "";
const unloadingSection =
groups.length || mixer
? sectionHtml(
COPY().comparison?.sectionUnloading || "Выгрузка",
legendHtml() + unloadHtml + mixerHtml
)
: "";
return (
`<article class="analytics-pf-card" data-loading-report-id="${escapeHtml(item.loadingReportId)}">` +
`<header class="analytics-pf-card__head">` +
`<strong>${escapeHtml(item.recipeName)}</strong>` +
`<span class="text-muted small">${when}</span>` +
`</header>` +
sectionHtml(
COPY().comparison?.sectionLoading || "Загрузка",
legendHtml() + compHtml
) +
unloadingSection +
`</article>`
);
}
function render(data) {
const el = document.getElementById("analyticsPlanFactList");
if (!el) return;
const items = data.items || [];
if (!items.length) {
el.innerHTML = `<div class="empty-state zt-empty wesp-content-reveal"><p class="zt-empty__hint">${escapeHtml(COPY().comparison?.empty || "")}</p></div>`;
return;
}
el.innerHTML = `<div class="analytics-pf-list">${items.map(renderItem).join("")}</div>`;
el.querySelectorAll("[data-loading-report-id]").forEach((card) => {
card.addEventListener("click", () => {
const id = card.getAttribute("data-loading-report-id");
if (id) global.WespFeedAlerts?.openReportFromAlert?.(id);
});
});
global.WespZootechModals?.revealContent(el.querySelector(".analytics-pf-list"));
}
async function load() {
const el = document.getElementById("analyticsPlanFactList");
if (!el) return;
el.innerHTML =
'<div class="loading-state wesp-content-reveal"><div class="zt-spinner" role="status"></div>' +
'<p class="zt-loading-caption">Загрузка сравнения…</p></div>';
try {
const resp = await fetch(buildUrl());
if (!resp.ok) throw new Error("Не удалось загрузить сравнение");
render(await resp.json());
} catch (err) {
el.innerHTML = `<div class="empty-state zt-empty"><p class="zt-empty__hint">${escapeHtml(err.message)}</p></div>`;
}
}
function onFiltersApplied() {
if (global.WespFeedAlerts?.getCurrentView?.() === "comparison") load();
}
global.WespAnalyticsPlanFact = { load, onFiltersApplied };
})(typeof window !== "undefined" ? window : globalThis);