Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,676 @@
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
const CREATE_NEW = "__create_new__";
|
||||
const ACCEPT_EXT = [".xml", ".pdf", ".xlsx", ".xls"];
|
||||
|
||||
let parseResult = null;
|
||||
let components = [];
|
||||
let selectedFile = null;
|
||||
|
||||
const WARN_LABELS = {
|
||||
omd_from_tdn: "ВРХ через TDN — не lab-grade",
|
||||
omd_missing: "нет ВРХ — подставим дефолт",
|
||||
dm_missing: "нет СВ — chemistry on hold",
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function showEl(el) {
|
||||
if (!el) return;
|
||||
el.hidden = false;
|
||||
el.classList.remove("lab-agrostar-reveal");
|
||||
requestAnimationFrame(() => el.classList.add("lab-agrostar-reveal"));
|
||||
}
|
||||
|
||||
function hideEl(el) {
|
||||
if (!el) return;
|
||||
el.hidden = true;
|
||||
el.classList.remove("lab-agrostar-reveal");
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function importKind() {
|
||||
return parseResult?.kind || "lab_samples";
|
||||
}
|
||||
|
||||
function isLabSamples() {
|
||||
return importKind() === "lab_samples";
|
||||
}
|
||||
|
||||
async function api(path, opts) {
|
||||
const resp = await fetch(path, opts);
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || data.error) {
|
||||
throw new Error(data.message || `Наука, чёрт возьми — ошибка ${resp.status}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function loadComponents(force) {
|
||||
if (components.length && !force) return components;
|
||||
const rows = await api("/api/components?limit=2000");
|
||||
components = Array.isArray(rows) ? rows : [];
|
||||
return components;
|
||||
}
|
||||
|
||||
function getSampleByNo(sampleNo) {
|
||||
return (parseResult?.samples || []).find((s) => s.sampleNo === sampleNo);
|
||||
}
|
||||
|
||||
function buildComponentOptions(matchSource, selectedId) {
|
||||
const suggested = matchSource?.suggestedComponents || [];
|
||||
const seen = new Set();
|
||||
const opts = [
|
||||
'<option value="">— какой реагент? —</option>',
|
||||
`<option value="${CREATE_NEW}">+ добавить новый реагент</option>`,
|
||||
];
|
||||
|
||||
for (const m of suggested) {
|
||||
if (!m.componentId || seen.has(m.componentId)) continue;
|
||||
seen.add(m.componentId);
|
||||
const sel = m.componentId === selectedId ? " selected" : "";
|
||||
opts.push(
|
||||
`<option value="${escapeHtml(m.componentId)}"${sel}>${escapeHtml(m.name)} (${Math.round(m.score * 100)}%)</option>`,
|
||||
);
|
||||
}
|
||||
for (const c of components) {
|
||||
if (!c.id || seen.has(c.id)) continue;
|
||||
const sel = c.id === selectedId ? " selected" : "";
|
||||
opts.push(`<option value="${escapeHtml(c.id)}"${sel}>${escapeHtml(c.name)}</option>`);
|
||||
}
|
||||
return opts.join("");
|
||||
}
|
||||
|
||||
function selectComponentForSample(sampleNo, componentId) {
|
||||
const sel = document.querySelector(
|
||||
`.lab-agrostar-component[data-sample-no="${CSS.escape(sampleNo)}"]`,
|
||||
);
|
||||
const sample = getSampleByNo(sampleNo);
|
||||
if (!sel || !sample) return;
|
||||
sel.innerHTML = buildComponentOptions(sample, componentId);
|
||||
sel.value = componentId;
|
||||
}
|
||||
|
||||
function formatUnsupportedNotice(storage) {
|
||||
if (!storage?.unsupportedCount) return "";
|
||||
return storage.unsupportedMessage || `${storage.unsupportedCount} показат. AgroStar без полей в WESP`;
|
||||
}
|
||||
|
||||
function notifyCatalogChanged() {
|
||||
try {
|
||||
global.dispatchEvent(new CustomEvent("wesp:components-catalog-changed"));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function createComponentFromSample(sampleNo, sel) {
|
||||
const sample = getSampleByNo(sampleNo);
|
||||
if (!sample || !sel) return;
|
||||
|
||||
const preview = sample.preview || {};
|
||||
const name = (preview.suggestedName || sample.label || "").trim();
|
||||
const type = preview.suggestedType || "Сочные корма";
|
||||
if (!name) {
|
||||
sel.value = "";
|
||||
setResult("Нет названия для нового реагента", true);
|
||||
return;
|
||||
}
|
||||
|
||||
sel.disabled = true;
|
||||
setResult(`Создаём «${name}»…`);
|
||||
try {
|
||||
const data = await api("/api/components", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
type,
|
||||
dry_matter: sample.dryMatterPct || 0,
|
||||
nutrients: sample.nutrients || {},
|
||||
}),
|
||||
});
|
||||
await loadComponents(true);
|
||||
selectComponentForSample(sampleNo, data.id);
|
||||
const storage = preview.storage || {};
|
||||
const skipNote = formatUnsupportedNotice(storage);
|
||||
setResult(
|
||||
`Реагент «${name}» создан — СВ и ${storage.nutrientCount || Object.keys(sample.nutrients || {}).length} показат.` +
|
||||
(skipNote ? ` ${skipNote}` : ""),
|
||||
false,
|
||||
);
|
||||
notifyCatalogChanged();
|
||||
} catch (err) {
|
||||
sel.value = "";
|
||||
setResult(err.message || "Не удалось создать реагент", true);
|
||||
} finally {
|
||||
sel.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onComponentSelectChange(e) {
|
||||
const sel = e.target;
|
||||
if (!sel.classList?.contains("lab-agrostar-component")) return;
|
||||
if (sel.value !== CREATE_NEW) return;
|
||||
const sampleNo = sel.getAttribute("data-sample-no") || "";
|
||||
createComponentFromSample(sampleNo, sel);
|
||||
}
|
||||
|
||||
function prime(reason) {
|
||||
global.WespLabHeisenbergPrime?.error?.(reason, "agrostar");
|
||||
}
|
||||
|
||||
function setStatus(text, isError) {
|
||||
const el = $("labAgrostarStatus");
|
||||
if (!el) return;
|
||||
el.textContent = text || "";
|
||||
el.className = "small mt-2 mb-0" + (isError ? " lab-agrostar-result--err" : " text-muted");
|
||||
if (isError && text) prime(text);
|
||||
}
|
||||
|
||||
function setResult(text, isError) {
|
||||
const el = $("labAgrostarResult");
|
||||
if (!el) return;
|
||||
el.textContent = text || "";
|
||||
el.className = "small mt-2 " + (isError ? "lab-agrostar-result--err" : "lab-agrostar-result--ok");
|
||||
if (isError && text) prime(text);
|
||||
}
|
||||
|
||||
function formatWarnings(warnings) {
|
||||
if (!warnings?.length) return "—";
|
||||
return warnings
|
||||
.map((w) => {
|
||||
const key = String(w).split("=")[0];
|
||||
const label = WARN_LABELS[key] || w;
|
||||
return `<span class="lab-agrostar-warn">${escapeHtml(label)}</span>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function getSelectedComponentLabel(sampleNo) {
|
||||
const sel = document.querySelector(`.lab-agrostar-component[data-sample-no="${CSS.escape(sampleNo)}"]`);
|
||||
if (!sel?.value || sel.value === CREATE_NEW) return null;
|
||||
const opt = sel.options[sel.selectedIndex];
|
||||
return opt ? opt.text.replace(/\s*\(\d+%\)\s*$/, "").trim() : null;
|
||||
}
|
||||
|
||||
function renderMetaLine(extra) {
|
||||
const meta = $("labAgrostarMeta");
|
||||
if (!meta || !parseResult) return;
|
||||
showEl(meta);
|
||||
const label = parseResult.sourceLabel || parseResult.labName || "Импорт";
|
||||
meta.innerHTML =
|
||||
`<strong>${escapeHtml(label)}</strong>${extra || ""}` +
|
||||
(parseResult.warnings?.length ? ` · ${escapeHtml(parseResult.warnings.join("; "))}` : "");
|
||||
}
|
||||
|
||||
function setActionButtons() {
|
||||
const previewBtn = $("labAgrostarPreview");
|
||||
const applyBtn = $("labAgrostarApply");
|
||||
const clearBtn = $("labAgrostarClear");
|
||||
if (previewBtn) previewBtn.disabled = false;
|
||||
if (clearBtn) clearBtn.disabled = false;
|
||||
if (applyBtn) {
|
||||
applyBtn.disabled = !isLabSamples();
|
||||
applyBtn.title = isLabSamples() ? "" : "Запись в каталог только для лабораторных проб AgroStar";
|
||||
}
|
||||
}
|
||||
|
||||
function renderLabSamplesTable() {
|
||||
const wrap = $("labAgrostarTableWrap");
|
||||
const body = $("labAgrostarTableBody");
|
||||
const head = $("labImportTableHead");
|
||||
const title = $("labImportTableTitle");
|
||||
if (!wrap || !body || !parseResult) return;
|
||||
|
||||
const samples = parseResult.samples || [];
|
||||
if (!samples.length) {
|
||||
hideEl(wrap);
|
||||
return;
|
||||
}
|
||||
|
||||
if (title) title.textContent = "Кому в каталог";
|
||||
if (head) {
|
||||
head.innerHTML = `<tr>
|
||||
<th>Образец</th><th>СВ, %</th><th>Реагент</th><th>Косяки</th>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
renderMetaLine(` · образцов: ${samples.length}`);
|
||||
|
||||
body.innerHTML = samples
|
||||
.map((sample) => {
|
||||
const sid = escapeHtml(sample.sampleNo);
|
||||
const defaultId = sample.suggestedComponents?.[0]?.componentId || "";
|
||||
return `<tr data-sample-no="${sid}">
|
||||
<td>
|
||||
<div class="fw-semibold">${escapeHtml(sample.label || sample.desc1 || sample.sampleNo)}</div>
|
||||
<div class="text-muted small">${sid} · ${escapeHtml(sample.datePrinted || "")}</div>
|
||||
</td>
|
||||
<td>${sample.dryMatterPct != null ? escapeHtml(sample.dryMatterPct) : "—"}</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm lab-agrostar-component" data-sample-no="${sid}">
|
||||
${buildComponentOptions(sample, defaultId)}
|
||||
</select>
|
||||
</td>
|
||||
<td>${formatWarnings(sample.warnings)}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
showEl(wrap);
|
||||
setActionButtons();
|
||||
}
|
||||
|
||||
function renderRationCompositionTable() {
|
||||
const wrap = $("labAgrostarTableWrap");
|
||||
const body = $("labAgrostarTableBody");
|
||||
const head = $("labImportTableHead");
|
||||
const title = $("labImportTableTitle");
|
||||
const lines = parseResult?.feedLines || [];
|
||||
if (!wrap || !body || !lines.length) return;
|
||||
|
||||
if (title) title.textContent = "Состав рациона";
|
||||
if (head) {
|
||||
head.innerHTML = `<tr>
|
||||
<th>Корм</th><th>кг/сут</th><th>руб.</th><th>Реагент WESP</th>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const meta = parseResult.meta || {};
|
||||
const extra = [
|
||||
meta.group ? escapeHtml(meta.group) : "",
|
||||
meta.rationDate ? escapeHtml(meta.rationDate) : "",
|
||||
meta.totalCostRub ? `Σ ${escapeHtml(meta.totalCostRub)} руб.` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
renderMetaLine(` · ${lines.length} корм(ов)${extra ? " · " + extra : ""}`);
|
||||
|
||||
body.innerHTML = lines
|
||||
.map((line, idx) => {
|
||||
const defaultId = line.suggestedComponents?.[0]?.componentId || "";
|
||||
return `<tr>
|
||||
<td>${escapeHtml(line.feedName)}</td>
|
||||
<td>${escapeHtml(line.dailyKg)}</td>
|
||||
<td>${escapeHtml(line.costRub)}</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm lab-agrostar-component lab-agrostar-component--readonly" data-feed-idx="${idx}" disabled>
|
||||
${buildComponentOptions(line, defaultId)}
|
||||
</select>
|
||||
</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
showEl(wrap);
|
||||
setActionButtons();
|
||||
}
|
||||
|
||||
function renderRationIndicatorsTable() {
|
||||
const wrap = $("labAgrostarTableWrap");
|
||||
const body = $("labAgrostarTableBody");
|
||||
const head = $("labImportTableHead");
|
||||
const title = $("labImportTableTitle");
|
||||
const rows = parseResult?.indicators || [];
|
||||
if (!wrap || !body || !rows.length) return;
|
||||
|
||||
if (title) title.textContent = "Показатели рациона";
|
||||
if (head) {
|
||||
head.innerHTML = `<tr>
|
||||
<th>Показатель</th><th>Норма</th><th>Текущий</th><th>Δ</th>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
const meta = parseResult.meta || {};
|
||||
renderMetaLine(
|
||||
` · ${rows.length} показат.${meta.group ? " · " + escapeHtml(meta.group) : ""}${meta.calcDate ? " · " + escapeHtml(meta.calcDate) : ""}`,
|
||||
);
|
||||
|
||||
body.innerHTML = rows
|
||||
.map((row) => {
|
||||
const delta =
|
||||
row.norm != null && row.current != null ? (row.current - row.norm).toFixed(2) : "—";
|
||||
return `<tr>
|
||||
<td>${escapeHtml(row.name)}</td>
|
||||
<td>${row.norm != null ? escapeHtml(row.norm) : "—"}</td>
|
||||
<td>${row.current != null ? escapeHtml(row.current) : "—"}</td>
|
||||
<td>${escapeHtml(delta)}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
showEl(wrap);
|
||||
setActionButtons();
|
||||
}
|
||||
|
||||
function renderAfterParse() {
|
||||
hideSniffPreview();
|
||||
hideEl($("labAgrostarTableWrap"));
|
||||
const kind = importKind();
|
||||
if (kind === "lab_samples") renderLabSamplesTable();
|
||||
else if (kind === "ration_composition") renderRationCompositionTable();
|
||||
else if (kind === "ration_indicators") renderRationIndicatorsTable();
|
||||
}
|
||||
|
||||
function renderSniffPreview() {
|
||||
const panel = $("labAgrostarSniff");
|
||||
const body = $("labAgrostarSniffBody");
|
||||
if (!panel || !body || !parseResult) {
|
||||
setResult("Нечего нюхать — сначала кинь файл", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const kind = importKind();
|
||||
if (kind === "ration_composition") {
|
||||
renderSniffRationComposition(panel, body);
|
||||
return;
|
||||
}
|
||||
if (kind === "ration_indicators") {
|
||||
renderSniffRationIndicators(panel, body);
|
||||
return;
|
||||
}
|
||||
renderSniffLabSamples(panel, body);
|
||||
}
|
||||
|
||||
function renderSniffRationComposition(panel, body) {
|
||||
const lines = parseResult.feedLines || [];
|
||||
const meta = parseResult.meta || {};
|
||||
const metaRows = Object.entries(meta)
|
||||
.map(([k, v]) => `<tr><th>${escapeHtml(k)}</th><td>${escapeHtml(v)}</td></tr>`)
|
||||
.join("");
|
||||
body.innerHTML = `<article class="lab-agrostar-sniff-card">
|
||||
<header class="lab-agrostar-sniff-card__head">
|
||||
<h3 class="h6 mb-0">ПЛИНОР — состав рациона</h3>
|
||||
<span class="badge text-bg-secondary">${lines.length} корм(ов)</span>
|
||||
</header>
|
||||
<p class="small text-muted">Превью · запись рациона в WESP — отдельным шагом</p>
|
||||
<table class="table table-sm lab-agrostar-sniff-nutrients mb-2">
|
||||
<thead><tr><th>Корм</th><th class="text-end">кг</th><th class="text-end">руб.</th></tr></thead>
|
||||
<tbody>${lines
|
||||
.map(
|
||||
(l) =>
|
||||
`<tr><td>${escapeHtml(l.feedName)}</td><td class="text-end">${escapeHtml(l.dailyKg)}</td><td class="text-end">${escapeHtml(l.costRub)}</td></tr>`,
|
||||
)
|
||||
.join("")}</tbody>
|
||||
</table>
|
||||
${metaRows ? `<table class="table table-sm lab-agrostar-sniff-meta mb-0"><tbody>${metaRows}</tbody></table>` : ""}
|
||||
</article>`;
|
||||
showEl(panel);
|
||||
setResult(`Понюхали · ${parseResult.sourceLabel || "ПЛИНОР"} · ${lines.length} строк`, false);
|
||||
}
|
||||
|
||||
function renderSniffRationIndicators(panel, body) {
|
||||
const rows = parseResult.indicators || [];
|
||||
body.innerHTML = `<article class="lab-agrostar-sniff-card">
|
||||
<header class="lab-agrostar-sniff-card__head">
|
||||
<h3 class="h6 mb-0">ПЛИНОР — зоопоказатели</h3>
|
||||
<span class="badge text-bg-secondary">${rows.length} показат.</span>
|
||||
</header>
|
||||
<table class="table table-sm lab-agrostar-sniff-nutrients mb-0">
|
||||
<thead><tr><th>Показатель</th><th class="text-end">Норма</th><th class="text-end">Текущий</th></tr></thead>
|
||||
<tbody>${rows
|
||||
.map(
|
||||
(r) =>
|
||||
`<tr><td>${escapeHtml(r.name)}</td><td class="text-end">${escapeHtml(r.norm)}</td><td class="text-end fw-semibold">${escapeHtml(r.current)}</td></tr>`,
|
||||
)
|
||||
.join("")}</tbody>
|
||||
</table>
|
||||
</article>`;
|
||||
showEl(panel);
|
||||
setResult(`Понюхали · ${parseResult.sourceLabel || "ПЛИНОР"} · ${rows.length} показат.`, false);
|
||||
}
|
||||
|
||||
function renderSniffLabSamples(panel, body) {
|
||||
if (!parseResult?.samples?.length) {
|
||||
setResult("Нечего нюхать — нет проб", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const labName = parseResult.labName || parseResult.sourceLabel || "AgroStar";
|
||||
const cards = parseResult.samples.map((sample) => {
|
||||
const preview = sample.preview;
|
||||
if (!preview) return "";
|
||||
|
||||
const selectedName = getSelectedComponentLabel(sample.sampleNo);
|
||||
const targetLine = selectedName
|
||||
? `<p class="lab-agrostar-sniff__target mb-2"><span class="text-muted">Куда запишем:</span> <strong>${escapeHtml(selectedName)}</strong></p>`
|
||||
: `<p class="lab-agrostar-sniff__target lab-agrostar-sniff__target--warn mb-2">Реагент не выбран — «+ добавить новый реагент»</p>`;
|
||||
|
||||
const metaRows = (preview.meta || [])
|
||||
.map((row) => `<tr><th scope="row">${escapeHtml(row.label)}</th><td>${escapeHtml(row.value)}</td></tr>`)
|
||||
.join("");
|
||||
const nutrientRows = (preview.willWrite || [])
|
||||
.map((row) => {
|
||||
const sourceCell =
|
||||
row.sourceValue != null
|
||||
? `<td class="text-end">${escapeHtml(row.sourceValue)} <span class="text-muted small">${escapeHtml(row.sourceUnit || "")}</span></td>`
|
||||
: `<td class="text-end text-muted">—</td>`;
|
||||
return `<tr><td>${escapeHtml(row.label)}</td>${sourceCell}<td class="text-end fw-semibold">${escapeHtml(row.value)}</td><td class="text-muted small">${escapeHtml(row.unit)}</td></tr>`;
|
||||
})
|
||||
.join("");
|
||||
const notes = (preview.notes || []).map((note) => `<li>${escapeHtml(note)}</li>`).join("");
|
||||
const notesBlock = notes
|
||||
? `<div class="lab-agrostar-sniff__notes mt-3"><div class="small text-muted mb-1">Замечания</div><ul class="small mb-0">${notes}</ul></div>`
|
||||
: "";
|
||||
const storage = preview.storage || {};
|
||||
const skipNote = formatUnsupportedNotice(storage);
|
||||
const storageBlock = skipNote
|
||||
? `<p class="small lab-agrostar-sniff__skip mt-3 mb-0">${escapeHtml(skipNote)}</p>`
|
||||
: `<p class="small text-muted mt-3 mb-0">В каталог: СВ + ${storage.nutrientCount || 0} показат.</p>`;
|
||||
|
||||
return `<article class="lab-agrostar-sniff-card">
|
||||
<header class="lab-agrostar-sniff-card__head">
|
||||
<h3 class="h6 mb-0">${escapeHtml(preview.title || sample.label)}</h3>
|
||||
<span class="badge text-bg-secondary">${escapeHtml(preview.feedTypeRu || "—")}</span>
|
||||
</header>
|
||||
${targetLine}
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><div class="small text-muted mb-1">Из файла</div>
|
||||
<table class="table table-sm lab-agrostar-sniff-meta mb-0"><tbody>${metaRows}</tbody></table>
|
||||
</div>
|
||||
<div class="col-md-6"><div class="small text-muted mb-1">WESP · ${preview.recognizedCount || 0} показат. · % из документа → г/кг СВ (×10)</div>
|
||||
<table class="table table-sm lab-agrostar-sniff-nutrients mb-0">
|
||||
<thead><tr><th>Показатель</th><th class="text-end">Документ</th><th class="text-end">WESP</th><th></th></tr></thead>
|
||||
<tbody>${nutrientRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
${notesBlock}${storageBlock}
|
||||
</article>`;
|
||||
});
|
||||
|
||||
body.innerHTML = cards.join("");
|
||||
showEl(panel);
|
||||
setResult(`Понюхали · ${labName} · ${parseResult.samples.length} образц(ов)`, false);
|
||||
}
|
||||
|
||||
function hideSniffPreview() {
|
||||
const panel = $("labAgrostarSniff");
|
||||
const body = $("labAgrostarSniffBody");
|
||||
hideEl(panel);
|
||||
if (body) body.innerHTML = "";
|
||||
}
|
||||
|
||||
function collectAssignments() {
|
||||
if (!parseResult?.samples?.length) return [];
|
||||
const byNo = new Map(parseResult.samples.map((s) => [s.sampleNo, s]));
|
||||
const assignments = [];
|
||||
document.querySelectorAll(".lab-agrostar-component[data-sample-no]").forEach((sel) => {
|
||||
const sampleNo = sel.getAttribute("data-sample-no") || "";
|
||||
const sample = byNo.get(sampleNo);
|
||||
if (!sample) return;
|
||||
const componentId = sel.value && sel.value !== CREATE_NEW ? sel.value : null;
|
||||
assignments.push({
|
||||
sampleNo,
|
||||
componentId,
|
||||
dryMatterPct: sample.dryMatterPct,
|
||||
nutrients: sample.nutrients || {},
|
||||
warnings: sample.warnings || [],
|
||||
});
|
||||
});
|
||||
return assignments;
|
||||
}
|
||||
|
||||
function fileAllowed(name) {
|
||||
const lower = (name || "").toLowerCase();
|
||||
return ACCEPT_EXT.some((ext) => lower.endsWith(ext));
|
||||
}
|
||||
|
||||
async function uploadFile(file) {
|
||||
if (!file) return;
|
||||
if (!fileAllowed(file.name)) {
|
||||
setStatus("Формат не тот — xml, pdf или xlsx", true);
|
||||
return;
|
||||
}
|
||||
selectedFile = file;
|
||||
const fileNameEl = $("labAgrostarFileName");
|
||||
if (fileNameEl) {
|
||||
showEl(fileNameEl);
|
||||
fileNameEl.textContent = file.name;
|
||||
}
|
||||
setStatus("Разбираем поставку…");
|
||||
setResult("");
|
||||
hideSniffPreview();
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
try {
|
||||
parseResult = await api("/api/lab/import/parse", { method: "POST", body: fd });
|
||||
await loadComponents();
|
||||
renderAfterParse();
|
||||
const kind = importKind();
|
||||
if (kind === "lab_samples") {
|
||||
setStatus(`На столе: ${parseResult.sampleCount || 0} образц(ов) · ${parseResult.sourceLabel || ""}`);
|
||||
} else if (kind === "ration_composition") {
|
||||
setStatus(`Рацион: ${parseResult.feedCount || 0} корм(ов) · ${parseResult.sourceLabel || ""}`);
|
||||
} else {
|
||||
setStatus(`Показатели: ${parseResult.indicatorCount || 0} · ${parseResult.sourceLabel || ""}`);
|
||||
}
|
||||
} catch (err) {
|
||||
parseResult = null;
|
||||
hideEl($("labAgrostarTableWrap"));
|
||||
hideEl($("labAgrostarMeta"));
|
||||
setStatus(err.message || "Файл не разобрался", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function runApply() {
|
||||
if (!isLabSamples()) {
|
||||
setResult("Запись в каталог — только для лабораторных проб AgroStar", true);
|
||||
return;
|
||||
}
|
||||
const assignments = collectAssignments();
|
||||
if (!assignments.length) {
|
||||
setResult("Нечего готовить — сначала кинь файл", true);
|
||||
return;
|
||||
}
|
||||
const missing = assignments.filter((a) => !a.componentId);
|
||||
if (missing.length) {
|
||||
setResult(`Укажи реагент для ${missing.length} образц(ов) — Jesse бы не забыл`, true);
|
||||
return;
|
||||
}
|
||||
setResult("Пишем в каталог…");
|
||||
try {
|
||||
const data = await api("/api/lab/import/agrostar-xml/apply", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ assignments, dryRun: false }),
|
||||
});
|
||||
const lines = (data.results || []).map(
|
||||
(r) => `${r.sampleNo}: ${r.skipped ? "мимо" : r.componentName || "—"} (${r.nutrientCount} показат.)`,
|
||||
);
|
||||
setResult(
|
||||
`Запомнили · в деле ${data.applied}, мимо ${data.skipped}` +
|
||||
(lines.length ? "\n" + lines.join("\n") : ""),
|
||||
false,
|
||||
);
|
||||
notifyCatalogChanged();
|
||||
} catch (err) {
|
||||
setResult(err.message || "Наука не сработала — импорт сгорел", true);
|
||||
}
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
parseResult = null;
|
||||
selectedFile = null;
|
||||
const input = $("labAgrostarFile");
|
||||
if (input) input.value = "";
|
||||
const fileNameEl = $("labAgrostarFileName");
|
||||
if (fileNameEl) {
|
||||
hideEl(fileNameEl);
|
||||
fileNameEl.textContent = "";
|
||||
}
|
||||
hideEl($("labAgrostarTableWrap"));
|
||||
hideEl($("labAgrostarMeta"));
|
||||
hideSniffPreview();
|
||||
$("labAgrostarPreview").disabled = true;
|
||||
$("labAgrostarApply").disabled = true;
|
||||
$("labAgrostarClear").disabled = true;
|
||||
setStatus("");
|
||||
setResult("");
|
||||
}
|
||||
|
||||
function bindDropZone() {
|
||||
const drop = $("labAgrostarDrop");
|
||||
const input = $("labAgrostarFile");
|
||||
if (!drop || !input) return;
|
||||
|
||||
drop.addEventListener("click", () => input.click());
|
||||
drop.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
input.click();
|
||||
}
|
||||
});
|
||||
input.addEventListener("change", () => {
|
||||
const file = input.files?.[0];
|
||||
if (file) uploadFile(file);
|
||||
});
|
||||
|
||||
["dragenter", "dragover"].forEach((ev) => {
|
||||
drop.addEventListener(ev, (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
drop.classList.add("lab-agrostar-drop--active");
|
||||
});
|
||||
});
|
||||
["dragleave", "drop"].forEach((ev) => {
|
||||
drop.addEventListener(ev, (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
drop.classList.remove("lab-agrostar-drop--active");
|
||||
});
|
||||
});
|
||||
drop.addEventListener("drop", (e) => {
|
||||
const file = e.dataTransfer?.files?.[0];
|
||||
if (file) uploadFile(file);
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!$("labAgrostarDrop")) return;
|
||||
bindDropZone();
|
||||
$("labAgrostarTableWrap")?.addEventListener("change", onComponentSelectChange);
|
||||
$("labAgrostarPreview")?.addEventListener("click", renderSniffPreview);
|
||||
$("labAgrostarApply")?.addEventListener("click", runApply);
|
||||
$("labAgrostarClear")?.addEventListener("click", clearAll);
|
||||
loadComponents().catch(() => {});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})(window);
|
||||
Reference in New Issue
Block a user