402 lines
15 KiB
JavaScript
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();
|
|
});
|
|
}
|
|
});
|
|
})();
|