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) => `
`).join(""); }; const loadInvoices = async () => { const response = await apiRequest("/api/invoices"); invoices = response.invoices; renderInvoiceList(); }; const openNewInvoice = () => { clearInvoiceEditor(); showInvoiceScreen("editor"); }; const saveInvoice = async () => { try { const response = await apiRequest(currentInvoiceId ? `/api/invoices/${currentInvoiceId}` : "/api/invoices", { method: currentInvoiceId ? "PUT" : "POST", body: JSON.stringify({ data: readInvoiceData() }) }); const invoice = response.invoice; currentInvoiceId = invoice.id; invoiceNumberDisplay.textContent = invoice.invoiceNumber; invoiceSaveStatus.textContent = "Faktura uložena"; await loadInvoices(); showInvoiceScreen("list"); } catch (error) { invoiceSaveStatus.textContent = error.message; } }; invoiceListNavButton.addEventListener("click", async () => { await loadInvoices(); showInvoiceScreen("list"); }); newInvoiceButton.addEventListener("click", openNewInvoice); exportInvoicesButton.addEventListener("click", async () => { try { const response = await fetch("/api/invoices/export", { credentials: "same-origin" }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || "Export se nepodařil."); } const blob = await response.blob(); const downloadUrl = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = downloadUrl; link.download = "faktury.zip"; link.click(); URL.revokeObjectURL(downloadUrl); } catch (error) { window.alert(error.message); } }); saveInvoiceButton.addEventListener("click", saveInvoice); deleteAllInvoicesButton.addEventListener("click", async () => { if (!invoices.length || !window.confirm("Opravdu chcete smazat všechny faktury a začít číslovat znovu od 0001?")) { return; } try { await apiRequest("/api/invoices", { method: "DELETE" }); invoices = []; renderInvoiceList(); clearInvoiceEditor(); showInvoiceScreen("list"); } catch (error) { window.alert(error.message); } }); invoiceList.addEventListener("click", async (event) => { const openButton = event.target.closest("[data-invoice-id]"); const deleteButton = event.target.closest("[data-delete-invoice-id]"); if (deleteButton) { if (!window.confirm("Opravdu chcete tuto fakturu smazat?")) { return; } await apiRequest(`/api/invoices/${deleteButton.dataset.deleteInvoiceId}`, { method: "DELETE" }); await loadInvoices(); return; } if (openButton) { const invoice = invoices.find(({ id }) => String(id) === openButton.dataset.invoiceId); if (invoice) { loadInvoiceIntoEditor(invoice); } } }); const saveSupplier = async () => { try { const data = readProfileFields("dodavatel"); data.typSubjektu = supplierTypeSelect.value; await apiRequest("/api/supplier", { method: "PUT", body: JSON.stringify(data) }); supplierSaveStatus.textContent = "Uloženo na serveru"; } catch (error) { supplierSaveStatus.textContent = error.message; } }; const loadSupplier = async () => { const { supplier } = await apiRequest("/api/supplier"); if (supplier) { writeProfileFields("dodavatel", supplier); supplierTypeSelect.value = supplier.typSubjektu || (supplier.nazevSpolecnosti ? "spolecnost" : "osoba"); updateSupplierTypeFields(); supplierSaveStatus.textContent = "Uloženo na serveru"; return true; } supplierSaveStatus.textContent = "Zatím není uložený dodavatel"; return false; }; const renderClientOptions = (selectedId = clientSelect.value) => { clientSelect.innerHTML = ""; clients.forEach((client) => { const option = document.createElement("option"); option.value = client.id; option.textContent = getClientLabel(client); clientSelect.appendChild(option); }); clientSelect.value = selectedId; deleteClientButton.disabled = !clientSelect.value; }; const loadClients = async () => { const response = await apiRequest("/api/clients"); clients = response.clients; renderClientOptions(); }; const loadSelectedClient = () => { const client = clients.find(({ id }) => String(id) === clientSelect.value); if (client) { writeProfileFields("odberatel", client.data); clientTypeSelect.value = client.typSubjektu || (client.data.nazevSpolecnosti ? "spolecnost" : "osoba"); updateClientTypeFields(); } deleteClientButton.disabled = !client; }; saveSupplierButton.addEventListener("click", saveSupplier); supplierTypeSelect.addEventListener("change", updateSupplierTypeFields); loadSupplierButton.addEventListener("click", async () => { try { await loadSupplier(); supplierSaveStatus.textContent = "Dodavatel načten"; } catch (error) { supplierSaveStatus.textContent = error.message; } }); clientTypeSelect.addEventListener("change", updateClientTypeFields); clientSelect.addEventListener("change", loadSelectedClient); saveClientButton.addEventListener("click", async () => { const data = readProfileFields("odberatel"); data.typSubjektu = clientTypeSelect.value; const existingClient = clients.find((client) => String(client.id) === clientSelect.value); try { const response = await apiRequest(existingClient ? `/api/clients/${existingClient.id}` : "/api/clients", { method: existingClient ? "PUT" : "POST", body: JSON.stringify(data) }); const client = response.client; clients = existingClient ? clients.map((entry) => entry.id === client.id ? client : entry) : [...clients, client]; renderClientOptions(String(client.id)); } catch (error) { window.alert(error.message); } }); deleteClientButton.addEventListener("click", async () => { if (!clientSelect.value) { return; } try { await apiRequest(`/api/clients/${clientSelect.value}`, { method: "DELETE" }); clients = clients.filter(({ id }) => String(id) !== clientSelect.value); renderClientOptions(); } catch (error) { window.alert(error.message); } }); const showInvoiceApp = async () => { authScreen.hidden = true; document.getElementById("main-container").hidden = false; await Promise.all([loadSupplier(), loadClients(), loadInvoices(), loadPaymentPresets(), loadSignaturePresets(), loadNumberingSettings()]); showInvoiceScreen("list"); }; const loadNumberingSettings = async () => { try { const { settings } = await apiRequest("/api/settings"); invoiceStartingNumberInput.value = settings.invoiceStartingNumber; invoiceStartingNumberInput.placeholder = String(settings.invoiceStartingNumber); } catch { // Keep the placeholder default if settings cannot be loaded. } }; const showNumberingSettingsPanel = (visible) => { numberingSettingsPanel.hidden = !visible; if (!visible) { numberingSettingsStatus.textContent = ""; } }; const showPasswordPanel = (visible) => { passwordPanel.hidden = !visible; if (!visible) { passwordForm.querySelectorAll("input").forEach((input) => { input.value = ""; }); passwordStatus.textContent = ""; } }; const setAuthMode = (registrationMode) => { isRegistrationMode = registrationMode; authSubmitButton.textContent = registrationMode ? "Vytvořit účet" : "Přihlásit se"; authModeButton.textContent = registrationMode ? "Máte účet? Přihlásit se" : "Nemáte účet? Zaregistrovat se"; authPassword.autocomplete = registrationMode ? "new-password" : "current-password"; authStatus.textContent = ""; }; authModeButton.addEventListener("click", () => setAuthMode(!isRegistrationMode)); forgotPasswordButton.addEventListener("click", () => { authStatus.textContent = "Obnova hesla e-mailem zatím není nakonfigurovaná. Přihlaste se a použijte Změnit heslo, nebo kontaktujte správce aplikace."; }); authForm.addEventListener("submit", async (event) => { event.preventDefault(); authStatus.textContent = ""; authSubmitButton.disabled = true; try { const { user } = await apiRequest(isRegistrationMode ? "/api/auth/register" : "/api/auth/login", { method: "POST", body: JSON.stringify({ email: authEmail.value, password: authPassword.value }) }); signedInEmail.textContent = user.email; await showInvoiceApp(); } catch (error) { authStatus.textContent = error.message; } finally { authSubmitButton.disabled = false; } }); logoutButton.addEventListener("click", async () => { await apiRequest("/api/auth/logout", { method: "POST" }); window.location.reload(); }); passwordButton.addEventListener("click", () => showPasswordPanel(passwordPanel.hidden)); cancelPasswordButton.addEventListener("click", () => showPasswordPanel(false)); numberingSettingsButton.addEventListener("click", () => showNumberingSettingsPanel(numberingSettingsPanel.hidden)); cancelNumberingSettingsButton.addEventListener("click", () => showNumberingSettingsPanel(false)); saveNumberingSettingsButton.addEventListener("click", async () => { numberingSettingsStatus.textContent = ""; const invoiceStartingNumber = Number.parseInt(invoiceStartingNumberInput.value, 10) || 1; try { const { settings } = await apiRequest("/api/settings", { method: "PUT", body: JSON.stringify({ invoiceStartingNumber }) }); invoiceStartingNumberInput.value = settings.invoiceStartingNumber; numberingSettingsStatus.textContent = "Uloženo"; } catch (error) { numberingSettingsStatus.textContent = error.message; } }); savePasswordButton.addEventListener("click", async () => { passwordStatus.textContent = ""; try { await apiRequest("/api/auth/password", { method: "POST", body: JSON.stringify({ currentPassword: document.getElementById("currentPassword").value, newPassword: document.getElementById("newPassword").value }) }); passwordForm.querySelectorAll("input").forEach((input) => { input.value = ""; }); passwordStatus.textContent = "Heslo bylo změněno"; } catch (error) { passwordStatus.textContent = error.message; } }); (async () => { try { const { user } = await apiRequest("/api/auth/me"); signedInEmail.textContent = user.email; await showInvoiceApp(); } catch { authScreen.hidden = false; document.getElementById("main-container").hidden = true; } })(); const escapeHtml = (value) => String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); const getValue = (id) => { const field = document.getElementById(id); if (!field) { return ""; } return ("value" in field ? field.value : field.textContent).trim(); }; const formatCzechNumber = (value) => Number(value || 0).toLocaleString("cs-CZ", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const updateTotal = () => { const total = Array.from(itemsContainer.querySelectorAll(".invoice-item")) .reduce((sum, item) => { const quantity = Number(item.querySelector('[name$="[mnozstvi]"]').value) || 0; const unitPrice = Number(item.querySelector('[name$="[cenaZaMj]"]').value) || 0; return sum + quantity * unitPrice; }, 0); document.getElementById("total").textContent = `${formatCzechNumber(total)} Kč`; }; printInvoiceButton.addEventListener("click", async () => { try { const response = await fetch("/api/invoices/pdf", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: readInvoiceData(), invoiceNumber: invoiceNumberDisplay.textContent }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || "Generování faktury se nepodařilo."); } const blob = await response.blob(); const downloadUrl = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = downloadUrl; link.download = `${invoiceNumberDisplay.textContent}.pdf`; link.click(); URL.revokeObjectURL(downloadUrl); } catch (error) { window.alert(error.message); } }); const addInvoiceItem = (values = {}) => { itemIndex++; const item = document.createElement("div"); item.classList.add("invoice-item"); item.innerHTML = `
`; const inputs = item.querySelectorAll("input"); inputs[0].value = values.popis || ""; inputs[1].value = values.mnozstvi ?? ""; inputs[2].value = values.mernaJednotka || ""; inputs[3].value = values.cenaZaMj ?? ""; const deleteButton = item.querySelector(".delete-item-button"); deleteButton.addEventListener("click", () => { item.remove(); updateTotal(); }); itemsContainer.appendChild(item); updateTotal(); }; addItemButton.addEventListener("click", () => addInvoiceItem()); itemsContainer.addEventListener("input", updateTotal); updateClientTypeFields(); updateSupplierTypeFields();