Add optional signature upload to invoice PDF
Adds a drag-and-drop/click upload field on the invoice editor for an optional 420x210 JPG/PNG signature image. The signature is stored as part of invoice_data and rendered on the generated PDF directly above the QR code, right-aligned with sensible margins. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -45,6 +45,12 @@ const dueDateDisplay = document.getElementById("dueDateDisplay");
|
||||
const paymentPresetSelect = document.getElementById("paymentPresetSelect");
|
||||
const paymentPresetName = document.getElementById("paymentPresetName");
|
||||
const savePaymentPresetButton = document.getElementById("savePaymentPresetButton");
|
||||
const signatureDropzone = document.getElementById("signatureDropzone");
|
||||
const signatureInput = document.getElementById("signatureInput");
|
||||
const signaturePreview = document.getElementById("signaturePreview");
|
||||
const signatureDropzoneText = document.getElementById("signatureDropzoneText");
|
||||
const removeSignatureButton = document.getElementById("removeSignatureButton");
|
||||
const signatureStatus = document.getElementById("signatureStatus");
|
||||
const loadPaymentPresetButton = document.getElementById("loadPaymentPresetButton");
|
||||
const deletePaymentPresetButton = document.getElementById("deletePaymentPresetButton");
|
||||
|
||||
@@ -68,6 +74,93 @@ let invoices = [];
|
||||
let paymentPresets = [];
|
||||
let currentInvoiceId = null;
|
||||
let isRegistrationMode = false;
|
||||
let signatureDataUrl = null;
|
||||
|
||||
const SIGNATURE_WIDTH = 420;
|
||||
const SIGNATURE_HEIGHT = 210;
|
||||
|
||||
const setSignatureStatus = (message, isError = false) => {
|
||||
signatureStatus.textContent = message || "";
|
||||
signatureStatus.classList.toggle("is-error", Boolean(isError));
|
||||
};
|
||||
|
||||
const applySignaturePreview = (dataUrl) => {
|
||||
signatureDataUrl = dataUrl || null;
|
||||
if (signatureDataUrl) {
|
||||
signaturePreview.src = signatureDataUrl;
|
||||
signaturePreview.hidden = false;
|
||||
signatureDropzoneText.hidden = true;
|
||||
removeSignatureButton.hidden = false;
|
||||
} else {
|
||||
signaturePreview.hidden = true;
|
||||
signaturePreview.src = "";
|
||||
signatureDropzoneText.hidden = false;
|
||||
removeSignatureButton.hidden = true;
|
||||
}
|
||||
};
|
||||
|
||||
const readImageDimensions = (dataUrl) => new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => resolve({ width: image.naturalWidth, height: image.naturalHeight });
|
||||
image.onerror = () => reject(new Error("Obrázek se nepodařilo načíst."));
|
||||
image.src = dataUrl;
|
||||
});
|
||||
|
||||
const handleSignatureFile = async (file) => {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (!["image/png", "image/jpeg"].includes(file.type)) {
|
||||
setSignatureStatus("Podporovány jsou pouze soubory JPG nebo PNG.", true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const dataUrl = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = () => reject(new Error("Soubor se nepodařilo načíst."));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
const { width, height } = await readImageDimensions(dataUrl);
|
||||
if (width !== SIGNATURE_WIDTH || height !== SIGNATURE_HEIGHT) {
|
||||
setSignatureStatus(`Obrázek musí mít rozměry přesně ${SIGNATURE_WIDTH}×${SIGNATURE_HEIGHT} px (nahráno ${width}×${height} px).`, true);
|
||||
return;
|
||||
}
|
||||
applySignaturePreview(dataUrl);
|
||||
setSignatureStatus("Podpis byl nahrán.");
|
||||
} catch (error) {
|
||||
setSignatureStatus(error.message, true);
|
||||
}
|
||||
};
|
||||
|
||||
signatureDropzone.addEventListener("click", () => signatureInput.click());
|
||||
signatureDropzone.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
signatureInput.click();
|
||||
}
|
||||
});
|
||||
signatureDropzone.addEventListener("dragover", (event) => {
|
||||
event.preventDefault();
|
||||
signatureDropzone.classList.add("is-dragover");
|
||||
});
|
||||
signatureDropzone.addEventListener("dragleave", () => {
|
||||
signatureDropzone.classList.remove("is-dragover");
|
||||
});
|
||||
signatureDropzone.addEventListener("drop", (event) => {
|
||||
event.preventDefault();
|
||||
signatureDropzone.classList.remove("is-dragover");
|
||||
handleSignatureFile(event.dataTransfer.files[0]);
|
||||
});
|
||||
signatureInput.addEventListener("change", () => {
|
||||
handleSignatureFile(signatureInput.files[0]);
|
||||
signatureInput.value = "";
|
||||
});
|
||||
removeSignatureButton.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
applySignaturePreview(null);
|
||||
setSignatureStatus("");
|
||||
});
|
||||
|
||||
const profileFieldMap = {
|
||||
nazevSpolecnosti: "NazevSpolecnosti",
|
||||
@@ -247,6 +340,7 @@ const readInvoiceData = () => ({
|
||||
iban: getValue("iban"),
|
||||
swift: getValue("swift")
|
||||
},
|
||||
signature: signatureDataUrl,
|
||||
total: Number.parseFloat(getValue("total").replace(",", ".")) || 0,
|
||||
items: Array.from(itemsContainer.querySelectorAll(".invoice-item")).map((item) => {
|
||||
const inputs = item.querySelectorAll("input");
|
||||
@@ -273,6 +367,8 @@ const clearInvoiceEditor = () => {
|
||||
["cisloUctu", "iban", "swift"].forEach((id) => {
|
||||
document.getElementById(id).value = "";
|
||||
});
|
||||
applySignaturePreview(null);
|
||||
setSignatureStatus("");
|
||||
itemsContainer.replaceChildren();
|
||||
itemIndex = 0;
|
||||
updateTotal();
|
||||
@@ -294,6 +390,8 @@ const loadInvoiceIntoEditor = (invoice) => {
|
||||
["cisloUctu", "iban", "swift"].forEach((id) => {
|
||||
document.getElementById(id).value = invoice.data.payment?.[id] || "";
|
||||
});
|
||||
applySignaturePreview(invoice.data.signature || null);
|
||||
setSignatureStatus("");
|
||||
itemsContainer.replaceChildren();
|
||||
itemIndex = 0;
|
||||
(invoice.data.items || []).forEach((item) => addInvoiceItem(item));
|
||||
|
||||
Reference in New Issue
Block a user