/** * Модалка «Учётные документы» на /feed_consumption * Сценарий: Реквизиты → Подписи → Экспорт */ (function () { const ORG_FIELDS = [ { id: "faOrgName", key: "organization_name", label: "Организация" }, { id: "faOkpo", key: "okpo", label: "ОКПО" }, { id: "faDepartment", key: "department", label: "Отделение (участок)" }, { id: "faFarm", key: "farm_name", label: "Ферма" }, { id: "faBrigade", key: "brigade", label: "Бригада" }, { id: "faAnimalType", key: "animal_type", label: "Вид / группа скота" }, { id: "faResponsible", key: "responsible_person", label: "Ответственный" }, { id: "faZootech", key: "zootechnician", label: "Зоотехник (ФИО)" }, { id: "faWarehouse", key: "warehouse_keeper", label: "Кладовщик (ФИО)" }, { id: "faDocPrefix", key: "document_number_prefix", label: "Префикс номера документа" }, ]; const SIGNATURES = [ { role: "zootechnician", label: "Зоотехник", canvasId: "faSigZootech" }, { role: "warehouse_keeper", label: "Кладовщик", canvasId: "faSigWarehouse" }, { role: "recipient", label: "Получатель", canvasId: "faSigRecipient" }, ]; const canvases = {}; let lastSignatureStatus = {}; function getExportMonth() { const monthEl = document.getElementById("faExportMonth"); if (monthEl && monthEl.value) { return monthEl.value; } const fromEl = document.getElementById("consumptionDateFrom"); if (fromEl && fromEl.value) { return fromEl.value.slice(0, 7); } const now = new Date(); return now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0"); } function getDateRange() { const fromEl = document.getElementById("consumptionDateFrom"); const toEl = document.getElementById("consumptionDateTo"); let from = fromEl && fromEl.value; let to = toEl && toEl.value; if (!from || !to) { const now = new Date(); from = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10); to = new Date(now.getFullYear(), now.getMonth() + 1, 0).toISOString().slice(0, 10); } return { from, to, month: getExportMonth() }; } function syncExportMonthField() { const monthEl = document.getElementById("faExportMonth"); if (!monthEl || monthEl.value) return; monthEl.value = getExportMonth(); } async function refreshSp20Status() { const el = document.getElementById("faSp20Status"); if (!el) return; const month = getExportMonth(); el.textContent = "Проверка данных за " + month + "…"; try { const r = await fetch("/api/feed-accounting/sp20-preview?month=" + encodeURIComponent(month), { credentials: "same-origin", }); if (!r.ok) { el.textContent = "Не удалось загрузить сводку по СП-20."; return; } const data = await r.json(); if (!data.lines_count) { el.textContent = "За " + month + " нет строк расхода в отчётах загрузки. Проверьте месяц или наличие отчётов."; el.className = "text-warning small mb-3"; return; } let msg = "СП-20 за " + month + ": " + data.lines_count + " строк, " + data.total_kg + " кг"; if (!data.has_requisites) { msg += ". Заполните реквизиты на вкладке «Реквизиты СП-20»."; el.className = "text-warning small mb-3"; } else { el.className = "text-success small mb-3"; } el.textContent = msg; } catch (err) { el.textContent = "Не удалось загрузить сводку по СП-20."; el.className = "text-danger small mb-3"; } } function userError(message, fallback) { const notifyApi = window.WespZootechNotify; if (notifyApi && typeof notifyApi.userFacingMessage === "function") { return notifyApi.userFacingMessage(message, fallback); } return fallback || "Не удалось выполнить операцию. Попробуйте ещё раз."; } function notify(msg, type) { if (typeof notyf !== "undefined") { if (type === "error") notyf.error(msg); else notyf.success(msg); } } async function downloadUrl(url, label) { const fallback = "Не удалось скачать документ. Попробуйте ещё раз."; try { const r = await fetch(url, { credentials: "same-origin" }); if (!r.ok) { let body = null; try { body = await r.json(); } catch (e) { /* ignore */ } const notifyApi = window.WespZootechNotify; const msg = notifyApi && typeof notifyApi.messageFromResponseBody === "function" ? notifyApi.messageFromResponseBody(body, fallback) : userError(null, fallback); notify(msg, "error"); return; } const blob = await r.blob(); const cd = r.headers.get("Content-Disposition") || ""; const m = cd.match(/filename="([^"]+)"/); const filename = m ? m[1] : "download"; const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(function () { URL.revokeObjectURL(a.href); }, 5000); } catch (err) { notify(userError(null, fallback), "error"); } } function showTab(name) { document.querySelectorAll(".fa-tab-panel").forEach(function (el) { el.hidden = el.dataset.tab !== name; }); document.querySelectorAll(".fa-tab-btn").forEach(function (btn) { btn.classList.toggle("active", btn.dataset.tab === name); }); if (name === "export") { syncExportMonthField(); refreshSp20Status(); } } function setupCanvas(canvas) { if (!canvas || canvases[canvas.id]) return; const ctx = canvas.getContext("2d"); ctx.strokeStyle = "#1a1a1a"; ctx.lineWidth = 2; ctx.lineCap = "round"; let drawing = false; function pos(e) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const ev = e.touches ? e.touches[0] : e; return { x: (ev.clientX - rect.left) * scaleX, y: (ev.clientY - rect.top) * scaleY, }; } function start(e) { drawing = true; const p = pos(e); ctx.beginPath(); ctx.moveTo(p.x, p.y); e.preventDefault(); } function move(e) { if (!drawing) return; const p = pos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); e.preventDefault(); } function end() { drawing = false; } canvas.addEventListener("mousedown", start); canvas.addEventListener("mousemove", move); canvas.addEventListener("mouseup", end); canvas.addEventListener("mouseleave", end); canvas.addEventListener("touchstart", start, { passive: false }); canvas.addEventListener("touchmove", move, { passive: false }); canvas.addEventListener("touchend", end); canvases[canvas.id] = { canvas: canvas, ctx: ctx }; } function clearCanvas(canvasId) { const item = canvases[canvasId]; if (!item) return; item.ctx.clearRect(0, 0, item.canvas.width, item.canvas.height); } function canvasHasInk(canvasId) { const item = canvases[canvasId]; if (!item) return false; const data = item.ctx.getImageData(0, 0, item.canvas.width, item.canvas.height).data; for (let i = 3; i < data.length; i += 4) { if (data[i] > 0) return true; } return false; } function fillFarmDatalist(farms) { const list = document.getElementById("faFarmList"); if (!list) return; list.innerHTML = ""; (farms || []).forEach(function (farm) { const opt = document.createElement("option"); opt.value = farm; list.appendChild(opt); }); } function isSetupComplete(settings, sigStatus) { const hasRequisites = Boolean( (settings.organization_name || "").trim() && (settings.zootechnician || "").trim() && (settings.warehouse_keeper || "").trim() ); const hasSignatures = Boolean(sigStatus.zootechnician && sigStatus.warehouse_keeper); return hasRequisites && hasSignatures; } async function loadOrgSettings() { const r = await fetch("/api/feed-accounting/org-settings", { credentials: "same-origin" }); if (!r.ok) return null; const data = await r.json(); ORG_FIELDS.forEach(function (f) { const el = document.getElementById(f.id); if (el) el.value = data[f.key] || ""; }); fillFarmDatalist(data.available_farms || []); if (data.signatures) { lastSignatureStatus = data.signatures; } return data; } async function loadSignaturesOntoCanvases() { const roles = lastSignatureStatus; await Promise.all( SIGNATURES.map(function (s) { if (!roles[s.role]) return Promise.resolve(); return new Promise(function (resolve) { const img = new Image(); img.onload = function () { const item = canvases[s.canvasId]; if (item) { item.ctx.clearRect(0, 0, item.canvas.width, item.canvas.height); item.ctx.drawImage(img, 0, 0, item.canvas.width, item.canvas.height); } resolve(); }; img.onerror = function () { resolve(); }; img.src = "/api/feed-accounting/signatures/" + s.role + ".png?t=" + Date.now(); }); }) ); } async function saveOrgSettings(silent) { const payload = {}; ORG_FIELDS.forEach(function (f) { const el = document.getElementById(f.id); if (el) payload[f.key] = el.value.trim(); }); const r = await fetch("/api/feed-accounting/org-settings", { method: "PUT", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await r.json(); if (!r.ok) { notify(userError(data.message, "Не удалось сохранить реквизиты"), "error"); return false; } if (!silent) notify("Реквизиты сохранены"); return true; } async function saveSignature(role, canvasId, silent) { const item = canvases[canvasId]; if (!item || !canvasHasInk(canvasId)) return true; const dataUrl = item.canvas.toDataURL("image/png"); const r = await fetch("/api/feed-accounting/signatures/" + role, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ png_base64: dataUrl }), }); const data = await r.json(); if (!r.ok) { notify(userError(data.message, "Не удалось сохранить подпись"), "error"); return false; } if (data.roles) lastSignatureStatus = data.roles; if (!silent) notify("Подпись сохранена"); return true; } async function saveAll() { const btn = document.getElementById("faSaveAllBtn"); if (btn) btn.disabled = true; try { const orgOk = await saveOrgSettings(true); if (!orgOk) return; for (const s of SIGNATURES) { const ok = await saveSignature(s.role, s.canvasId, true); if (!ok) return; } notify("Реквизиты и подписи сохранены"); const settings = await loadOrgSettings(); if (settings && isSetupComplete(settings, lastSignatureStatus)) { showTab("export"); } } finally { if (btn) btn.disabled = false; } } window.openFeedAccountingModal = async function () { const modal = document.getElementById("feedAccountingModal"); if (!modal) return; modal.style.display = "flex"; document.body.style.overflow = "hidden"; SIGNATURES.forEach(function (s) { setupCanvas(document.getElementById(s.canvasId)); }); const settings = await loadOrgSettings(); await loadSignaturesOntoCanvases(); syncExportMonthField(); const complete = settings && isSetupComplete(settings, lastSignatureStatus); showTab(complete ? "export" : "requisites"); }; window.closeFeedAccountingModal = function () { const modal = document.getElementById("feedAccountingModal"); if (!modal) return; modal.style.display = "none"; document.body.style.overflow = ""; }; document.addEventListener("DOMContentLoaded", function () { const openBtn = document.getElementById("openFeedAccountingBtn"); if (openBtn) openBtn.addEventListener("click", function () { openFeedAccountingModal(); }); document.querySelectorAll(".fa-tab-btn").forEach(function (btn) { btn.addEventListener("click", function () { showTab(btn.dataset.tab); }); }); const exportMonthEl = document.getElementById("faExportMonth"); if (exportMonthEl) { exportMonthEl.addEventListener("change", refreshSp20Status); } const saveAllBtn = document.getElementById("faSaveAllBtn"); if (saveAllBtn) saveAllBtn.addEventListener("click", saveAll); SIGNATURES.forEach(function (s) { const clearBtn = document.getElementById("clear_" + s.canvasId); if (clearBtn) clearBtn.addEventListener("click", function () { clearCanvas(s.canvasId); }); }); const exports = [ { id: "faExportConsumptionXlsx", path: function (d) { return "/api/feed-accounting/consumption.xlsx?date_from=" + encodeURIComponent(d.from) + "&date_to=" + encodeURIComponent(d.to); }, label: "Потребление Excel" }, { id: "faExportConsumptionPdf", path: function (d) { return "/api/feed-accounting/consumption.pdf?date_from=" + encodeURIComponent(d.from) + "&date_to=" + encodeURIComponent(d.to); }, label: "Потребление PDF" }, { id: "faExportStock", path: function (d) { return "/api/feed-accounting/stock-balances.xlsx?date_from=" + encodeURIComponent(d.from) + "&date_to=" + encodeURIComponent(d.to); }, label: "Остатки" }, { id: "faExportSp20Xlsx", path: function (d) { return "/api/feed-accounting/sp20.xlsx?month=" + encodeURIComponent(d.month); }, label: "СП-20 Excel" }, { id: "faExportSp20Pdf", path: function (d) { return "/api/feed-accounting/sp20.pdf?month=" + encodeURIComponent(d.month); }, label: "СП-20 PDF" }, { id: "faExportJournalXlsx", path: function (d) { return "/api/feed-accounting/journal.xlsx?month=" + encodeURIComponent(d.month); }, label: "Журнал Excel" }, { id: "faExportJournalPdf", path: function (d) { return "/api/feed-accounting/journal.pdf?month=" + encodeURIComponent(d.month); }, label: "Журнал PDF" }, { id: "faExportZip", path: function (d) { return "/api/feed-accounting/documents.zip?date_from=" + encodeURIComponent(d.from) + "&date_to=" + encodeURIComponent(d.to) + "&month=" + encodeURIComponent(d.month); }, label: "ZIP" }, ]; exports.forEach(function (ex) { const btn = document.getElementById(ex.id); if (!btn) return; btn.addEventListener("click", function () { const d = getDateRange(); downloadUrl(ex.path(d), ex.label); }); }); const modal = document.getElementById("feedAccountingModal"); if (modal) { modal.addEventListener("click", function (e) { if (e.target === modal) closeFeedAccountingModal(); }); } }); })();