Test_DB / static /js /main.js
Antoni09's picture
Rename static/main.js to static/js/main.js
9362f36 verified
raw
history blame
31.1 kB
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 = `
<p><strong>${business.company_name}</strong></p>
<p>${business.owner_name}</p>
<p>${business.address_line}</p>
<p>${business.postal_code} ${business.city}</p>
<p>NIP: ${business.tax_id}</p>
<p>Konto: ${business.bank_account}</p>
`;
}
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]) =>
`<div class="rate-summary-item">
<span>${label}</span>
<span>Netto: ${totals.net.toFixed(2)} PLN</span>
<span>VAT: ${totals.vat.toFixed(2)} PLN</span>
<span>Brutto: ${totals.gross.toFixed(2)} PLN</span>
</div>`
)
.join("");
rateSummaryContainer.innerHTML = `<h4>Podsumowanie stawek</h4>${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 = "<p>Brak danych faktury.</p>";
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) => `
<tr>
<td>${item.name}</td>
<td>${parseNumber(item.quantity).toFixed(2)}</td>
<td>${formatCurrency(item.unit_price_net)}</td>
<td>${formatCurrency(item.net_total)}</td>
<td>${item.vat_label}</td>
<td>${formatCurrency(item.vat_amount)}</td>
<td>${formatCurrency(item.gross_total)}</td>
</tr>`
)
.join("");
const summaryRows = (invoice.summary || [])
.map(
(entry) =>
`<div class="rate-summary-item">
<span>${entry.vat_label}</span>
<span>Netto: ${formatCurrency(entry.net_total)}</span>
<span>VAT: ${formatCurrency(entry.vat_total)}</span>
<span>Brutto: ${formatCurrency(entry.gross_total)}</span>
</div>`
)
.join("");
invoiceOutput.innerHTML = `
<div class="invoice-preview-meta">
<span><strong>Numer:</strong> ${invoice.invoice_id}</span>
<span><strong>Data wystawienia:</strong> ${invoice.issued_at}</span>
<span><strong>Data sprzedazy:</strong> ${invoice.sale_date}</span>
</div>
<div class="invoice-preview-header">
<div class="invoice-preview-card">
<h4>Nabywca</h4>
${
hasClientData
? `
<p>${client.name || "---"}</p>
<p>${client.address_line || "---"}</p>
<p>${client.postal_code || ""} ${client.city || ""}</p>
<p>${client.tax_id ? `NIP: ${client.tax_id}` : ""}</p>
`
: "<p>Brak danych nabywcy.</p>"
}
</div>
<div class="invoice-preview-card">
<h4>Sprzedawca</h4>
<p>${currentBusiness.company_name}</p>
<p>${currentBusiness.owner_name}</p>
<p>${currentBusiness.address_line}</p>
<p>${currentBusiness.postal_code} ${currentBusiness.city}</p>
<p>NIP: ${currentBusiness.tax_id}</p>
<p>Konto: ${currentBusiness.bank_account}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>Nazwa</th>
<th>Ilość</th>
<th>Cena jedn. netto</th>
<th>Wartość netto (pozycja)</th>
<th>Stawka VAT</th>
<th>Kwota VAT (pozycja)</th>
<th>Wartość brutto</th>
</tr>
</thead>
<tbody>${itemsRows}</tbody>
</table>
<div class="rate-summary">
<h4>Podsumowanie stawek</h4>
${summaryRows}
</div>
<div class="invoice-preview-summary">
<span>Netto: ${formatCurrency(invoice.totals.net)}</span>
<span>VAT: ${formatCurrency(invoice.totals.vat)}</span>
<span>Brutto: ${formatCurrency(invoice.totals.gross)}</span>
</div>
${
invoice.exemption_note
? `<div class="invoice-preview-note"><strong>Podstawa prawna zwolnienia:</strong> ${invoice.exemption_note}</div>`
: ""
}
`;
}
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.");
});