const addItemButton = document.getElementById("addItemButton"); const itemsContainer = document.getElementById("itemsContainer"); const printInvoiceButton = document.getElementById("printInvoiceButton"); const saveInvoiceButton = document.getElementById("saveInvoiceButton"); const invoiceSaveStatus = document.getElementById("invoiceSaveStatus"); const invoiceNumberDisplay = document.getElementById("invoiceNumberDisplay"); const invoiceList = document.getElementById("invoiceList"); const invoiceListScreen = document.getElementById("invoice-list-screen"); const invoiceEditorScreen = document.getElementById("invoice-editor-screen"); const invoiceListNavButton = document.getElementById("invoiceListNavButton"); const newInvoiceButton = document.getElementById("newInvoiceButton"); const exportInvoicesButton = document.getElementById("exportInvoicesButton"); const deleteAllInvoicesButton = document.getElementById("deleteAllInvoicesButton"); const currentViewLabel = document.getElementById("currentViewLabel"); const saveSupplierButton = document.getElementById("saveSupplierButton"); const loadSupplierButton = document.getElementById("loadSupplierButton"); const supplierSaveStatus = document.getElementById("supplierSaveStatus"); const clientSelect = document.getElementById("clientSelect"); const clientTypeSelect = document.getElementById("odberatelTypSubjektu"); const personFields = document.getElementById("personFields"); const companyFields = document.getElementById("companyFields"); const supplierTypeSelect = document.getElementById("dodavatelTypSubjektu"); const supplierPersonFields = document.getElementById("supplierPersonFields"); const supplierCompanyFields = document.getElementById("supplierCompanyFields"); const saveClientButton = document.getElementById("saveClientButton"); const deleteClientButton = document.getElementById("deleteClientButton"); const authScreen = document.getElementById("auth-screen"); const authForm = document.getElementById("auth-form"); const authEmail = document.getElementById("authEmail"); const authPassword = document.getElementById("authPassword"); const authSubmitButton = document.getElementById("authSubmitButton"); const authModeButton = document.getElementById("authModeButton"); const forgotPasswordButton = document.getElementById("forgotPasswordButton"); const authStatus = document.getElementById("authStatus"); const logoutButton = document.getElementById("logoutButton"); const signedInEmail = document.getElementById("navEmail"); const passwordButton = document.getElementById("passwordButton"); const passwordPanel = document.getElementById("passwordPanel"); const passwordForm = document.getElementById("passwordForm"); const savePasswordButton = document.getElementById("savePasswordButton"); const cancelPasswordButton = document.getElementById("cancelPasswordButton"); const passwordStatus = document.getElementById("passwordStatus"); const numberingSettingsButton = document.getElementById("numberingSettingsButton"); const numberingSettingsPanel = document.getElementById("numberingSettingsPanel"); const invoiceStartingNumberInput = document.getElementById("invoiceStartingNumber"); const saveNumberingSettingsButton = document.getElementById("saveNumberingSettingsButton"); const cancelNumberingSettingsButton = document.getElementById("cancelNumberingSettingsButton"); const numberingSettingsStatus = document.getElementById("numberingSettingsStatus"); const issueDateInput = document.getElementById("issueDate"); const dueDateInput = document.getElementById("dueDate"); 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 signaturePresetSelect = document.getElementById("signaturePresetSelect"); const signaturePresetName = document.getElementById("signaturePresetName"); const saveSignaturePresetButton = document.getElementById("saveSignaturePresetButton"); const deleteSignaturePresetButton = document.getElementById("deleteSignaturePresetButton"); const loadPaymentPresetButton = document.getElementById("loadPaymentPresetButton"); const deletePaymentPresetButton = document.getElementById("deletePaymentPresetButton"); document.querySelectorAll(".nav-menu button").forEach((button) => { button.addEventListener("click", () => { button.closest("details").open = false; }); }); document.addEventListener("click", (event) => { document.querySelectorAll(".nav-menu[open]").forEach((menu) => { if (!menu.contains(event.target)) { menu.open = false; } }); }); let itemIndex = 0; let clients = []; let invoices = []; let paymentPresets = []; let signaturePresets = []; 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.removeAttribute("src"); signatureDropzoneText.hidden = false; removeSignatureButton.hidden = true; } }; const renderSignaturePresetOptions = (selectedId = signaturePresetSelect.value) => { signaturePresetSelect.innerHTML = ""; signaturePresets.forEach((preset) => { const option = document.createElement("option"); option.value = preset.id; option.textContent = preset.name; signaturePresetSelect.appendChild(option); }); signaturePresetSelect.value = selectedId; deleteSignaturePresetButton.disabled = !signaturePresetSelect.value; }; const loadSignaturePresets = async () => { const response = await apiRequest("/api/signature-presets"); signaturePresets = response.presets; renderSignaturePresetOptions(); }; 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 = ""; }); signaturePresetSelect.addEventListener("change", () => { const preset = signaturePresets.find(({ id }) => String(id) === signaturePresetSelect.value); deleteSignaturePresetButton.disabled = !preset; if (preset) { signaturePresetName.value = preset.name; applySignaturePreview(preset.dataUrl); setSignatureStatus("Uložený podpis načten."); } }); saveSignaturePresetButton.addEventListener("click", async () => { if (!signatureDataUrl) { setSignatureStatus("Nejprve nahrajte nebo vyberte podpis.", true); return; } try { const response = await apiRequest("/api/signature-presets", { method: "POST", body: JSON.stringify({ name: signaturePresetName.value, dataUrl: signatureDataUrl }) }); const preset = response.preset; signaturePresets = [ ...signaturePresets.filter((entry) => entry.id !== preset.id && entry.name !== preset.name), preset ].sort((left, right) => left.name.localeCompare(right.name)); renderSignaturePresetOptions(String(preset.id)); signaturePresetName.value = preset.name; setSignatureStatus("Podpis byl uložen."); } catch (error) { setSignatureStatus(error.message, true); } }); deleteSignaturePresetButton.addEventListener("click", async () => { if (!signaturePresetSelect.value) { return; } try { await apiRequest(`/api/signature-presets/${signaturePresetSelect.value}`, { method: "DELETE" }); signaturePresets = signaturePresets.filter(({ id }) => String(id) !== signaturePresetSelect.value); signaturePresetName.value = ""; renderSignaturePresetOptions(); } catch (error) { setSignatureStatus(error.message, true); } }); removeSignatureButton.addEventListener("click", (event) => { event.stopPropagation(); applySignaturePreview(null); setSignatureStatus(""); }); const profileFieldMap = { nazevSpolecnosti: "NazevSpolecnosti", ico: "Ico", jmeno: "Jmeno", prijmeni: "Prijmeni", uliceCp: "UliceCp", psc: "Psc", mesto: "Mesto" }; const readProfileFields = (prefix) => Object.fromEntries( Object.entries(profileFieldMap).map(([key, suffix]) => [ key, document.getElementById(`${prefix}${suffix}`).value.trim() ]) ); const writeProfileFields = (prefix, fields) => Object.entries(profileFieldMap).forEach(([key, suffix]) => { const field = document.getElementById(`${prefix}${suffix}`); if (field) { field.value = fields[key] || ""; } }); const writeFields = (fields) => Object.entries(fields).forEach(([id, value]) => { const field = document.getElementById(id); if (field) { field.value = value || ""; } }); const getClientLabel = (client) => [ client.data.nazevSpolecnosti, [client.data.jmeno, client.data.prijmeni].filter(Boolean).join(" "), client.data.ico && `IČO: ${client.data.ico}` ].filter(Boolean).join(" ") || "Bez názvu"; const formatCzechDate = (dateValue) => new Date(`${dateValue}T00:00:00`).toLocaleDateString("cs-CZ"); const toDateInputValue = (date) => date.toISOString().slice(0, 10); const getIssueDate = () => issueDateInput.value || toDateInputValue(new Date()); const getDueDate = () => { if (dueDateInput.value) { return dueDateInput.value; } const dueDate = new Date(`${getIssueDate()}T00:00:00`); dueDate.setDate(dueDate.getDate() + 14); return toDateInputValue(dueDate); }; const updateClientTypeFields = () => { const isCompany = clientTypeSelect.value === "spolecnost"; personFields.hidden = isCompany; companyFields.hidden = !isCompany; }; const updateSupplierTypeFields = () => { const isCompany = supplierTypeSelect.value === "spolecnost"; supplierPersonFields.hidden = isCompany; supplierCompanyFields.hidden = !isCompany; }; const apiRequest = async (url, options = {}) => { const response = await fetch(url, { ...options, credentials: "same-origin", headers: { "Content-Type": "application/json", ...(options.headers || {}) } }); const data = response.status === 204 ? null : await response.json(); if (!response.ok) { throw new Error(data?.error || "Požadavek se nepodařilo dokončit."); } return data; }; const showInvoiceScreen = (screen) => { invoiceListScreen.hidden = screen !== "list"; invoiceEditorScreen.hidden = screen !== "editor"; currentViewLabel.textContent = screen === "list" ? "Seznam faktur" : (currentInvoiceId ? "Upravit fakturu" : "Nová faktura"); }; const renderPaymentPresetOptions = (selectedId = paymentPresetSelect.value) => { paymentPresetSelect.innerHTML = ""; paymentPresets.forEach((preset) => { const option = document.createElement("option"); option.value = preset.id; option.textContent = preset.name; paymentPresetSelect.appendChild(option); }); paymentPresetSelect.value = selectedId; deletePaymentPresetButton.disabled = !paymentPresetSelect.value; }; const loadPaymentPresets = async () => { const response = await apiRequest("/api/payment-presets"); paymentPresets = response.presets; renderPaymentPresetOptions(); }; paymentPresetSelect.addEventListener("change", () => { const preset = paymentPresets.find(({ id }) => String(id) === paymentPresetSelect.value); if (!preset) { deletePaymentPresetButton.disabled = true; return; } paymentPresetName.value = preset.name; deletePaymentPresetButton.disabled = false; }); loadPaymentPresetButton.addEventListener("click", () => { const preset = paymentPresets.find(({ id }) => String(id) === paymentPresetSelect.value); if (!preset) { window.alert("Nejprve vyberte uložené platební údaje."); return; } document.getElementById("cisloUctu").value = preset.accountNumber; document.getElementById("iban").value = preset.iban; document.getElementById("swift").value = preset.swift; }); savePaymentPresetButton.addEventListener("click", async () => { try { const response = await apiRequest("/api/payment-presets", { method: "POST", body: JSON.stringify({ name: paymentPresetName.value, accountNumber: getValue("cisloUctu"), iban: getValue("iban"), swift: getValue("swift") }) }); const preset = response.preset; paymentPresets = [ ...paymentPresets.filter((entry) => entry.id !== preset.id && entry.name !== preset.name), preset ].sort((left, right) => left.name.localeCompare(right.name)); renderPaymentPresetOptions(String(preset.id)); paymentPresetName.value = preset.name; } catch (error) { window.alert(error.message); } }); deletePaymentPresetButton.addEventListener("click", async () => { if (!paymentPresetSelect.value) { return; } try { await apiRequest(`/api/payment-presets/${paymentPresetSelect.value}`, { method: "DELETE" }); paymentPresets = paymentPresets.filter(({ id }) => String(id) !== paymentPresetSelect.value); paymentPresetName.value = ""; renderPaymentPresetOptions(); } catch (error) { window.alert(error.message); } }); const readInvoiceData = () => ({ issueDate: getIssueDate(), dueDate: getDueDate(), supplier: { ...readProfileFields("dodavatel"), typSubjektu: supplierTypeSelect.value }, customer: { ...readProfileFields("odberatel"), typSubjektu: clientTypeSelect.value }, payment: { cisloUctu: getValue("cisloUctu"), 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"); return { popis: inputs[0].value.trim(), mnozstvi: Number(inputs[1].value) || 0, mernaJednotka: inputs[2].value.trim(), cenaZaMj: Number(inputs[3].value) || 0 }; }) }); const clearInvoiceEditor = () => { currentInvoiceId = null; invoiceNumberDisplay.textContent = "Nová faktura"; invoiceSaveStatus.textContent = ""; issueDateInput.value = ""; dueDateInput.value = ""; writeProfileFields("odberatel", {}); clientSelect.value = ""; clientTypeSelect.value = "osoba"; updateClientTypeFields(); paymentPresetSelect.value = ""; paymentPresetName.value = ""; deletePaymentPresetButton.disabled = true; signaturePresetSelect.value = ""; signaturePresetName.value = ""; deleteSignaturePresetButton.disabled = true; ["cisloUctu", "iban", "swift"].forEach((id) => { document.getElementById(id).value = ""; }); applySignaturePreview(null); setSignatureStatus(""); itemsContainer.replaceChildren(); itemIndex = 0; updateTotal(); }; const loadInvoiceIntoEditor = (invoice) => { currentInvoiceId = invoice.id; invoiceNumberDisplay.textContent = invoice.invoiceNumber; invoiceSaveStatus.textContent = ""; issueDateInput.value = invoice.data.issueDate || ""; dueDateInput.value = invoice.data.dueDate || ""; writeProfileFields("dodavatel", invoice.data.supplier || {}); supplierTypeSelect.value = invoice.data.supplier?.typSubjektu || "osoba"; updateSupplierTypeFields(); writeProfileFields("odberatel", invoice.data.customer || {}); clientTypeSelect.value = invoice.data.customer?.typSubjektu || "osoba"; updateClientTypeFields(); paymentPresetSelect.value = ""; paymentPresetName.value = ""; deletePaymentPresetButton.disabled = true; ["cisloUctu", "iban", "swift"].forEach((id) => { document.getElementById(id).value = invoice.data.payment?.[id] || ""; }); applySignaturePreview(invoice.data.signature || null); signaturePresetSelect.value = ""; signaturePresetName.value = ""; deleteSignaturePresetButton.disabled = true; setSignatureStatus(""); itemsContainer.replaceChildren(); itemIndex = 0; (invoice.data.items || []).forEach((item) => addInvoiceItem(item)); updateTotal(); showInvoiceScreen("editor"); }; const getInvoiceLabel = (invoice) => { const customer = invoice.data.customer || {}; return customer.typSubjektu === "spolecnost" ? customer.nazevSpolecnosti || "Bez odběratele" : [customer.jmeno, customer.prijmeni].filter(Boolean).join(" ") || "Bez odběratele"; }; const renderInvoiceList = () => { if (!invoices.length) { invoiceList.innerHTML = "
Zatím nemáte uložené žádné faktury.
"; return; } invoiceList.innerHTML = invoices.map((invoice) => `