diff --git "a/static/js/main.js" "b/static/js/main.js"
--- "a/static/js/main.js"
+++ "b/static/js/main.js"
@@ -1,984 +1,2072 @@
-const VAT_OPTIONS = [
- { value: "23", label: "23%" },
- { value: "8", label: "8%" },
- { value: "5", label: "5%" },
- { value: "0", label: "0% (ZW)" },
- { value: "ZW", label: "ZW - zwolnione" },
- { value: "NP", label: "NP - poza zakresem" },
-];
-
-const VAT_RATE_VALUES = {
- "23": 0.23,
- "8": 0.08,
- "5": 0.05,
- "0": 0,
- ZW: 0,
- NP: 0,
-};
-
-const setupSection = document.getElementById("setup-section");
-const loginSection = document.getElementById("login-section");
-const appSection = document.getElementById("app-section");
-
-const setupForm = document.getElementById("setup-form");
-const loginForm = document.getElementById("login-form");
-const invoiceForm = document.getElementById("invoice-form");
-const businessForm = document.getElementById("business-form");
-
-const setupFeedback = document.getElementById("setup-feedback");
-const loginFeedback = document.getElementById("login-feedback");
-const businessFeedback = document.getElementById("business-feedback");
-
-const businessDisplay = document.getElementById("business-display");
-const toggleBusinessFormButton = document.getElementById("toggle-business-form");
-const cancelBusinessUpdateButton = document.getElementById("cancel-business-update");
-
-const itemsBody = document.getElementById("items-body");
-const addItemButton = document.getElementById("add-item-button");
-
-const totalNetLabel = document.getElementById("total-net");
-const totalVatLabel = document.getElementById("total-vat");
-const totalGrossLabel = document.getElementById("total-gross");
-const rateSummaryContainer = document.getElementById("rate-summary");
-
-const exemptionNoteWrapper = document.getElementById("exemption-note-wrapper");
-const exemptionNoteInput = document.getElementById("exemption-note");
-
-const invoiceResult = document.getElementById("invoice-result");
-const invoiceOutput = document.getElementById("invoice-output");
-const downloadButton = document.getElementById("download-button");
-const logoutButton = document.getElementById("logout-button");
-
-let authToken = sessionStorage.getItem("invoiceAuthToken") || null;
-let currentBusiness = null;
-let lastInvoice = null;
-let pdfFontPromise = null;
-let pdfFontBase64 = null;
-
-function setState(state) {
- setupSection.classList.add("hidden");
- loginSection.classList.add("hidden");
- appSection.classList.add("hidden");
-
- if (state === "setup") {
- setupSection.classList.remove("hidden");
- } else if (state === "login") {
- loginSection.classList.remove("hidden");
- } else if (state === "app") {
- appSection.classList.remove("hidden");
- }
-}
-
-function clearFeedback(element) {
- element.textContent = "";
- element.classList.remove("error", "success");
-}
-
-function showFeedback(element, message, type = "error") {
- element.textContent = message;
- element.classList.remove("error", "success");
- if (type) {
- element.classList.add(type);
- }
-}
-
-function parseNumber(value) {
- if (typeof value === "number") {
- return Number.isFinite(value) ? value : 0;
- }
- if (!value) {
- return 0;
- }
- const normalized = value.toString().replace(",", ".");
- const parsed = Number.parseFloat(normalized);
- return Number.isFinite(parsed) ? parsed : 0;
-}
-
-function formatCurrency(value) {
- const number = parseNumber(value);
- return `${number.toFixed(2)} PLN`;
-}
-
-function vatLabelFromCode(code) {
- if (code === "ZW" || code === "0") {
- return "ZW";
- }
- if (code === "NP") {
- return "NP";
- }
- return `${code}%`;
-}
-
-function requiresExemption(code) {
- return code === "ZW" || code === "0";
-}
-
-async function apiRequest(path, { method = "GET", body, headers = {} } = {}, requireAuth = false) {
- const options = {
- method,
- headers: {
- "Content-Type": "application/json",
- ...headers,
- },
- };
-
- if (body !== undefined) {
- options.body = JSON.stringify(body);
- }
-
- if (requireAuth) {
- if (!authToken) {
- throw new Error("Brak tokenu autoryzacyjnego.");
- }
- options.headers.Authorization = `Bearer ${authToken}`;
- }
-
- const response = await fetch(path, options);
- const isJson = response.headers.get("content-type")?.includes("application/json");
- const data = isJson ? await response.json() : {};
-
- if (response.status === 401) {
- authToken = null;
- sessionStorage.removeItem("invoiceAuthToken");
- setState("login");
- throw new Error(data.error || "Sesja wygasla. Zaloguj sie ponownie.");
- }
-
- if (!response.ok) {
- throw new Error(data.error || "Wystapil blad podczas komunikacji z serwerem.");
- }
-
- return data;
-}
-
-function renderBusinessDisplay(business) {
- if (!business) {
- businessDisplay.textContent = "Brak zapisanych danych firmy.";
- return;
- }
-
- businessDisplay.innerHTML = `
-
${business.company_name}
- ${business.owner_name}
- ${business.address_line}
- ${business.postal_code} ${business.city}
- NIP: ${business.tax_id}
- Konto: ${business.bank_account}
- `;
-}
-
-function fillBusinessForm(business) {
- if (!business) {
- return;
- }
- businessForm.elements.company_name.value = business.company_name || "";
- businessForm.elements.owner_name.value = business.owner_name || "";
- businessForm.elements.address_line.value = business.address_line || "";
- businessForm.elements.postal_code.value = business.postal_code || "";
- businessForm.elements.city.value = business.city || "";
- businessForm.elements.tax_id.value = business.tax_id || "";
- businessForm.elements.bank_account.value = business.bank_account || "";
-}
-
-function vatSelectElement(initialValue = "23") {
- const select = document.createElement("select");
- select.className = "item-vat";
- VAT_OPTIONS.forEach((option) => {
- const element = document.createElement("option");
- element.value = option.value;
- element.textContent = option.label;
- select.appendChild(element);
- });
- select.value = VAT_OPTIONS.some((option) => option.value === initialValue) ? initialValue : "23";
- return select;
-}
-
-function createItemRow(initialValues = {}) {
- const row = document.createElement("tr");
-
- const nameCell = document.createElement("td");
- const nameInput = document.createElement("input");
- nameInput.type = "text";
- nameInput.className = "item-name";
- nameInput.placeholder = "Nazwa towaru lub uslugi";
- if (initialValues.name) {
- nameInput.value = initialValues.name;
- }
- nameCell.appendChild(nameInput);
-
- const quantityCell = document.createElement("td");
- const quantityInput = document.createElement("input");
- quantityInput.type = "number";
- quantityInput.className = "item-quantity";
- quantityInput.min = "0.01";
- quantityInput.step = "0.01";
- quantityInput.value = initialValues.quantity ?? "1";
- quantityCell.appendChild(quantityInput);
-
- const unitGrossCell = document.createElement("td");
- const unitGrossInput = document.createElement("input");
- unitGrossInput.type = "number";
- unitGrossInput.className = "item-gross";
- unitGrossInput.min = "0.01";
- unitGrossInput.step = "0.01";
- unitGrossInput.placeholder = "Brutto";
- if (initialValues.unit_price_gross) {
- unitGrossInput.value = initialValues.unit_price_gross;
- }
- unitGrossCell.appendChild(unitGrossInput);
-
- const vatCell = document.createElement("td");
- const vatSelect = vatSelectElement(initialValues.vat_code);
- vatCell.appendChild(vatSelect);
-
- const totalCell = document.createElement("td");
- totalCell.className = "item-total";
- totalCell.textContent = "0.00 PLN";
-
- const actionsCell = document.createElement("td");
- const removeButton = document.createElement("button");
- removeButton.type = "button";
- removeButton.className = "remove-item";
- removeButton.textContent = "Usun";
- actionsCell.appendChild(removeButton);
-
- row.appendChild(nameCell);
- row.appendChild(quantityCell);
- row.appendChild(unitGrossCell);
- row.appendChild(vatCell);
- row.appendChild(totalCell);
- row.appendChild(actionsCell);
-
- const handleChange = () => updateTotals();
- nameInput.addEventListener("input", handleChange);
- quantityInput.addEventListener("input", handleChange);
- unitGrossInput.addEventListener("input", handleChange);
- vatSelect.addEventListener("change", handleChange);
-
- removeButton.addEventListener("click", () => {
- if (itemsBody.children.length === 1) {
- nameInput.value = "";
- quantityInput.value = "1";
- unitGrossInput.value = "";
- vatSelect.value = "23";
- updateTotals();
- return;
- }
- row.remove();
- updateTotals();
- });
-
- itemsBody.appendChild(row);
- updateTotals();
-}
-
-function calculateRowTotals(row) {
- const name = row.querySelector(".item-name")?.value.trim() ?? "";
- const quantity = parseNumber(row.querySelector(".item-quantity")?.value);
- const unitGross = parseNumber(row.querySelector(".item-gross")?.value);
- const vatCode = row.querySelector(".item-vat")?.value ?? "23";
- const rate = VAT_RATE_VALUES[vatCode] ?? 0;
-
- const hasValues = name || quantity > 0 || unitGross > 0;
- if (!hasValues) {
- return {
- valid: false,
- vatCode,
- vatLabel: vatLabelFromCode(vatCode),
- requiresExemption: requiresExemption(vatCode),
- quantity,
- unitGross,
- unitNet: 0,
- netTotal: 0,
- vatAmount: 0,
- grossTotal: 0,
- };
- }
-
- if (quantity <= 0 || unitGross <= 0) {
- return {
- valid: false,
- vatCode,
- vatLabel: vatLabelFromCode(vatCode),
- requiresExemption: requiresExemption(vatCode),
- quantity,
- unitGross,
- unitNet: 0,
- netTotal: 0,
- vatAmount: 0,
- grossTotal: quantity * unitGross,
- };
- }
-
- const grossTotal = quantity * unitGross;
- const netTotal = rate > 0 ? grossTotal / (1 + rate) : grossTotal;
- const vatAmount = grossTotal - netTotal;
- const unitNet = netTotal / quantity;
-
- return {
- valid: true,
- vatCode,
- vatLabel: vatLabelFromCode(vatCode),
- requiresExemption: requiresExemption(vatCode),
- quantity,
- unitGross,
- unitNet,
- netTotal,
- vatAmount,
- grossTotal,
- };
-}
-
-function updateTotals() {
- let totalNet = 0;
- let totalVat = 0;
- let totalGross = 0;
- const summary = new Map();
- let exemptionNeeded = false;
-
- const rows = Array.from(itemsBody.querySelectorAll("tr"));
- rows.forEach((row) => {
- const totals = calculateRowTotals(row);
- if (totals.requiresExemption) {
- exemptionNeeded = true;
- }
- const totalCell = row.querySelector(".item-total");
- totalCell.textContent = formatCurrency(totals.grossTotal);
-
- if (!totals.valid) {
- return;
- }
-
- totalNet += totals.netTotal;
- totalVat += totals.vatAmount;
- totalGross += totals.grossTotal;
-
- const existing = summary.get(totals.vatLabel) || { net: 0, vat: 0, gross: 0 };
- existing.net += totals.netTotal;
- existing.vat += totals.vatAmount;
- existing.gross += totals.grossTotal;
- summary.set(totals.vatLabel, existing);
- });
-
- totalNetLabel.textContent = `Suma netto: ${totalNet.toFixed(2)} PLN`;
- totalVatLabel.textContent = `Kwota VAT: ${totalVat.toFixed(2)} PLN`;
- totalGrossLabel.textContent = `Suma brutto: ${totalGross.toFixed(2)} PLN`;
- renderRateSummary(summary);
-
- if (exemptionNeeded) {
- exemptionNoteWrapper.classList.remove("hidden");
- } else {
- exemptionNoteWrapper.classList.add("hidden");
- exemptionNoteInput.value = "";
- }
-}
-
-function renderRateSummary(summary) {
- if (!summary || summary.size === 0) {
- rateSummaryContainer.innerHTML = "";
- return;
- }
-
- const entries = Array.from(summary.entries()).sort(([a], [b]) => a.localeCompare(b));
- const markup = entries
- .map(
- ([label, totals]) =>
- `
- ${label}
- Netto: ${totals.net.toFixed(2)} PLN
- VAT: ${totals.vat.toFixed(2)} PLN
- Brutto: ${totals.gross.toFixed(2)} PLN
-
`
- )
- .join("");
- rateSummaryContainer.innerHTML = `Podsumowanie stawek
${markup}`;
-}
-
-function collectInvoicePayload() {
- const items = [];
- const rows = Array.from(itemsBody.querySelectorAll("tr"));
-
- rows.forEach((row) => {
- const name = row.querySelector(".item-name")?.value.trim() ?? "";
- const quantity = parseNumber(row.querySelector(".item-quantity")?.value);
- const unitGross = parseNumber(row.querySelector(".item-gross")?.value);
- const vatCode = row.querySelector(".item-vat")?.value ?? "23";
-
- const hasValues = name || quantity > 0 || unitGross > 0;
- if (!hasValues) {
- return;
- }
-
- if (!name) {
- throw new Error("Kazda pozycja musi miec nazwe.");
- }
- if (quantity <= 0) {
- throw new Error("Ilosc musi byc wieksza od zera.");
- }
- if (unitGross <= 0) {
- throw new Error("Cena brutto musi byc wieksza od zera.");
- }
-
- items.push({
- name,
- quantity: quantity.toFixed(2),
- unit_price_gross: unitGross.toFixed(2),
- vat_code: vatCode,
- });
- });
-
- if (items.length === 0) {
- throw new Error("Dodaj przynajmniej jedna pozycje.");
- }
-
- const saleDate = invoiceForm.elements.saleDate.value || null;
- const exemptionNote = exemptionNoteInput.value.trim();
- const requiresExemptionNote = items.some((item) => item.vat_code === "ZW" || item.vat_code === "0");
- if (requiresExemptionNote && !exemptionNote) {
- throw new Error("Podaj podstawe prawna zwolnienia dla pozycji rozliczanych jako ZW.");
- }
-
- const client = {
- name: (invoiceForm.elements.clientName.value || "").trim(),
- tax_id: (invoiceForm.elements.clientTaxId.value || "").trim(),
- address_line: (invoiceForm.elements.clientAddress.value || "").trim(),
- postal_code: (invoiceForm.elements.clientPostalCode.value || "").trim(),
- city: (invoiceForm.elements.clientCity.value || "").trim(),
- };
-
- return {
- sale_date: saleDate,
- client,
- items,
- exemption_note: exemptionNote,
- };
-}
-
-function renderInvoicePreview(invoice) {
- if (!invoice || !currentBusiness) {
- invoiceOutput.innerHTML = "Brak danych faktury.
";
- return;
- }
-
- const client = invoice.client || {};
- const hasClientData = client.name || client.address_line || client.postal_code || client.city || client.tax_id;
-
- const itemsRows = invoice.items
- .map(
- (item) => `
-
- | ${item.name} |
- ${parseNumber(item.quantity).toFixed(2)} |
- ${formatCurrency(item.unit_price_net)} |
- ${formatCurrency(item.net_total)} |
- ${item.vat_label} |
- ${formatCurrency(item.vat_amount)} |
- ${formatCurrency(item.gross_total)} |
-
`
- )
- .join("");
-
- const summaryRows = (invoice.summary || [])
- .map(
- (entry) =>
- `
- ${entry.vat_label}
- Netto: ${formatCurrency(entry.net_total)}
- VAT: ${formatCurrency(entry.vat_total)}
- Brutto: ${formatCurrency(entry.gross_total)}
-
`
- )
- .join("");
-
- invoiceOutput.innerHTML = `
-
- Numer: ${invoice.invoice_id}
- Data wystawienia: ${invoice.issued_at}
- Data sprzedazy: ${invoice.sale_date}
-
-
-
-
-
- | Nazwa |
- Ilość |
- Cena jedn. netto |
- Wartość netto (pozycja) |
- Stawka VAT |
- Kwota VAT (pozycja) |
- Wartość brutto |
-
-
- ${itemsRows}
-
-
-
Podsumowanie stawek
- ${summaryRows}
-
-
- Netto: ${formatCurrency(invoice.totals.net)}
- VAT: ${formatCurrency(invoice.totals.vat)}
- Brutto: ${formatCurrency(invoice.totals.gross)}
-
- ${
- invoice.exemption_note
- ? `Podstawa prawna zwolnienia: ${invoice.exemption_note}
`
- : ""
- }
- `;
-}
-
-function drawPartyBox(doc, title, lines, x, y, width) {
- const lineHeight = 5;
- const wrappedLines = lines.flatMap((line) => doc.splitTextToSize(line, width));
- const boxHeight = wrappedLines.length * lineHeight + 14;
-
- doc.roundedRect(x - 4, y - 8, width + 8, boxHeight, 2, 2);
- doc.setFontSize(11);
- doc.text(title, x, y);
- doc.setFontSize(10);
-
- let cursor = y + 5;
- wrappedLines.forEach((line) => {
- doc.text(line, x, cursor);
- cursor += lineHeight;
- });
-
- return y - 8 + boxHeight;
-}
-
-function arrayBufferToBase64(buffer) {
- const bytes = new Uint8Array(buffer);
- const chunkSize = 0x8000;
- let binary = "";
- for (let offset = 0; offset < bytes.length; offset += chunkSize) {
- const chunk = bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length));
- binary += String.fromCharCode.apply(null, chunk);
- }
- return btoa(binary);
-}
-
-const PDF_FONT_FILE = "Roboto-VariableFont_wdth,wght.ttf";
-const PDF_FONT_NAME = "RobotoPolish";
-
-async function ensurePdfFont() {
- if (pdfFontPromise) {
- return pdfFontPromise;
- }
-
- if (!window.jspdf || !window.jspdf.jsPDF) {
- throw new Error("Biblioteka jsPDF nie zostala zaladowana.");
- }
-
- const { jsPDF } = window.jspdf;
- const loadBase64 = async () => {
- if (typeof window !== "undefined" && window.PDF_FONT_BASE64) {
- return window.PDF_FONT_BASE64;
- }
- const response = await fetch(`/${encodeURIComponent(PDF_FONT_FILE)}`);
- if (!response.ok) {
- throw new Error(`Nie udalo sie pobrac czcionki Roboto (status ${response.status}).`);
- }
- const buffer = await response.arrayBuffer();
- return arrayBufferToBase64(buffer);
- };
-
- pdfFontPromise = loadBase64().then((data) => {
- pdfFontBase64 = data;
- return data;
- });
-
- return pdfFontPromise;
-}
-
-async function generatePdf(business, invoice) {
- if (!window.jspdf || !window.jspdf.jsPDF) {
- alert("Biblioteka jsPDF nie zostala zaladowana. Sprawdz polaczenie z internetem.");
- return;
- }
-
- let fontBase64;
- try {
- fontBase64 = await ensurePdfFont();
- } catch (error) {
- alert(error.message || "Nie udalo sie przygotowac czcionki do PDF.");
- return;
- }
-
- const { jsPDF } = window.jspdf;
- const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
- const marginX = 18;
- let cursorY = 20;
-
- if (!doc.getFontList()[PDF_FONT_NAME]) {
- const embeddedFont = pdfFontBase64 || fontBase64;
- doc.addFileToVFS(PDF_FONT_FILE, embeddedFont);
- doc.addFont(PDF_FONT_FILE, PDF_FONT_NAME, "normal");
- }
-
- doc.setFont(PDF_FONT_NAME, "normal");
- doc.setFontSize(16);
- doc.text(`Faktura ${invoice.invoice_id}`, marginX, cursorY);
- doc.setFontSize(10);
- doc.text(`Data wystawienia: ${invoice.issued_at}`, marginX, cursorY + 6);
- doc.text(`Data sprzedaży: ${invoice.sale_date}`, marginX, cursorY + 12);
-
- cursorY += 22;
- const columnWidth = 85;
- const sellerX = marginX + columnWidth + 12;
-
- const clientLines = invoice.client && (invoice.client.name || invoice.client.address_line || invoice.client.city || invoice.client.tax_id)
- ? [
- invoice.client.name || "---",
- invoice.client.address_line || "",
- `${(invoice.client.postal_code || "").trim()} ${(invoice.client.city || "").trim()}`.trim(),
- invoice.client.tax_id ? `NIP: ${invoice.client.tax_id}` : "",
- ].filter((line) => line && line.trim())
- : ["Brak danych nabywcy"];
-
- const sellerLines = [
- business.company_name,
- business.owner_name,
- business.address_line,
- `${business.postal_code} ${business.city}`.trim(),
- `NIP: ${business.tax_id}`,
- `Konto: ${business.bank_account}`,
- ];
-
- const buyerBottom = drawPartyBox(doc, "NABYWCA", clientLines, marginX, cursorY, columnWidth);
- const sellerBottom = drawPartyBox(doc, "SPRZEDAWCA", sellerLines, sellerX, cursorY, columnWidth);
- cursorY = Math.max(buyerBottom, sellerBottom) + 12;
-
- const tableColumns = [
- { key: "name", label: "Nazwa", width: 52 },
- { key: "quantity", label: "Ilość", width: 16 },
- { key: "unitNet", label: "Cena jedn. netto", width: 24 },
- { key: "netTotal", label: "Wartość netto", width: 24 },
- { key: "vatLabel", label: "Stawka VAT", width: 15 },
- { key: "vatAmount", label: "Kwota VAT", width: 22 },
- { key: "grossTotal", label: "Wartość brutto", width: 21 },
- ];
- const tableWidth = tableColumns.reduce((sum, col) => sum + col.width, 0);
- const lineHeight = 5;
- const headerLineHeight = 4.2;
- tableColumns.forEach((column) => {
- column.headerLines = doc.splitTextToSize(column.label, column.width - 4);
- });
- const headerHeight = Math.max(...tableColumns.map((column) => column.headerLines.length * headerLineHeight + 3));
-
- doc.setFillColor(241, 243, 247);
- doc.rect(marginX, cursorY, tableWidth, headerHeight, "F");
- doc.rect(marginX, cursorY, tableWidth, headerHeight);
- let offsetX = marginX;
- doc.setFontSize(10);
- tableColumns.forEach((column) => {
- doc.rect(offsetX, cursorY, column.width, headerHeight);
- column.headerLines.forEach((line, index) => {
- const textY = cursorY + 4 + index * headerLineHeight;
- doc.text((line || "").trim(), offsetX + 2, textY);
- });
- offsetX += column.width;
- });
- cursorY += headerHeight;
-
- invoice.items.forEach((item) => {
- const quantity = parseNumber(item.quantity).toFixed(2);
- const unitNet = formatCurrency(item.unit_price_net);
- const netTotal = formatCurrency(item.net_total);
- const vatAmount = formatCurrency(item.vat_amount);
- const grossTotal = formatCurrency(item.gross_total);
-
- const wrapText = (text, width) =>
- doc
- .splitTextToSize(text ?? "", width)
- .map((line) => line.trim());
-
- const columnData = tableColumns.map((column) => {
- switch (column.key) {
- case "name":
- return wrapText(item.name, column.width - 4);
- case "quantity":
- return [quantity];
- case "unitNet":
- return [unitNet];
- case "netTotal":
- return [netTotal];
- case "vatLabel":
- return [item.vat_label];
- case "vatAmount":
- return [vatAmount];
- case "grossTotal":
- return [grossTotal];
- default:
- return [""];
- }
- });
-
- const rowHeight = Math.max(...columnData.map((data) => data.length)) * lineHeight + 2;
- offsetX = marginX;
- tableColumns.forEach((column, index) => {
- doc.rect(offsetX, cursorY, column.width, rowHeight);
- const lines = columnData[index];
- lines.forEach((line, lineIndex) => {
- const textY = cursorY + (lineIndex + 1) * lineHeight;
- const content = (line || "").trim();
- doc.text(content, offsetX + 2, textY);
- });
- offsetX += column.width;
- });
-
- cursorY += rowHeight;
- });
-
- cursorY += 10;
- doc.setFontSize(11);
- doc.text("Podsumowanie stawek:", marginX, cursorY);
- cursorY += 6;
-
- const summaryEntries = Array.isArray(invoice.summary) ? invoice.summary : [];
- summaryEntries.forEach((entry) => {
- const summaryLine = `${entry.vat_label} – Netto: ${formatCurrency(entry.net_total)} / VAT: ${formatCurrency(entry.vat_total)} / Brutto: ${formatCurrency(entry.gross_total)}`;
- const wrapped = doc.splitTextToSize(summaryLine, 170);
- wrapped.forEach((line) => {
- doc.text((line || "").trim(), marginX, cursorY);
- cursorY += lineHeight;
- });
- });
-
- cursorY += 6;
- doc.setFontSize(12);
- doc.text(`Suma netto: ${formatCurrency(invoice.totals.net)}`, marginX, cursorY);
- doc.text(`Suma VAT: ${formatCurrency(invoice.totals.vat)}`, marginX, cursorY + 6);
- doc.text(`Suma brutto: ${formatCurrency(invoice.totals.gross)}`, marginX, cursorY + 12);
- cursorY += 20;
-
- if (invoice.exemption_note) {
- doc.setFontSize(10);
- const noteLines = doc.splitTextToSize(`Podstawa prawna zwolnienia: ${invoice.exemption_note}`, 170);
- doc.text(noteLines, marginX, cursorY);
- }
-
- doc.save(`${invoice.invoice_id}.pdf`);
-}
-
-async function loadBusinessData() {
- const data = await apiRequest("/api/business", {}, true);
- currentBusiness = data.business;
- renderBusinessDisplay(currentBusiness);
- fillBusinessForm(currentBusiness);
-}
-
-function resetInvoiceForm() {
- invoiceForm.reset();
- exemptionNoteInput.value = "";
- exemptionNoteWrapper.classList.add("hidden");
- itemsBody.innerHTML = "";
- createItemRow();
- const today = new Date().toISOString().slice(0, 10);
- invoiceForm.elements.saleDate.value = today;
-}
-
-async function bootstrapApp() {
- try {
- await loadBusinessData();
- setState("app");
- } catch (error) {
- console.error(error);
- authToken = null;
- sessionStorage.removeItem("invoiceAuthToken");
- showFeedback(loginFeedback, error.message || "Nie udalo sie pobrac danych firmy.");
- setState("login");
- }
-}
-
-async function initialize() {
- resetInvoiceForm();
- try {
- const status = await apiRequest("/api/status");
- if (!status.configured) {
- setState("setup");
- return;
- }
-
- if (authToken) {
- await bootstrapApp();
- } else {
- setState("login");
- }
- } catch (error) {
- console.error(error);
- setState("setup");
- showFeedback(setupFeedback, "Nie udalo sie nawiazac polaczenia z serwerem.");
- }
-}
-
-setupForm.addEventListener("submit", async (event) => {
- event.preventDefault();
- clearFeedback(setupFeedback);
-
- const formData = new FormData(setupForm);
- const password = formData.get("password")?.toString() ?? "";
- const confirmPassword = formData.get("confirm_password")?.toString() ?? "";
-
- if (password !== confirmPassword) {
- showFeedback(setupFeedback, "Hasla musza byc identyczne.");
- return;
- }
-
- if (password.trim().length < 4) {
- showFeedback(setupFeedback, "Haslo musi miec co najmniej 4 znaki.");
- return;
- }
-
- const payload = {
- company_name: formData.get("company_name")?.toString().trim(),
- owner_name: formData.get("owner_name")?.toString().trim(),
- address_line: formData.get("address_line")?.toString().trim(),
- postal_code: formData.get("postal_code")?.toString().trim(),
- city: formData.get("city")?.toString().trim(),
- tax_id: formData.get("tax_id")?.toString().trim(),
- bank_account: formData.get("bank_account")?.toString().trim(),
- password,
- };
-
- try {
- await apiRequest("/api/setup", { method: "POST", body: payload });
- showFeedback(setupFeedback, "Dane zapisane. Mozesz sie zalogowac.", "success");
- setTimeout(() => {
- setState("login");
- clearFeedback(setupFeedback);
- setupForm.reset();
- }, 1500);
- } catch (error) {
- showFeedback(setupFeedback, error.message || "Nie udalo sie zapisac danych.");
- }
-});
-
-loginForm.addEventListener("submit", async (event) => {
- event.preventDefault();
- clearFeedback(loginFeedback);
-
- const password = loginForm.elements.password.value;
- if (!password) {
- showFeedback(loginFeedback, "Podaj haslo.");
- return;
- }
-
- try {
- const response = await apiRequest("/api/login", { method: "POST", body: { password } });
- authToken = response.token;
- sessionStorage.setItem("invoiceAuthToken", authToken);
- loginForm.reset();
- await bootstrapApp();
- } catch (error) {
- showFeedback(loginFeedback, error.message || "Logowanie nie powiodlo sie.");
- }
-});
-
-toggleBusinessFormButton.addEventListener("click", () => {
- const isHidden = businessForm.classList.contains("hidden");
- if (isHidden) {
- fillBusinessForm(currentBusiness);
- businessForm.classList.remove("hidden");
- toggleBusinessFormButton.textContent = "Ukryj formularz";
- } else {
- businessForm.classList.add("hidden");
- toggleBusinessFormButton.textContent = "Edytuj dane";
- clearFeedback(businessFeedback);
- }
-});
-
-cancelBusinessUpdateButton.addEventListener("click", () => {
- businessForm.classList.add("hidden");
- toggleBusinessFormButton.textContent = "Edytuj dane";
- clearFeedback(businessFeedback);
-});
-
-businessForm.addEventListener("submit", async (event) => {
- event.preventDefault();
- clearFeedback(businessFeedback);
-
- const formData = new FormData(businessForm);
- const payload = {
- company_name: formData.get("company_name")?.toString().trim(),
- owner_name: formData.get("owner_name")?.toString().trim(),
- address_line: formData.get("address_line")?.toString().trim(),
- postal_code: formData.get("postal_code")?.toString().trim(),
- city: formData.get("city")?.toString().trim(),
- tax_id: formData.get("tax_id")?.toString().trim(),
- bank_account: formData.get("bank_account")?.toString().trim(),
- };
-
- try {
- const data = await apiRequest("/api/business", { method: "PUT", body: payload }, true);
- currentBusiness = data.business;
- renderBusinessDisplay(currentBusiness);
- showFeedback(businessFeedback, "Dane sprzedawcy zaktualizowane.", "success");
- setTimeout(() => clearFeedback(businessFeedback), 2000);
- } catch (error) {
- showFeedback(businessFeedback, error.message || "Nie udalo sie zaktualizowac danych.");
- }
-});
-
-invoiceForm.addEventListener("submit", async (event) => {
- event.preventDefault();
- try {
- const payload = collectInvoicePayload();
- const response = await apiRequest("/api/invoices", { method: "POST", body: payload }, true);
- lastInvoice = response.invoice;
- renderInvoicePreview(lastInvoice);
- invoiceResult.classList.remove("hidden");
- resetInvoiceForm();
- } catch (error) {
- alert(error.message || "Nie udalo sie wygenerowac faktury.");
- }
-});
-
-addItemButton.addEventListener("click", () => {
- createItemRow();
-});
-
-downloadButton.addEventListener("click", async () => {
- if (!lastInvoice || !currentBusiness) {
- alert("Brak faktury do pobrania. Wygeneruj ja najpierw.");
- return;
- }
- await generatePdf(currentBusiness, lastInvoice);
-});
-
-logoutButton.addEventListener("click", () => {
- authToken = null;
- sessionStorage.removeItem("invoiceAuthToken");
- lastInvoice = null;
- currentBusiness = null;
- invoiceResult.classList.add("hidden");
- setState("login");
-});
-
-initialize().catch((error) => {
- console.error(error);
- showFeedback(setupFeedback, "Nie udalo sie uruchomic aplikacji.");
-});
+const VAT_OPTIONS = [
+ { value: "23", label: "23%" },
+ { value: "8", label: "8%" },
+ { value: "5", label: "5%" },
+ { value: "0", label: "0% (ZW)" },
+ { value: "ZW", label: "ZW - zwolnione" },
+ { value: "NP", label: "NP - poza zakresem" },
+];
+
+const VAT_RATE_VALUES = {
+ "23": 0.23,
+ "8": 0.08,
+ "5": 0.05,
+ "0": 0,
+ ZW: 0,
+ NP: 0,
+};
+
+const UNIT_OPTIONS = [
+ { value: "szt.", label: "szt." },
+ { value: "godz.", label: "godz." },
+];
+
+const DEFAULT_UNIT = UNIT_OPTIONS[0].value;
+
+const EXEMPTION_REASONS = [
+ {
+ value: "art_43_1_19",
+ label: "Art. 43 ust. 1 pkt 19 ustawy o VAT - usługi medyczne",
+ note: "Art. 43 ust. 1 pkt 19 ustawy o VAT - usługi w zakresie opieki medycznej.",
+ },
+ {
+ value: "art_43_1_18",
+ label: "Art. 43 ust. 1 pkt 18 ustawy o VAT - usługi edukacyjne",
+ note: "Art. 43 ust. 1 pkt 18 ustawy o VAT - usługi edukacyjne w formach przewidzianych w przepisach.",
+ },
+ {
+ value: "art_43_1_37",
+ label: "Art. 43 ust. 1 pkt 37 ustawy o VAT - usługi finansowe",
+ note: "Art. 43 ust. 1 pkt 37 ustawy o VAT - usługi finansowe i pośrednictwa finansowego.",
+ },
+ {
+ value: "art_113",
+ label: "Art. 113 ust. 1 i 9 ustawy o VAT - zwolnienie podmiotowe",
+ note: "Art. 113 ust. 1 i 9 ustawy o VAT - zwolnienie podmiotowe do 200 000 PLN obrotu.",
+ },
+ {
+ value: "par_3_ust_1_pkt_1",
+ label: "Par. 3 ust. 1 pkt 1 rozporządzenia MF z 20.12.2013 r.",
+ note: "Par. 3 ust. 1 pkt 1 rozporządzenia MF z 20.12.2013 r. - dostawa towarów używanych.",
+ },
+ {
+ value: "custom",
+ label: "Inne (wpisz własny opis)",
+ note: "",
+ },
+];
+
+const EXEMPTION_REASON_LOOKUP = new Map(EXEMPTION_REASONS.map((reason) => [reason.value, reason]));
+
+const authSection = document.getElementById("auth-section");
+const appSection = document.getElementById("app-section");
+
+const registerForm = document.getElementById("register-form");
+const loginForm = document.getElementById("login-form");
+const loginSubmitButton = loginForm ? loginForm.querySelector('button[type="submit"]') : null;
+const loginSubmitButtonDefaultText = loginSubmitButton && loginSubmitButton.textContent ? loginSubmitButton.textContent.trim() || "Zaloguj" : "Zaloguj";
+const invoiceForm = document.getElementById("invoice-form");
+const businessForm = document.getElementById("business-form");
+
+const registerFeedback = document.getElementById("register-feedback");
+const loginFeedback = document.getElementById("login-feedback");
+const businessFeedback = document.getElementById("business-feedback");
+const logoFeedback = document.getElementById("logo-feedback");
+const registerSection = document.getElementById("register-section");
+const showRegisterButton = document.getElementById("show-register-button");
+const backToLoginButton = document.getElementById("back-to-login");
+const cancelRegisterButton = document.getElementById("cancel-register");
+
+const businessDisplay = document.getElementById("business-display");
+const toggleBusinessFormButton = document.getElementById("toggle-business-form");
+const cancelBusinessUpdateButton = document.getElementById("cancel-business-update");
+const currentLoginLabel = document.getElementById("current-login-label");
+
+const itemsBody = document.getElementById("items-body");
+const addItemButton = document.getElementById("add-item-button");
+
+const totalNetLabel = document.getElementById("total-net");
+const totalVatLabel = document.getElementById("total-vat");
+const totalGrossLabel = document.getElementById("total-gross");
+const rateSummaryContainer = document.getElementById("rate-summary");
+
+const exemptionNoteWrapper = document.getElementById("exemption-note-wrapper");
+const exemptionReasonSelect = document.getElementById("exemption-reason");
+const exemptionNoteInput = document.getElementById("exemption-note");
+
+const invoiceResult = document.getElementById("invoice-result");
+const invoiceOutput = document.getElementById("invoice-output");
+const downloadButton = document.getElementById("download-button");
+const logoutButton = document.getElementById("logout-button");
+const cancelEditInvoiceButton = document.getElementById("cancel-edit-invoice");
+const saveInvoiceButton = document.getElementById("save-invoice-button");
+
+const invoiceBuilderSection = document.getElementById("invoice-builder-section");
+const dashboardSection = document.getElementById("dashboard-section");
+const appNavButtons = Array.from(document.querySelectorAll(".app-nav-button"));
+
+const invoicesTableBody = document.getElementById("invoices-table-body");
+const invoicesEmpty = document.getElementById("invoices-empty");
+const dashboardFeedback = document.getElementById("dashboard-feedback");
+
+const filterStartDate = document.getElementById("filter-start-date");
+const filterEndDate = document.getElementById("filter-end-date");
+const clearFiltersButton = document.getElementById("clear-filters");
+
+const summaryMonthCount = document.getElementById("summary-month-count");
+const summaryMonthAmount = document.getElementById("summary-month-amount");
+const summaryQuarterCount = document.getElementById("summary-quarter-count");
+const summaryQuarterAmount = document.getElementById("summary-quarter-amount");
+const summaryYearCount = document.getElementById("summary-year-count");
+const summaryYearAmount = document.getElementById("summary-year-amount");
+
+const logoInput = document.getElementById("logo-input");
+const logoPreview = document.getElementById("logo-preview");
+const logoPreviewImage = document.getElementById("logo-preview-image");
+const removeLogoButton = document.getElementById("remove-logo-button");
+const legacyLoginHint = document.getElementById("legacy-login-hint");
+const invoicesChartCanvas = document.getElementById("invoices-chart");
+
+let authToken = sessionStorage.getItem("invoiceAuthToken") || null;
+let currentLogin = sessionStorage.getItem("invoiceLogin") || "";
+let currentBusiness = null;
+let currentLogo = null;
+let lastInvoice = null;
+let invoicesCache = [];
+let editingInvoiceId = null;
+let activeView = "invoice-builder";
+let invoicesChart = null;
+let maxLogoSize = 512 * 1024;
+let pdfFontPromise = null;
+let pdfFontBase64 = null;
+let customExemptionNote = "";
+
+function setVisibility(element, visible) {
+ if (!element) {
+ return;
+ }
+ if (visible) {
+ element.classList.remove("hidden");
+ element.style.removeProperty("display");
+ } else {
+ element.classList.add("hidden");
+ element.style.display = "none";
+ }
+}
+
+function setAppState(state) {
+ if (state === "app") {
+ setVisibility(authSection, false);
+ setVisibility(registerSection, false);
+ setVisibility(appSection, true);
+ } else {
+ setVisibility(authSection, true);
+ setVisibility(registerSection, false);
+ setVisibility(appSection, false);
+ }
+}
+
+function openRegisterPanel() {
+ if (!registerSection) {
+ return;
+ }
+ setVisibility(authSection, false);
+ setVisibility(registerSection, true);
+ setVisibility(appSection, false);
+ clearFeedback(registerFeedback);
+ clearFeedback(loginFeedback);
+ if (registerForm) {
+ const emailInput = registerForm.elements.email;
+ if (emailInput) {
+ emailInput.focus();
+ }
+ }
+ const scrollTarget = registerSection.querySelector(".register-card") || registerSection;
+ const scrollIntoView = () => scrollTarget.scrollIntoView({ behavior: "smooth", block: "start" });
+ if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
+ window.requestAnimationFrame(scrollIntoView);
+ } else if (typeof requestAnimationFrame === "function") {
+ requestAnimationFrame(scrollIntoView);
+ } else {
+ scrollIntoView();
+ }
+}
+
+function closeRegisterPanel({ resetForm = true, focusTrigger = false } = {}) {
+ if (!registerSection) {
+ return;
+ }
+ setVisibility(registerSection, false);
+ setVisibility(authSection, true);
+ setVisibility(appSection, false);
+ clearFeedback(registerFeedback);
+ clearFeedback(loginFeedback);
+ if (resetForm && registerForm) {
+ registerForm.reset();
+ }
+ if (focusTrigger) {
+ if (showRegisterButton) {
+ showRegisterButton.focus();
+ }
+ const scrollTarget = authSection ? authSection.querySelector(".login-card") || authSection : null;
+ if (scrollTarget) {
+ const scrollToLogin = () => scrollTarget.scrollIntoView({ behavior: "smooth", block: "start" });
+ if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
+ window.requestAnimationFrame(scrollToLogin);
+ } else if (typeof requestAnimationFrame === "function") {
+ requestAnimationFrame(scrollToLogin);
+ } else {
+ scrollToLogin();
+ }
+ }
+ }
+}
+
+function clearFeedback(element) {
+ if (!element) {
+ return;
+ }
+ element.textContent = "";
+ element.classList.remove("error", "success");
+}
+
+function showFeedback(element, message, type = "error") {
+ if (!element) {
+ return;
+ }
+ element.textContent = message;
+ element.classList.remove("error", "success");
+ if (type) {
+ element.classList.add(type);
+ }
+}
+
+function parseNumber(value) {
+ if (typeof value === "number") {
+ return Number.isFinite(value) ? value : 0;
+ }
+ if (!value) {
+ return 0;
+ }
+ const normalized = value.toString().replace(",", ".");
+ const parsed = Number.parseFloat(normalized);
+ return Number.isFinite(parsed) ? parsed : 0;
+}
+
+function parseIntegerString(value) {
+ if (value === null || value === undefined) {
+ return Number.NaN;
+ }
+ const normalized = value.toString().trim();
+ if (!normalized) {
+ return 0;
+ }
+ if (!/^\d+$/.test(normalized)) {
+ return Number.NaN;
+ }
+ const parsed = Number.parseInt(normalized, 10);
+ return Number.isNaN(parsed) ? Number.NaN : parsed;
+}
+
+function formatQuantity(value) {
+ const parsed = parseIntegerString(value);
+ if (Number.isNaN(parsed)) {
+ return "0";
+ }
+ return parsed.toString();
+}
+
+function formatCurrency(value) {
+ const number = parseNumber(value);
+ return `${number.toFixed(2)} PLN`;
+}
+
+function vatLabelFromCode(code) {
+ if (code === "ZW" || code === "0") {
+ return "ZW";
+ }
+ if (code === "NP") {
+ return "NP";
+ }
+ return `${code}%`;
+}
+
+function requiresExemption(code) {
+ return code === "ZW" || code === "0";
+}
+
+function populateExemptionReasons() {
+ if (!exemptionReasonSelect || exemptionReasonSelect.dataset.initialized === "true") {
+ return;
+ }
+ const existingValues = new Set(Array.from(exemptionReasonSelect.options).map((option) => option.value));
+ EXEMPTION_REASONS.forEach((reason) => {
+ if (existingValues.has(reason.value)) {
+ return;
+ }
+ const option = document.createElement("option");
+ option.value = reason.value;
+ option.textContent = reason.label;
+ exemptionReasonSelect.appendChild(option);
+ });
+ exemptionReasonSelect.dataset.initialized = "true";
+}
+
+function applyExemptionReasonSelection({ preserveCustom = false } = {}) {
+ if (!exemptionReasonSelect || !exemptionNoteInput) {
+ return;
+ }
+ const selectedValue = exemptionReasonSelect.value;
+ const selectedReason = EXEMPTION_REASON_LOOKUP.get(selectedValue);
+
+ // Ukryj pole "Podstawa prawna zwolnienia" jeśli nie wybrano opcji "Inne..."
+ const exemptionNoteLabel = document.getElementById("exemption-note-label");
+ if (exemptionNoteLabel) {
+ if (selectedValue === "custom") {
+ exemptionNoteLabel.style.display = "block";
+ exemptionNoteInput.style.display = "block";
+ } else {
+ exemptionNoteLabel.style.display = "none";
+ exemptionNoteInput.style.display = "none";
+ }
+ }
+
+ if (!selectedReason) {
+ if (!preserveCustom) {
+ exemptionNoteInput.readOnly = false;
+ exemptionNoteInput.value = "";
+ }
+ return;
+ }
+ if (selectedValue === "custom") {
+ exemptionNoteInput.readOnly = false;
+ if (!preserveCustom) {
+ exemptionNoteInput.value = customExemptionNote;
+ }
+ return;
+ }
+ exemptionNoteInput.readOnly = true;
+ exemptionNoteInput.value = selectedReason.note;
+}
+
+function findExemptionReasonByNote(note) {
+ if (!note) {
+ return null;
+ }
+ const normalized = note.trim().toLowerCase();
+ return (
+ EXEMPTION_REASONS.find(
+ (reason) =>
+ reason.value !== "custom" && reason.note && reason.note.trim().toLowerCase() === normalized
+ ) || null
+ );
+}
+
+function syncExemptionControlsWithNote(note) {
+ if (!exemptionNoteInput) {
+ return;
+ }
+ const trimmed = (note || "").trim();
+ exemptionNoteInput.readOnly = false;
+ if (!exemptionReasonSelect) {
+ exemptionNoteInput.value = trimmed;
+ return;
+ }
+ if (!trimmed) {
+ customExemptionNote = "";
+ exemptionReasonSelect.value = "";
+ exemptionNoteInput.value = "";
+ return;
+ }
+ const matchedReason = findExemptionReasonByNote(trimmed);
+ if (matchedReason && matchedReason.value !== "custom") {
+ exemptionReasonSelect.value = matchedReason.value;
+ applyExemptionReasonSelection({ preserveCustom: true });
+ } else {
+ customExemptionNote = trimmed;
+ exemptionReasonSelect.value = "custom";
+ exemptionNoteInput.readOnly = false;
+ exemptionNoteInput.value = trimmed;
+ }
+}
+
+function updateExemptionVisibility(exemptionNeeded) {
+ if (!exemptionNoteWrapper || !exemptionNoteInput) {
+ return;
+ }
+ if (exemptionNeeded) {
+ populateExemptionReasons();
+ setVisibility(exemptionNoteWrapper, true);
+ applyExemptionReasonSelection({ preserveCustom: true });
+ return;
+ }
+ setVisibility(exemptionNoteWrapper, false);
+ if (exemptionReasonSelect) {
+ exemptionReasonSelect.value = "";
+ }
+ customExemptionNote = "";
+ exemptionNoteInput.readOnly = false;
+ exemptionNoteInput.value = "";
+}
+
+function formatInvoicesCount(count) {
+ const value = Number.parseInt(count, 10) || 0;
+ const absolute = Math.abs(value);
+ const mod10 = absolute % 10;
+ const mod100 = absolute % 100;
+ let suffix = "faktur";
+ if (mod10 === 1 && mod100 !== 11) {
+ suffix = "faktura";
+ } else if ([2, 3, 4].includes(mod10) && ![12, 13, 14].includes(mod100)) {
+ suffix = "faktury";
+ }
+ return `${value} ${suffix}`;
+}
+
+function parseInvoiceIssuedAt(invoice) {
+ if (!invoice || !invoice.issued_at) {
+ return null;
+ }
+ const normalized = invoice.issued_at.replace(" ", "T");
+ const parsed = new Date(normalized);
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
+}
+
+function parseDateInput(value) {
+ if (!value) {
+ return null;
+ }
+ const parts = value.split("-").map((part) => Number.parseInt(part, 10));
+ if (parts.length !== 3 || parts.some(Number.isNaN)) {
+ return null;
+ }
+ return new Date(parts[0], parts[1] - 1, parts[2]);
+}
+
+function setActiveView(view) {
+ activeView = view === "dashboard" ? "dashboard" : "invoice-builder";
+ setVisibility(invoiceBuilderSection, activeView === "invoice-builder");
+ setVisibility(dashboardSection, activeView === "dashboard");
+ const showDashboard = activeView === "dashboard";
+ appNavButtons.forEach((button) => {
+ button.classList.toggle("active", button.dataset.view === activeView);
+ });
+ if (showDashboard) {
+ applyInvoiceFilters();
+ }
+}
+
+function updateLoginLabel() {
+ if (!currentLogin) {
+ currentLoginLabel.textContent = "";
+ return;
+ }
+ currentLoginLabel.textContent = `Zalogowany jako ${currentLogin}`;
+}
+
+function updateLogoPreview() {
+ if (currentLogo && currentLogo.data && currentLogo.mime_type) {
+ const dataUrl = currentLogo.data_url || `data:${currentLogo.mime_type};base64,${currentLogo.data}`;
+ logoPreviewImage.src = dataUrl;
+ logoPreview.classList.remove("hidden");
+ removeLogoButton.classList.remove("hidden");
+ } else {
+ logoPreviewImage.removeAttribute("src");
+ logoPreview.classList.add("hidden");
+ removeLogoButton.classList.add("hidden");
+ }
+}
+
+function renderInvoicesTable(invoices) {
+ invoicesTableBody.innerHTML = "";
+ if (!Array.isArray(invoices) || invoices.length === 0) {
+ invoicesEmpty.classList.remove("hidden");
+ return;
+ }
+
+ invoicesEmpty.classList.add("hidden");
+ invoices.forEach((invoice) => {
+ const row = document.createElement("tr");
+
+ const numberCell = document.createElement("td");
+ numberCell.textContent = invoice.invoice_id || "---";
+ row.appendChild(numberCell);
+
+ const issuedCell = document.createElement("td");
+ issuedCell.textContent = invoice.issued_at || "-";
+ row.appendChild(issuedCell);
+
+ const clientCell = document.createElement("td");
+ const clientName = invoice.client?.name || "";
+ const clientCity = invoice.client?.city || "";
+ clientCell.textContent = clientName ? `${clientName}${clientCity ? ` (${clientCity})` : ""}` : "-";
+ row.appendChild(clientCell);
+
+ const grossCell = document.createElement("td");
+ grossCell.textContent = formatCurrency(invoice.totals?.gross ?? 0);
+ row.appendChild(grossCell);
+
+ const actionsCell = document.createElement("td");
+ const actionsWrapper = document.createElement("div");
+ actionsWrapper.className = "table-actions";
+
+ const editButton = document.createElement("button");
+ editButton.type = "button";
+ editButton.textContent = "Edytuj";
+ editButton.addEventListener("click", () => {
+ startInvoiceEdit(invoice.invoice_id);
+ });
+
+ const deleteButton = document.createElement("button");
+ deleteButton.type = "button";
+ deleteButton.className = "button secondary";
+ deleteButton.textContent = "Usuń";
+ deleteButton.addEventListener("click", async () => {
+ clearFeedback(dashboardFeedback);
+ const shouldDelete = window.confirm(`Usuńac fakturę ${invoice.invoice_id}?`);
+ if (!shouldDelete) {
+ return;
+ }
+ await deleteInvoice(invoice.invoice_id);
+ });
+
+ actionsWrapper.appendChild(editButton);
+ actionsWrapper.appendChild(deleteButton);
+ actionsCell.appendChild(actionsWrapper);
+ row.appendChild(actionsCell);
+
+ invoicesTableBody.appendChild(row);
+ });
+}
+
+function applyInvoiceFilters() {
+ if (!Array.isArray(invoicesCache)) {
+ renderInvoicesTable([]);
+ return;
+ }
+
+ let filtered = invoicesCache.slice();
+ const startDate = parseDateInput(filterStartDate?.value);
+ const endDate = parseDateInput(filterEndDate?.value);
+
+ if (startDate) {
+ const startTime = startDate.getTime();
+ filtered = filtered.filter((invoice) => {
+ const issued = parseInvoiceIssuedAt(invoice);
+ return !issued || issued.getTime() >= startTime;
+ });
+ }
+
+ if (endDate) {
+ const endBoundary = new Date(endDate);
+ endBoundary.setHours(23, 59, 59, 999);
+ const endTime = endBoundary.getTime();
+ filtered = filtered.filter((invoice) => {
+ const issued = parseInvoiceIssuedAt(invoice);
+ return !issued || issued.getTime() <= endTime;
+ });
+ }
+
+ filtered.sort((a, b) => (b.issued_at || "").localeCompare(a.issued_at || ""));
+ renderInvoicesTable(filtered);
+}
+
+async function refreshInvoices() {
+ if (!authToken) {
+ invoicesCache = [];
+ renderInvoicesTable([]);
+ return;
+ }
+ clearFeedback(dashboardFeedback);
+ try {
+ const data = await apiRequest("/api/invoices", {}, true);
+ invoicesCache = Array.isArray(data.invoices) ? data.invoices.slice() : [];
+ invoicesCache.sort((a, b) => (b.issued_at || "").localeCompare(a.issued_at || ""));
+ applyInvoiceFilters();
+ } catch (error) {
+ console.error(error);
+ showFeedback(dashboardFeedback, error.message || "Nie udało się pobrać faktur.");
+ }
+}
+
+function updateSummaryCards(summary) {
+ const monthSummary = summary?.last_month || { count: 0, gross_total: 0 };
+ const quarterSummary = summary?.quarter || { count: 0, gross_total: 0 };
+ const yearSummary = summary?.year || { count: 0, gross_total: 0 };
+
+ summaryMonthCount.textContent = formatInvoicesCount(monthSummary.count);
+ summaryQuarterCount.textContent = formatInvoicesCount(quarterSummary.count);
+ summaryYearCount.textContent = formatInvoicesCount(yearSummary.count);
+
+ summaryMonthAmount.textContent = formatCurrency(monthSummary.gross_total ?? 0);
+ summaryQuarterAmount.textContent = formatCurrency(quarterSummary.gross_total ?? 0);
+ summaryYearAmount.textContent = formatCurrency(yearSummary.gross_total ?? 0);
+}
+
+function updateSummaryChart(summary) {
+ if (!invoicesChartCanvas || typeof window.Chart === "undefined") {
+ return;
+ }
+
+ const labels = ["Ostatnie 30 dni", "Bieżący kwartał", "Bieżący rok"];
+ const counts = [
+ Number.parseInt(summary?.last_month?.count ?? 0, 10) || 0,
+ Number.parseInt(summary?.quarter?.count ?? 0, 10) || 0,
+ Number.parseInt(summary?.year?.count ?? 0, 10) || 0,
+ ];
+ const amounts = [
+ parseNumber(summary?.last_month?.gross_total ?? 0),
+ parseNumber(summary?.quarter?.gross_total ?? 0),
+ parseNumber(summary?.year?.gross_total ?? 0),
+ ];
+
+ const chartData = {
+ labels,
+ datasets: [
+ {
+ label: "Liczba faktur",
+ data: counts,
+ backgroundColor: "rgba(26, 115, 232, 0.65)",
+ yAxisID: "count",
+ borderRadius: 6,
+ },
+ {
+ label: "Suma brutto (PLN)",
+ data: amounts,
+ type: "line",
+ fill: false,
+ borderColor: "rgba(26, 115, 232, 0.65)",
+ backgroundColor: "rgba(26, 115, 232, 0.35)",
+ tension: 0.3,
+ yAxisID: "amount",
+ },
+ ],
+ };
+
+ const options = {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ count: {
+ beginAtZero: true,
+ position: "left",
+ ticks: {
+ precision: 0,
+ stepSize: 1,
+ },
+ },
+ amount: {
+ beginAtZero: true,
+ position: "right",
+ grid: {
+ drawOnChartArea: false,
+ },
+ ticks: {
+ callback: (value) => `${new Intl.NumberFormat("pl-PL", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value)} PLN`,
+ },
+ },
+ },
+ plugins: {
+ legend: {
+ position: "bottom",
+ },
+ tooltip: {
+ callbacks: {
+ label(context) {
+ if (context.dataset.yAxisID === "amount") {
+ return `${context.dataset.label}: ${new Intl.NumberFormat("pl-PL", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(context.parsed.y)} PLN`;
+ }
+ return `${context.dataset.label}: ${context.parsed.y}`;
+ },
+ },
+ },
+ },
+ };
+
+ if (!invoicesChart) {
+ invoicesChart = new window.Chart(invoicesChartCanvas, {
+ type: "bar",
+ data: chartData,
+ options,
+ });
+ } else {
+ invoicesChart.data = chartData;
+ invoicesChart.options = options;
+ invoicesChart.update();
+ }
+}
+
+async function refreshSummary() {
+ if (!authToken) {
+ updateSummaryCards({});
+ updateSummaryChart({});
+ return;
+ }
+ clearFeedback(dashboardFeedback);
+ try {
+ const data = await apiRequest("/api/invoices/summary", {}, true);
+ updateSummaryCards(data.summary);
+ updateSummaryChart(data.summary);
+ } catch (error) {
+ console.error(error);
+ showFeedback(dashboardFeedback, error.message || "Nie udało się pobrać podsumowania.");
+ }
+}
+
+async function deleteInvoice(invoiceId) {
+ if (!invoiceId) {
+ return;
+ }
+ try {
+ await apiRequest(`/api/invoices/${encodeURIComponent(invoiceId)}`, { method: "DELETE" }, true);
+ invoicesCache = invoicesCache.filter((invoice) => invoice.invoice_id !== invoiceId);
+ applyInvoiceFilters();
+ await refreshSummary();
+ } catch (error) {
+ console.error(error);
+ showFeedback(dashboardFeedback, error.message || "Nie udało się usunąć faktury.");
+ }
+}
+
+function startInvoiceEdit(invoiceId) {
+ if (!invoiceId) {
+ return;
+ }
+ const invoice = invoicesCache.find((item) => item.invoice_id === invoiceId);
+ if (!invoice) {
+ showFeedback(dashboardFeedback, "Nie znaleziono wybranej faktury.");
+ return;
+ }
+
+ editingInvoiceId = invoiceId;
+ saveInvoiceButton.textContent = "Zapisz zmiany";
+ cancelEditInvoiceButton.classList.remove("hidden");
+ setActiveView("invoice-builder");
+
+ resetInvoiceForm();
+ invoiceForm.elements.saleDate.value = invoice.sale_date || "";
+ invoiceForm.elements.paymentTerm.value = invoice.payment_term || 14;
+
+ if (invoice.client) {
+ invoiceForm.elements.clientName.value = invoice.client.name || "";
+ invoiceForm.elements.clientTaxId.value = invoice.client.tax_id || "";
+ invoiceForm.elements.clientAddress.value = invoice.client.address_line || "";
+ invoiceForm.elements.clientPostalCode.value = invoice.client.postal_code || "";
+ invoiceForm.elements.clientCity.value = invoice.client.city || "";
+ invoiceForm.elements.clientPhone.value = invoice.client.phone || "";
+ }
+
+ itemsBody.innerHTML = "";
+ if (Array.isArray(invoice.items) && invoice.items.length > 0) {
+ invoice.items.forEach((item) => {
+ createItemRow({
+ name: item.name,
+ quantity: item.quantity,
+ unit_price_gross: item.unit_price_gross ?? item.gross_total,
+ vat_code: item.vat_code,
+ unit: item.unit,
+ });
+ });
+ } else {
+ createItemRow();
+ }
+
+ const note = invoice.exemption_note || "";
+ syncExemptionControlsWithNote(note);
+ const requiresNote = Array.isArray(invoice.items)
+ ? invoice.items.some((item) => requiresExemption(item.vat_code))
+ : false;
+ updateExemptionVisibility(requiresNote);
+
+ lastInvoice = invoice;
+}
+
+function exitInvoiceEdit() {
+ editingInvoiceId = null;
+ saveInvoiceButton.textContent = "Generuj fakturę";
+ cancelEditInvoiceButton.classList.add("hidden");
+}
+
+async function apiRequest(path, { method = "GET", body, headers = {} } = {}, requireAuth = false) {
+ const options = {
+ method,
+ headers: {
+ "Content-Type": "application/json",
+ ...headers,
+ },
+ };
+
+ if (body !== undefined) {
+ options.body = JSON.stringify(body);
+ }
+
+ if (requireAuth) {
+ if (!authToken) {
+ throw new Error("Brak tokenu autoryzacyjnego.");
+ }
+ options.headers.Authorization = `Bearer ${authToken}`;
+ }
+
+ const response = await fetch(path, options);
+ const isJson = response.headers.get("content-type")?.includes("application/json");
+ const data = isJson ? await response.json() : {};
+
+ if (response.status === 401) {
+ authToken = null;
+ currentLogin = "";
+ sessionStorage.removeItem("invoiceAuthToken");
+ sessionStorage.removeItem("invoiceLogin");
+ setAppState("auth");
+ throw new Error(data.error || "Sesja wygasła. Zaloguj się ponownie.");
+ }
+
+ if (!response.ok) {
+ throw new Error(data.error || "Wystapil błąd podczas komunikacji z serwerem.");
+ }
+
+ return data;
+}
+
+function renderBusinessDisplay(business) {
+ if (!business) {
+ businessDisplay.textContent = "Brak zapisanych danych firmy.";
+ return;
+ }
+
+ const fallback = (value) => {
+ if (!value) {
+ return "---";
+ }
+ const trimmed = value.toString().trim();
+ return trimmed || "---";
+ };
+
+ const companyName = fallback(business.company_name);
+ const ownerName = fallback(business.owner_name);
+ const addressLine = fallback(business.address_line);
+ const location = fallback([business.postal_code, business.city].filter(Boolean).join(" "));
+ const taxLine = `NIP: ${fallback(business.tax_id)}`;
+ const bankLine = `Konto: ${fallback(business.bank_account)}`;
+
+ businessDisplay.innerHTML = `
+
+
+ ${companyName}
+ ${ownerName}
+
+
+ ${addressLine}
+ ${location}
+
+
+ ${taxLine}
+ ${bankLine}
+
+
+ `;
+}
+
+function fillBusinessForm(business) {
+ if (!business) {
+ return;
+ }
+ businessForm.elements.company_name.value = business.company_name || "";
+ businessForm.elements.owner_name.value = business.owner_name || "";
+ businessForm.elements.address_line.value = business.address_line || "";
+ businessForm.elements.postal_code.value = business.postal_code || "";
+ businessForm.elements.city.value = business.city || "";
+ businessForm.elements.tax_id.value = business.tax_id || "";
+ businessForm.elements.bank_account.value = business.bank_account || "";
+}
+
+function setBusinessFormVisibility(visible, { preserveFeedback = false } = {}) {
+ setVisibility(businessForm, visible);
+ if (toggleBusinessFormButton) {
+ toggleBusinessFormButton.textContent = visible ? "Ukryj formularz" : "Edycja danych";
+ }
+ if (!visible && !preserveFeedback) {
+ clearFeedback(businessFeedback);
+ }
+}
+
+function vatSelectElement(initialValue = "23") {
+ const select = document.createElement("select");
+ select.className = "item-vat";
+ VAT_OPTIONS.forEach((option) => {
+ const element = document.createElement("option");
+ element.value = option.value;
+ element.textContent = option.label;
+ select.appendChild(element);
+ });
+ select.value = VAT_OPTIONS.some((option) => option.value === initialValue) ? initialValue : "23";
+ return select;
+}
+
+function unitSelectElement(initialValue = DEFAULT_UNIT) {
+ const select = document.createElement("select");
+ select.className = "item-unit";
+ UNIT_OPTIONS.forEach((option) => {
+ const element = document.createElement("option");
+ element.value = option.value;
+ element.textContent = option.label;
+ select.appendChild(element);
+ });
+ select.value = UNIT_OPTIONS.some((option) => option.value === initialValue) ? initialValue : DEFAULT_UNIT;
+ return select;
+}
+
+function createItemRow(initialValues = {}) {
+ const row = document.createElement("tr");
+
+ const nameCell = document.createElement("td");
+ const nameInput = document.createElement("input");
+ nameInput.type = "text";
+ nameInput.className = "item-name";
+ nameInput.placeholder = "Nazwa towaru lub usługi";
+ if (initialValues.name) {
+ nameInput.value = initialValues.name;
+ }
+ nameCell.appendChild(nameInput);
+
+ const quantityCell = document.createElement("td");
+ const quantityInput = document.createElement("input");
+ quantityInput.type = "number";
+ quantityInput.className = "item-quantity";
+ quantityInput.min = "1";
+ quantityInput.step = "1";
+ quantityInput.inputMode = "numeric";
+ const parsedQuantity = parseIntegerString(initialValues.quantity);
+ const safeQuantity = Number.isNaN(parsedQuantity) || parsedQuantity <= 0 ? 1 : parsedQuantity;
+ quantityInput.value = String(safeQuantity);
+ quantityCell.appendChild(quantityInput);
+
+ const unitCell = document.createElement("td");
+ const unitSelect = unitSelectElement(initialValues.unit);
+ unitCell.appendChild(unitSelect);
+
+ const unitGrossCell = document.createElement("td");
+ const unitGrossInput = document.createElement("input");
+ unitGrossInput.type = "number";
+ unitGrossInput.className = "item-gross";
+ unitGrossInput.min = "0.01";
+ unitGrossInput.step = "0.01";
+ unitGrossInput.placeholder = "Brutto";
+ if (initialValues.unit_price_gross) {
+ unitGrossInput.value = initialValues.unit_price_gross;
+ }
+ unitGrossCell.appendChild(unitGrossInput);
+
+ const vatCell = document.createElement("td");
+ const vatSelect = vatSelectElement(initialValues.vat_code);
+ vatCell.appendChild(vatSelect);
+
+ const totalCell = document.createElement("td");
+ totalCell.className = "item-total";
+ totalCell.textContent = "0.00 PLN";
+
+ const actionsCell = document.createElement("td");
+ const removeButton = document.createElement("button");
+ removeButton.type = "button";
+ removeButton.className = "remove-item";
+ removeButton.textContent = "Usuń";
+ actionsCell.appendChild(removeButton);
+
+ row.appendChild(nameCell);
+ row.appendChild(quantityCell);
+ row.appendChild(unitCell);
+ row.appendChild(unitGrossCell);
+ row.appendChild(vatCell);
+ row.appendChild(totalCell);
+ row.appendChild(actionsCell);
+
+ const handleChange = () => updateTotals();
+ nameInput.addEventListener("input", handleChange);
+ quantityInput.addEventListener("input", () => {
+ const sanitized = quantityInput.value.replace(/[^0-9]/g, "");
+ quantityInput.value = sanitized;
+ handleChange();
+ });
+ quantityInput.addEventListener("blur", () => {
+ const parsed = parseIntegerString(quantityInput.value);
+ quantityInput.value = Number.isNaN(parsed) || parsed <= 0 ? "1" : String(parsed);
+ handleChange();
+ });
+ unitGrossInput.addEventListener("input", handleChange);
+ vatSelect.addEventListener("change", handleChange);
+ unitSelect.addEventListener("change", handleChange);
+
+ removeButton.addEventListener("click", () => {
+ if (itemsBody.children.length === 1) {
+ nameInput.value = "";
+ quantityInput.value = "1";
+ unitGrossInput.value = "";
+ vatSelect.value = "23";
+ unitSelect.value = DEFAULT_UNIT;
+ updateTotals();
+ return;
+ }
+ row.remove();
+ updateTotals();
+ });
+
+ itemsBody.appendChild(row);
+ updateTotals();
+}
+
+function calculateRowTotals(row) {
+ const name = row.querySelector(".item-name")?.value.trim() ?? "";
+ const quantityRaw = row.querySelector(".item-quantity")?.value;
+ const quantityParsed = parseIntegerString(quantityRaw);
+ const quantityValid = Number.isInteger(quantityParsed) && quantityParsed > 0;
+ const quantity = quantityValid ? quantityParsed : 0;
+ const unitGross = parseNumber(row.querySelector(".item-gross")?.value);
+ const vatCode = row.querySelector(".item-vat")?.value ?? "23";
+ const rate = VAT_RATE_VALUES[vatCode] ?? 0;
+ const unit = row.querySelector(".item-unit")?.value || DEFAULT_UNIT;
+ const unitLabel = UNIT_OPTIONS.some((option) => option.value === unit) ? unit : DEFAULT_UNIT;
+
+ const hasValues = name || quantity > 0 || unitGross > 0;
+ if (!hasValues) {
+ return {
+ valid: false,
+ vatCode,
+ vatLabel: vatLabelFromCode(vatCode),
+ requiresExemption: requiresExemption(vatCode),
+ quantity,
+ unitGross,
+ unitNet: 0,
+ netTotal: 0,
+ vatAmount: 0,
+ grossTotal: 0,
+ unit: unitLabel,
+ };
+ }
+
+ if (!quantityValid || unitGross <= 0) {
+ return {
+ valid: false,
+ vatCode,
+ vatLabel: vatLabelFromCode(vatCode),
+ requiresExemption: requiresExemption(vatCode),
+ quantity,
+ unitGross,
+ unitNet: 0,
+ netTotal: 0,
+ vatAmount: 0,
+ grossTotal: quantity * unitGross,
+ unit: unitLabel,
+ };
+ }
+
+ const grossTotal = quantity * unitGross;
+ const netTotal = rate > 0 ? grossTotal / (1 + rate) : grossTotal;
+ const vatAmount = grossTotal - netTotal;
+ const unitNet = netTotal / quantity;
+
+ return {
+ valid: true,
+ vatCode,
+ vatLabel: vatLabelFromCode(vatCode),
+ requiresExemption: requiresExemption(vatCode),
+ quantity,
+ unitGross,
+ unitNet,
+ netTotal,
+ vatAmount,
+ grossTotal,
+ unit: unitLabel,
+ };
+}
+
+function updateTotals() {
+ let totalNet = 0;
+ let totalVat = 0;
+ let totalGross = 0;
+ const summary = new Map();
+ let exemptionNeeded = false;
+
+ const rows = Array.from(itemsBody.querySelectorAll("tr"));
+ rows.forEach((row) => {
+ const totals = calculateRowTotals(row);
+ if (totals.requiresExemption) {
+ exemptionNeeded = true;
+ }
+ const totalCell = row.querySelector(".item-total");
+ totalCell.textContent = formatCurrency(totals.grossTotal);
+
+ if (!totals.valid) {
+ return;
+ }
+
+ totalNet += totals.netTotal;
+ totalVat += totals.vatAmount;
+ totalGross += totals.grossTotal;
+
+ const existing = summary.get(totals.vatLabel) || { net: 0, vat: 0, gross: 0 };
+ existing.net += totals.netTotal;
+ existing.vat += totals.vatAmount;
+ existing.gross += totals.grossTotal;
+ summary.set(totals.vatLabel, existing);
+ });
+
+ totalNetLabel.textContent = `Suma netto: ${totalNet.toFixed(2)} PLN`;
+ totalVatLabel.textContent = `Kwota VAT: ${totalVat.toFixed(2)} PLN`;
+ totalGrossLabel.textContent = `Suma brutto: ${totalGross.toFixed(2)} PLN`;
+ renderRateSummary(summary);
+
+ updateExemptionVisibility(exemptionNeeded);
+}
+
+function renderRateSummary(summary) {
+ if (!summary || summary.size === 0) {
+ rateSummaryContainer.innerHTML = "";
+ return;
+ }
+
+ const entries = Array.from(summary.entries()).sort(([a], [b]) => a.localeCompare(b));
+ const markup = entries
+ .map(
+ ([label, totals]) =>
+ `
+ ${label}
+ Netto: ${totals.net.toFixed(2)} PLN
+ VAT: ${totals.vat.toFixed(2)} PLN
+ Brutto: ${totals.gross.toFixed(2)} PLN
+
`
+ )
+ .join("");
+ rateSummaryContainer.innerHTML = `Podsumowanie stawek
${markup}`;
+}
+
+function collectInvoicePayload() {
+ const items = [];
+ const rows = Array.from(itemsBody.querySelectorAll("tr"));
+
+ rows.forEach((row) => {
+ const name = row.querySelector(".item-name")?.value.trim() ?? "";
+ const quantityRaw = row.querySelector(".item-quantity")?.value;
+ const quantityParsed = parseIntegerString(quantityRaw);
+ const quantityValid = Number.isInteger(quantityParsed) && quantityParsed > 0;
+ const quantity = quantityValid ? quantityParsed : 0;
+ const unitGross = parseNumber(row.querySelector(".item-gross")?.value);
+ const vatCode = row.querySelector(".item-vat")?.value ?? "23";
+ const unitValue = row.querySelector(".item-unit")?.value || DEFAULT_UNIT;
+ const unit = UNIT_OPTIONS.some((option) => option.value === unitValue) ? unitValue : DEFAULT_UNIT;
+
+ const hasValues = name || quantity > 0 || unitGross > 0;
+ if (!hasValues) {
+ return;
+ }
+
+ if (!name) {
+ throw new Error("Każda pozycja musi mieć nazwę.");
+ }
+ if (!quantityValid) {
+ throw new Error("Ilość musi byc dodatnia liczba calkowita.");
+ }
+ if (unitGross <= 0) {
+ throw new Error("Cena brutto musi być większa od zera.");
+ }
+
+ items.push({
+ name,
+ quantity,
+ unit,
+ unit_price_gross: unitGross.toFixed(2),
+ vat_code: vatCode,
+ });
+ });
+
+ if (items.length === 0) {
+ throw new Error("Dodaj przynajmniej jedną pozycję.");
+ }
+
+ const saleDate = invoiceForm.elements.saleDate.value || null;
+ const paymentTerm = parseInt(invoiceForm.elements.paymentTerm.value) || 14;
+ const requiresExemptionNote = items.some((item) => item.vat_code === "ZW" || item.vat_code === "0");
+ let exemptionNote = "";
+ if (requiresExemptionNote) {
+ const noteFromTextarea = exemptionNoteInput.value.trim();
+ if (exemptionReasonSelect) {
+ const selectedReason = EXEMPTION_REASON_LOOKUP.get(exemptionReasonSelect.value);
+ if (selectedReason && selectedReason.value !== "custom") {
+ exemptionNote = selectedReason.note;
+ } else {
+ exemptionNote = noteFromTextarea;
+ }
+ } else {
+ exemptionNote = noteFromTextarea;
+ }
+ if (!exemptionNote) {
+ throw new Error("Wybierz lub wpisz podstawę zwolnienia dla pozycji ze stawka ZW/0%.");
+ }
+ }
+ const client = {
+ name: (invoiceForm.elements.clientName.value || "").trim(),
+ tax_id: (invoiceForm.elements.clientTaxId.value || "").trim(),
+ address_line: (invoiceForm.elements.clientAddress.value || "").trim(),
+ postal_code: (invoiceForm.elements.clientPostalCode.value || "").trim(),
+ city: (invoiceForm.elements.clientCity.value || "").trim(),
+ phone: (invoiceForm.elements.clientPhone.value || "").trim(),
+ };
+
+ return {
+ sale_date: saleDate,
+ payment_term: paymentTerm,
+ client,
+ items,
+ exemption_note: exemptionNote,
+ };
+}
+
+function renderInvoicePreview(invoice) {
+ if (!invoice || !currentBusiness) {
+ invoiceOutput.innerHTML = "Brak danych faktury.
";
+ return;
+ }
+
+ const client = invoice.client || {};
+ const hasClientData = client.name || client.address_line || client.postal_code || client.city || client.tax_id;
+
+ const itemsRows = (invoice.items || [])
+ .map((item) => {
+ const quantityDisplay = formatQuantity(item.quantity);
+ const unitDisplay = UNIT_OPTIONS.some((option) => option.value === item.unit) ? item.unit : DEFAULT_UNIT;
+ return `
+
+ | ${item.name} |
+ ${quantityDisplay} |
+ ${unitDisplay} |
+ ${formatCurrency(item.unit_price_net)} |
+ ${formatCurrency(item.net_total)} |
+ ${item.vat_label} |
+ ${formatCurrency(item.vat_amount)} |
+ ${formatCurrency(item.gross_total)} |
+
`;
+ })
+ .join("");
+
+ const summaryRows = (invoice.summary || [])
+ .map(
+ (entry) =>
+ `
+ ${entry.vat_label}
+ Netto: ${formatCurrency(entry.net_total)}
+ VAT: ${formatCurrency(entry.vat_total)}
+ Brutto: ${formatCurrency(entry.gross_total)}
+
`
+ )
+ .join("");
+
+ invoiceOutput.innerHTML = `
+
+ Numer: ${invoice.invoice_id}
+ Data wystawienia: ${invoice.issued_at}
+ Data sprzedaży: ${invoice.sale_date}
+ ${invoice.payment_term ? `Termin płatności: ${invoice.payment_term} dni` : ''}
+
+
+
+
+
+ | Nazwa |
+ Ilość |
+ Jednostka |
+ Cena jedn. netto |
+ Wartość netto (pozycja) |
+ Stawka VAT |
+ Kwota VAT (pozycja) |
+ Wartość brutto |
+
+
+ ${itemsRows}
+
+
+
Podsumowanie stawek
+ ${summaryRows}
+
+
+ Netto: ${formatCurrency(invoice.totals.net)}
+ VAT: ${formatCurrency(invoice.totals.vat)}
+ Brutto: ${formatCurrency(invoice.totals.gross)}
+
+ ${
+ invoice.exemption_note
+ ? `Podstawa prawna zwolnienia: ${invoice.exemption_note}
`
+ : ""
+ }
+ `;
+}
+
+function drawPartyBox(doc, title, lines, x, y, width) {
+ const lineHeight = 5;
+ const wrappedLines = lines.flatMap((line) => doc.splitTextToSize(line, width));
+ const boxHeight = wrappedLines.length * lineHeight + 14;
+
+ doc.roundedRect(x - 4, y - 8, width + 8, boxHeight, 2, 2);
+ doc.setFontSize(11);
+ doc.text(title, x, y);
+ doc.setFontSize(10);
+
+ let cursor = y + 5;
+ wrappedLines.forEach((line) => {
+ doc.text(line, x, cursor);
+ cursor += lineHeight;
+ });
+
+ return y - 8 + boxHeight;
+}
+
+function arrayBufferToBase64(buffer) {
+ const bytes = new Uint8Array(buffer);
+ const chunkSize = 0x8000;
+ let binary = "";
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
+ const chunk = bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length));
+ binary += String.fromCharCode.apply(null, chunk);
+ }
+ return btoa(binary);
+}
+
+const PDF_FONT_FILE = "Roboto-VariableFont_wdth,wght.ttf";
+const PDF_FONT_NAME = "RobotoPolish";
+
+async function ensurePdfFont() {
+ if (pdfFontPromise) {
+ return pdfFontPromise;
+ }
+
+ if (!window.jspdf || !window.jspdf.jsPDF) {
+ throw new Error("Biblioteka jsPDF nie została załadowana.");
+ }
+
+ const { jsPDF } = window.jspdf;
+ const loadBase64 = async () => {
+ if (typeof window !== "undefined" && window.PDF_FONT_BASE64) {
+ return window.PDF_FONT_BASE64;
+ }
+ const response = await fetch(`/${encodeURIComponent(PDF_FONT_FILE)}`);
+ if (!response.ok) {
+ throw new Error(`Nie udało się pobrać czcionki Roboto (status ${response.status}).`);
+ }
+ const buffer = await response.arrayBuffer();
+ return arrayBufferToBase64(buffer);
+ };
+
+ pdfFontPromise = loadBase64().then((data) => {
+ pdfFontBase64 = data;
+ return data;
+ });
+
+ return pdfFontPromise;
+}
+
+async function generatePdf(business, invoice, logo) {
+ if (!window.jspdf || !window.jspdf.jsPDF) {
+ alert("Biblioteka jsPDF nie została załadowana. Sprawdź połączenie z internetem.");
+ return;
+ }
+
+ let fontBase64;
+ try {
+ fontBase64 = await ensurePdfFont();
+ } catch (error) {
+ alert(error.message || "Nie udało się przygotować czcionki do PDF.");
+ return;
+ }
+
+ const { jsPDF } = window.jspdf;
+ const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
+ const marginX = 18;
+ let cursorY = 20;
+ const pageWidth = doc.internal.pageSize.getWidth();
+
+ if (!doc.getFontList()[PDF_FONT_NAME]) {
+ const embeddedFont = pdfFontBase64 || fontBase64;
+ doc.addFileToVFS(PDF_FONT_FILE, embeddedFont);
+ doc.addFont(PDF_FONT_FILE, PDF_FONT_NAME, "normal");
+ }
+
+ doc.setFont(PDF_FONT_NAME, "normal");
+ if (logo && logo.data && logo.mime_type) {
+ const format = logo.mime_type === "image/png" ? "PNG" : "JPEG";
+ const dataUrl = logo.data_url || `data:${logo.mime_type};base64,${logo.data}`;
+ try {
+ let logoWidth = 45;
+ let logoHeight = 18;
+ if (doc.getImageProperties) {
+ const props = doc.getImageProperties(dataUrl);
+ if (props?.width && props?.height) {
+ const ratio = props.height / props.width;
+ logoHeight = logoWidth * ratio;
+ if (logoHeight > 22) {
+ logoHeight = 22;
+ logoWidth = logoHeight / ratio;
+ }
+ }
+ }
+ const logoX = pageWidth - marginX - logoWidth;
+ const logoY = 14;
+ doc.addImage(dataUrl, format, logoX, logoY, logoWidth, logoHeight);
+ } catch (error) {
+ console.warn("Nie udało się dodać logo do PDF:", error);
+ }
+ }
+ doc.setFontSize(16);
+ doc.text(`Faktura ${invoice.invoice_id}`, marginX, cursorY);
+ doc.setFontSize(10);
+ doc.text(`Data wystawienia: ${invoice.issued_at}`, marginX, cursorY + 6);
+ doc.text(`Data sprzedaży: ${invoice.sale_date}`, marginX, cursorY + 12);
+
+ cursorY += 22;
+ const columnWidth = 85;
+ const sellerX = marginX + columnWidth + 12;
+
+ const clientLines = invoice.client && (invoice.client.name || invoice.client.address_line || invoice.client.city || invoice.client.tax_id || invoice.client.phone)
+ ? [
+ invoice.client.name || "---",
+ invoice.client.address_line || "",
+ `${(invoice.client.postal_code || "").trim()} ${(invoice.client.city || "").trim()}`.trim(),
+ invoice.client.tax_id ? `NIP: ${invoice.client.tax_id}` : "",
+ invoice.client.phone ? `Tel: ${invoice.client.phone}` : "",
+ ].filter((line) => line && line.trim())
+ : ["Brak danych nabywcy"];
+
+ const sellerLines = [
+ business.company_name,
+ business.owner_name,
+ business.address_line,
+ `${business.postal_code} ${business.city}`.trim(),
+ `NIP: ${business.tax_id}`,
+ `Konto: ${business.bank_account}`,
+ ];
+
+ const buyerBottom = drawPartyBox(doc, "NABYWCA", clientLines, marginX, cursorY, columnWidth);
+ const sellerBottom = drawPartyBox(doc, "SPRZEDAWCA", sellerLines, sellerX, cursorY, columnWidth);
+ cursorY = Math.max(buyerBottom, sellerBottom) + 12;
+
+ const tableColumns = [
+ { key: "name", label: "Nazwa", width: 44 },
+ { key: "quantity", label: "Ilość", width: 14 },
+ { key: "unit", label: "Jednostka", width: 14 },
+ { key: "unitNet", label: "Cena jedn. netto", width: 23 },
+ { key: "netTotal", label: "Wartość netto", width: 23 },
+ { key: "vatLabel", label: "Stawka VAT", width: 14 },
+ { key: "vatAmount", label: "Kwota VAT", width: 21 },
+ { key: "grossTotal", label: "Wartość brutto", width: 21 },
+ ];
+ const lineHeight = 5;
+ const headerLineHeight = 4.2;
+ tableColumns.forEach((column) => {
+ column.headerLines = doc.splitTextToSize(column.label, column.width - 4);
+ });
+ const headerHeight = Math.max(...tableColumns.map((column) => column.headerLines.length * headerLineHeight + 3));
+
+ const tableWidth = tableColumns.reduce((sum, column) => sum + column.width, 0);
+
+ doc.setFillColor(241, 243, 247);
+ doc.rect(marginX, cursorY, tableWidth, headerHeight, "F");
+ doc.rect(marginX, cursorY, tableWidth, headerHeight);
+ let offsetX = marginX;
+ doc.setFontSize(10);
+ tableColumns.forEach((column) => {
+ doc.rect(offsetX, cursorY, column.width, headerHeight);
+ column.headerLines.forEach((line, index) => {
+ const textY = cursorY + 4 + index * headerLineHeight;
+ doc.text((line || "").trim(), offsetX + 2, textY);
+ });
+ offsetX += column.width;
+ });
+ cursorY += headerHeight;
+
+ const invoiceItems = Array.isArray(invoice.items) ? invoice.items : [];
+ invoiceItems.forEach((item) => {
+ const quantity = formatQuantity(item.quantity);
+ const unitLabel = UNIT_OPTIONS.some((option) => option.value === item.unit) ? item.unit : DEFAULT_UNIT;
+ const unitNet = formatCurrency(item.unit_price_net);
+ const netTotal = formatCurrency(item.net_total);
+ const vatAmount = formatCurrency(item.vat_amount);
+ const grossTotal = formatCurrency(item.gross_total);
+
+ const wrapText = (text, width) =>
+ doc
+ .splitTextToSize(text ?? "", width)
+ .map((line) => line.trim());
+
+ const columnData = tableColumns.map((column) => {
+ switch (column.key) {
+ case "name":
+ return wrapText(item.name, column.width - 4);
+ case "quantity":
+ return [quantity];
+ case "unit":
+ return [unitLabel];
+ case "unitNet":
+ return [unitNet];
+ case "netTotal":
+ return [netTotal];
+ case "vatLabel":
+ return [item.vat_label];
+ case "vatAmount":
+ return [vatAmount];
+ case "grossTotal":
+ return [grossTotal];
+ default:
+ return [""];
+ }
+ });
+
+ const rowHeight = Math.max(...columnData.map((data) => data.length)) * lineHeight + 2;
+ offsetX = marginX;
+ tableColumns.forEach((column, index) => {
+ doc.rect(offsetX, cursorY, column.width, rowHeight);
+ const lines = columnData[index];
+ lines.forEach((line, lineIndex) => {
+ const textY = cursorY + (lineIndex + 1) * lineHeight;
+ const content = (line || "").trim();
+ doc.text(content, offsetX + 2, textY);
+ });
+ offsetX += column.width;
+ });
+
+ cursorY += rowHeight;
+ });
+
+ cursorY += 10;
+ doc.setFontSize(11);
+ doc.text("Podsumowanie stawek:", marginX, cursorY);
+ cursorY += 6;
+
+ const summaryEntries = Array.isArray(invoice.summary) ? invoice.summary : [];
+ summaryEntries.forEach((entry) => {
+ const summaryLine = `${entry.vat_label} – Netto: ${formatCurrency(entry.net_total)} / VAT: ${formatCurrency(entry.vat_total)} / Brutto: ${formatCurrency(entry.gross_total)}`;
+ const wrapped = doc.splitTextToSize(summaryLine, 170);
+ wrapped.forEach((line) => {
+ doc.text((line || "").trim(), marginX, cursorY);
+ cursorY += lineHeight;
+ });
+ });
+
+ cursorY += 6;
+ doc.setFontSize(12);
+ doc.text(`Suma netto: ${formatCurrency(invoice.totals.net)}`, marginX, cursorY);
+ doc.text(`Suma VAT: ${formatCurrency(invoice.totals.vat)}`, marginX, cursorY + 6);
+ doc.text(`Suma brutto: ${formatCurrency(invoice.totals.gross)}`, marginX, cursorY + 12);
+ if (invoice.payment_term) {
+ doc.text(`Termin płatności: ${invoice.payment_term} dni`, marginX, cursorY + 18);
+ }
+ cursorY += 30;
+
+ if (invoice.exemption_note) {
+ doc.setFontSize(10);
+ const noteLines = doc.splitTextToSize(`Podstawa prawna zwolnienia: ${invoice.exemption_note}`, 170);
+ doc.text(noteLines, marginX, cursorY);
+ }
+
+ doc.save(`${invoice.invoice_id}.pdf`);
+}
+
+async function loadBusinessData() {
+ const data = await apiRequest("/api/business", {}, true);
+ currentBusiness = data.business;
+ renderBusinessDisplay(currentBusiness);
+ fillBusinessForm(currentBusiness);
+ setBusinessFormVisibility(false);
+}
+
+async function loadLogo() {
+ try {
+ const data = await apiRequest("/api/logo", {}, true);
+ currentLogo = data.logo || null;
+ } catch (error) {
+ console.error("Nie udało się pobrać logo:", error);
+ currentLogo = null;
+ }
+ updateLogoPreview();
+}
+
+function resetInvoiceForm() {
+ invoiceForm.reset();
+ customExemptionNote = "";
+ updateExemptionVisibility(false);
+ itemsBody.innerHTML = "";
+ createItemRow();
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = String(now.getMonth() + 1).padStart(2, "0");
+ const day = String(now.getDate()).padStart(2, "0");
+ if (invoiceForm.elements.saleDate) {
+ invoiceForm.elements.saleDate.value = `${year}-${month}-${day}`;
+ }
+ updateTotals();
+}
+
+async function bootstrapApp() {
+ try {
+ await loadBusinessData();
+ await loadLogo();
+ exitInvoiceEdit();
+ resetInvoiceForm();
+ invoiceResult.classList.add("hidden");
+ lastInvoice = null;
+ await refreshInvoices();
+ await refreshSummary();
+ updateLoginLabel();
+ setAppState("app");
+ activeView = "invoice-builder";
+ setActiveView(activeView);
+ } catch (error) {
+ console.error(error);
+ authToken = null;
+ currentLogin = "";
+ sessionStorage.removeItem("invoiceAuthToken");
+ sessionStorage.removeItem("invoiceLogin");
+ showFeedback(loginFeedback, error.message || "Nie udało się pobrać danych konta.");
+ setAppState("auth");
+ }
+}
+
+async function initialize() {
+ exitInvoiceEdit();
+ resetInvoiceForm();
+ updateLogoPreview();
+ updateSummaryCards({});
+ updateSummaryChart({});
+ setActiveView("invoice-builder");
+ setAppState("auth");
+ closeRegisterPanel({ resetForm: true, focusTrigger: false });
+ clearFeedback(registerFeedback);
+ clearFeedback(loginFeedback);
+ try {
+ const status = await apiRequest("/api/status");
+ if (typeof status.max_logo_size === "number") {
+ maxLogoSize = status.max_logo_size;
+ }
+ if (status.legacy_login_hint) {
+ legacyLoginHint.textContent = `Zaloguj się korzystajac ze starego loginu "${status.legacy_login_hint}", a nastepnie dodaj adres email.`;
+ legacyLoginHint.classList.remove("hidden");
+ } else {
+ legacyLoginHint.classList.add("hidden");
+ legacyLoginHint.textContent = "";
+ }
+
+ if (authToken) {
+ await bootstrapApp();
+ }
+ } catch (error) {
+ console.error(error);
+ showFeedback(registerFeedback, "Nie udało się nawiązać połączenia z serwerem.");
+ }
+}
+
+if (registerForm && registerFeedback && loginFeedback) {
+ registerForm.addEventListener("submit", async (event) => {
+ event.preventDefault();
+ clearFeedback(registerFeedback);
+ clearFeedback(loginFeedback);
+
+ const formData = new FormData(registerForm);
+ const emailValue = formData.get("email")?.toString().trim() ?? "";
+ const password = formData.get("password")?.toString() ?? "";
+ const confirmPassword = formData.get("confirm_password")?.toString() ?? "";
+
+ if (!emailValue) {
+ showFeedback(registerFeedback, "Podaj adres email.");
+ return;
+ }
+ if (password !== confirmPassword) {
+ showFeedback(registerFeedback, "Hasła musza byc identyczne.");
+ return;
+ }
+
+ if (password.trim().length < 4) {
+ showFeedback(registerFeedback, "Hasło musi miec co najmniej 4 znaki.");
+ return;
+ }
+
+ const payload = {
+ email: emailValue,
+ company_name: formData.get("company_name")?.toString().trim(),
+ owner_name: formData.get("owner_name")?.toString().trim(),
+ address_line: formData.get("address_line")?.toString().trim(),
+ postal_code: formData.get("postal_code")?.toString().trim(),
+ city: formData.get("city")?.toString().trim(),
+ tax_id: formData.get("tax_id")?.toString().trim(),
+ bank_account: formData.get("bank_account")?.toString().trim(),
+ password,
+ };
+
+ try {
+ await apiRequest("/api/setup", { method: "POST", body: payload });
+ showFeedback(registerFeedback, "Konto utworzone. Możesz sie zalogowac.", "success");
+ if (loginForm && loginForm.elements.email) {
+ loginForm.elements.email.value = emailValue;
+ }
+ registerForm.reset();
+ setTimeout(() => {
+ closeRegisterPanel({ resetForm: true, focusTrigger: false });
+ clearFeedback(registerFeedback);
+ clearFeedback(loginFeedback);
+ showFeedback(loginFeedback, "Konto utworzone. Zaloguj się haslem.", "success");
+ if (loginForm) {
+ const passwordInput = loginForm.elements.password;
+ if (passwordInput) {
+ passwordInput.focus();
+ }
+ }
+ }, 1600);
+ } catch (error) {
+ showFeedback(registerFeedback, error.message || "Nie udało się utworzyć konta.");
+ }
+ });
+}
+
+if (loginForm && loginFeedback) {
+ const setLoginSubmittingState = (isSubmitting) => {
+ if (!loginSubmitButton) {
+ return;
+ }
+ if (isSubmitting) {
+ loginSubmitButton.disabled = true;
+ loginSubmitButton.setAttribute("data-loading", "true");
+ loginSubmitButton.textContent = "Logowanie...";
+ } else {
+ loginSubmitButton.disabled = false;
+ loginSubmitButton.textContent = loginSubmitButtonDefaultText;
+ loginSubmitButton.removeAttribute("data-loading");
+ }
+ };
+
+ loginForm.addEventListener("submit", async (event) => {
+ event.preventDefault();
+ clearFeedback(loginFeedback);
+
+ const emailElement = loginForm.elements.email;
+ const emailValue = emailElement ? emailElement.value.trim() : "";
+ const password = loginForm.elements.password.value;
+
+ if (!emailValue) {
+ showFeedback(loginFeedback, "Podaj adres email.");
+ return;
+ }
+
+ if (!password) {
+ showFeedback(loginFeedback, "Podaj hasło.");
+ return;
+ }
+
+ setLoginSubmittingState(true);
+
+ try {
+ const response = await apiRequest("/api/login", { method: "POST", body: { email: emailValue, password } });
+ authToken = response.token;
+ currentLogin = response.email || response.login || emailValue;
+ sessionStorage.setItem("invoiceAuthToken", authToken);
+ sessionStorage.setItem("invoiceLogin", currentLogin);
+ loginForm.reset();
+ await bootstrapApp();
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error || "");
+ let feedbackMessage = errorMessage || "Logowanie nie powiodło się.";
+ if (/nieprawidlowy (login|email) lub hasło/i.test(errorMessage)) {
+ feedbackMessage = "Podany email lub hasło są nieprawidłowe. Utwórz konto, jeśli jeszcze go nie masz.";
+ } else if (/brak autoryzacji/i.test(errorMessage) || /brak tokenu autoryzacyjnego/i.test(errorMessage)) {
+ feedbackMessage = "Sesja wygasła. Zaloguj się ponownie.";
+ } else if (/failed to fetch|networkerror/i.test(errorMessage) || error instanceof TypeError) {
+ feedbackMessage = "Nie udało się nawiązać połączenia z serwerem. Sprawdź, czy aplikacja serwerowa jest uruchomiona.";
+ }
+ showFeedback(loginFeedback, feedbackMessage);
+ } finally {
+ setLoginSubmittingState(false);
+ }
+ });
+}
+
+if (toggleBusinessFormButton && businessForm && businessFeedback) {
+ toggleBusinessFormButton.addEventListener("click", () => {
+ const isVisible = !businessForm.classList.contains("hidden");
+ if (!isVisible) {
+ fillBusinessForm(currentBusiness);
+ setBusinessFormVisibility(true, { preserveFeedback: true });
+ } else {
+ setBusinessFormVisibility(false);
+ }
+ });
+}
+
+if (cancelBusinessUpdateButton && businessForm && businessFeedback && toggleBusinessFormButton) {
+ cancelBusinessUpdateButton.addEventListener("click", () => {
+ setBusinessFormVisibility(false);
+ });
+}
+
+if (businessForm && businessFeedback) {
+ businessForm.addEventListener("submit", async (event) => {
+ event.preventDefault();
+ clearFeedback(businessFeedback);
+
+ const formData = new FormData(businessForm);
+ const payload = {
+ company_name: formData.get("company_name")?.toString().trim(),
+ owner_name: formData.get("owner_name")?.toString().trim(),
+ address_line: formData.get("address_line")?.toString().trim(),
+ postal_code: formData.get("postal_code")?.toString().trim(),
+ city: formData.get("city")?.toString().trim(),
+ tax_id: formData.get("tax_id")?.toString().trim(),
+ bank_account: formData.get("bank_account")?.toString().trim(),
+ };
+
+ try {
+ const data = await apiRequest("/api/business", { method: "PUT", body: payload }, true);
+ currentBusiness = data.business;
+ renderBusinessDisplay(currentBusiness);
+ fillBusinessForm(currentBusiness);
+ setBusinessFormVisibility(false, { preserveFeedback: true });
+ showFeedback(businessFeedback, "Dane sprzedawcy zaktualizowane.", "success");
+ setTimeout(() => clearFeedback(businessFeedback), 2000);
+ } catch (error) {
+ showFeedback(businessFeedback, error.message || "Nie udało się zaktualizować danych.");
+ }
+ });
+}
+
+if (exemptionReasonSelect) {
+ populateExemptionReasons();
+ let previousReasonValue = exemptionReasonSelect.value;
+ applyExemptionReasonSelection({ preserveCustom: true });
+ exemptionReasonSelect.addEventListener("change", () => {
+ if (previousReasonValue === "custom" && exemptionNoteInput) {
+ customExemptionNote = exemptionNoteInput.value.trim();
+ }
+ previousReasonValue = exemptionReasonSelect.value;
+ applyExemptionReasonSelection();
+ if (exemptionReasonSelect.value === "custom" && exemptionNoteInput) {
+ exemptionNoteInput.focus();
+ }
+ });
+}
+
+if (exemptionNoteInput) {
+ exemptionNoteInput.addEventListener("input", () => {
+ if (exemptionReasonSelect && exemptionReasonSelect.value === "custom") {
+ customExemptionNote = exemptionNoteInput.value;
+ }
+ });
+}
+
+if (invoiceForm) {
+ invoiceForm.addEventListener("submit", async (event) => {
+ event.preventDefault();
+ try {
+ const payload = collectInvoicePayload();
+ let response;
+ if (editingInvoiceId) {
+ response = await apiRequest(`/api/invoices/${encodeURIComponent(editingInvoiceId)}`, { method: "PUT", body: payload }, true);
+ exitInvoiceEdit();
+ } else {
+ response = await apiRequest("/api/invoices", { method: "POST", body: payload }, true);
+ }
+ lastInvoice = response.invoice;
+ renderInvoicePreview(lastInvoice);
+ if (invoiceResult) {
+ invoiceResult.classList.remove("hidden");
+ }
+ await refreshInvoices();
+ await refreshSummary();
+ resetInvoiceForm();
+ } catch (error) {
+ alert(error.message || "Nie udało się zapisać faktury.");
+ }
+ });
+}
+
+if (addItemButton) {
+ addItemButton.addEventListener("click", () => {
+ createItemRow();
+ });
+}
+
+if (downloadButton) {
+ downloadButton.addEventListener("click", async () => {
+ if (!lastInvoice || !currentBusiness) {
+ alert("Brak faktury do pobrania. Wygeneruj ją najpierw.");
+ return;
+ }
+ await generatePdf(currentBusiness, lastInvoice, currentLogo);
+ });
+}
+
+if (logoutButton) {
+ logoutButton.addEventListener("click", () => {
+ authToken = null;
+ currentLogin = "";
+ sessionStorage.removeItem("invoiceAuthToken");
+ sessionStorage.removeItem("invoiceLogin");
+ lastInvoice = null;
+ currentBusiness = null;
+ currentLogo = null;
+ invoicesCache = [];
+ exitInvoiceEdit();
+ resetInvoiceForm();
+ setBusinessFormVisibility(false);
+ if (invoiceResult) {
+ invoiceResult.classList.add("hidden");
+ }
+ updateLogoPreview();
+ updateLoginLabel();
+ renderInvoicesTable([]);
+ updateSummaryCards({});
+ updateSummaryChart({});
+ closeRegisterPanel({ resetForm: true, focusTrigger: true });
+ clearFeedback(registerFeedback);
+ clearFeedback(loginFeedback);
+ clearFeedback(businessFeedback);
+ clearFeedback(logoFeedback);
+ clearFeedback(dashboardFeedback);
+ setAppState("auth");
+ });
+}
+
+appNavButtons.forEach((button) => {
+ button.addEventListener("click", () => {
+ setActiveView(button.dataset.view);
+ });
+});
+
+if (filterStartDate) {
+ filterStartDate.addEventListener("change", applyInvoiceFilters);
+}
+if (filterEndDate) {
+ filterEndDate.addEventListener("change", applyInvoiceFilters);
+}
+if (clearFiltersButton) {
+ clearFiltersButton.addEventListener("click", () => {
+ if (filterStartDate) {
+ filterStartDate.value = "";
+ }
+ if (filterEndDate) {
+ filterEndDate.value = "";
+ }
+ applyInvoiceFilters();
+ });
+}
+
+if (showRegisterButton) {
+ showRegisterButton.addEventListener("click", () => {
+ openRegisterPanel();
+ });
+}
+
+if (backToLoginButton) {
+ backToLoginButton.addEventListener("click", () => {
+ closeRegisterPanel({ resetForm: false, focusTrigger: true });
+ });
+}
+
+if (cancelRegisterButton) {
+ cancelRegisterButton.addEventListener("click", () => {
+ closeRegisterPanel({ resetForm: true, focusTrigger: true });
+ });
+}
+
+if (logoInput) {
+ logoInput.addEventListener("change", (event) => {
+ const file = event.target.files?.[0];
+ if (!file) {
+ return;
+ }
+ clearFeedback(logoFeedback);
+ if (file.size > maxLogoSize) {
+ showFeedback(logoFeedback, `Logo jest zbyt duze. Maksymalny rozmiar to ${(maxLogoSize / 1024).toFixed(0)} KB.`);
+ logoInput.value = "";
+ return;
+ }
+ const reader = new FileReader();
+ reader.onload = async () => {
+ try {
+ const base64 = reader.result?.toString();
+ if (!base64) {
+ throw new Error("Nie udało się odczytać pliku.");
+ }
+ const response = await apiRequest(
+ "/api/logo",
+ {
+ method: "POST",
+ body: {
+ filename: file.name,
+ mime_type: file.type,
+ content: base64,
+ },
+ },
+ true
+ );
+ currentLogo = response.logo;
+ updateLogoPreview();
+ showFeedback(logoFeedback, "Logo zapisane.", "success");
+ } catch (error) {
+ showFeedback(logoFeedback, error.message || "Nie udało się zapisać logo.");
+ } finally {
+ logoInput.value = "";
+ }
+ };
+ reader.onerror = () => {
+ showFeedback(logoFeedback, "Nie udało się wczytać pliku logo.");
+ logoInput.value = "";
+ };
+ reader.readAsDataURL(file);
+ });
+}
+
+if (removeLogoButton) {
+ removeLogoButton.addEventListener("click", async () => {
+ clearFeedback(logoFeedback);
+ if (!currentLogo) {
+ showFeedback(logoFeedback, "Brak logo do usunięcia.");
+ return;
+ }
+ try {
+ await apiRequest("/api/logo", { method: "DELETE" }, true);
+ currentLogo = null;
+ updateLogoPreview();
+ showFeedback(logoFeedback, "Logo usunięte.", "success");
+ } catch (error) {
+ showFeedback(logoFeedback, error.message || "Nie udało się usunąć logo.");
+ }
+ });
+}
+
+if (cancelEditInvoiceButton) {
+ cancelEditInvoiceButton.addEventListener("click", () => {
+ exitInvoiceEdit();
+ resetInvoiceForm();
+ });
+}
+
+initialize().catch((error) => {
+ console.error(error);
+ showFeedback(registerFeedback, "Nie udało się uruchomić aplikacji.");
+});