я косяк, поправил веб. Капитан, работайте!
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Модалка «Учётные документы» на /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();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user