Files
site/WESP_REL/static/js/feed-accounting-modal.js
2026-07-17 12:57:18 +03:00

402 lines
15 KiB
JavaScript

/**
* Модалка «Учётные документы» на /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();
});
}
});
})();