v2
This commit is contained in:
+205
-19
@@ -7,10 +7,106 @@
|
||||
<title>Fakturovač</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="main-container">
|
||||
<div>
|
||||
<section id="auth-screen" aria-labelledby="auth-title">
|
||||
<div class="auth-card">
|
||||
<p class="eyebrow">FAKTUROVAČ</p>
|
||||
<h1 id="auth-title">Vaše faktury, bezpečně po ruce</h1>
|
||||
<p class="auth-intro">Přihlaste se a mějte dodavatele i odběratele uložené ve svém účtu.</p>
|
||||
<form id="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="authEmail">E-mail</label>
|
||||
<input id="authEmail" type="email" autocomplete="email" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="authPassword">Heslo</label>
|
||||
<input id="authPassword" type="password" minlength="8" autocomplete="current-password" required />
|
||||
</div>
|
||||
<button type="submit" id="authSubmitButton">Přihlásit se</button>
|
||||
<button type="button" id="authModeButton" class="text-button">Nemáte účet? Zaregistrovat se</button>
|
||||
<button type="button" id="forgotPasswordButton" class="text-button">Zapomenuté heslo?</button>
|
||||
<p id="authStatus" class="auth-status" role="alert" aria-live="polite"></p>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main id="main-container" hidden>
|
||||
<nav class="top-navigation" aria-label="Hlavní navigace">
|
||||
<details class="nav-menu">
|
||||
<summary>Faktury</summary>
|
||||
<div class="nav-menu-content">
|
||||
<button type="button" id="invoiceListNavButton" class="nav-button">
|
||||
<span class="menu-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>
|
||||
</span>
|
||||
Seznam faktur
|
||||
</button>
|
||||
<button type="button" id="newInvoiceButton" class="nav-button">
|
||||
<span class="menu-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||
</span>
|
||||
Nová faktura
|
||||
</button>
|
||||
<button type="button" id="exportInvoicesButton" class="nav-button">
|
||||
<span class="menu-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg>
|
||||
</span>
|
||||
Export PDF (ZIP)
|
||||
</button>
|
||||
<button type="button" id="deleteAllInvoicesButton" class="nav-button danger-nav-button">
|
||||
<span class="menu-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24"><path d="M3 6h18"></path><path d="M8 6V4h8v2"></path><path d="M19 6l-1 14H6L5 6"></path><path d="M10 11v5"></path><path d="M14 11v5"></path></svg>
|
||||
</span>
|
||||
Smazat všechny a resetovat číslování
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
<span id="currentViewLabel" class="current-view-label">Seznam faktur</span>
|
||||
<details class="nav-menu nav-account">
|
||||
<summary><span class="account-mark" aria-hidden="true"></span><span id="navEmail" class="signed-in-email"></span></summary>
|
||||
<div class="nav-menu-content account-menu-content">
|
||||
<button type="button" id="passwordButton" class="text-button">Změnit heslo</button>
|
||||
<button type="button" id="logoutButton" class="text-button">Odhlásit se</button>
|
||||
</div>
|
||||
</details>
|
||||
</nav>
|
||||
|
||||
<section id="passwordPanel" class="password-panel" hidden>
|
||||
<div id="passwordForm">
|
||||
<h3>Změnit heslo</h3>
|
||||
<div class="password-fields">
|
||||
<div class="form-group">
|
||||
<label for="currentPassword">Současné heslo</label>
|
||||
<input id="currentPassword" type="password" autocomplete="current-password" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newPassword">Nové heslo</label>
|
||||
<input id="newPassword" type="password" minlength="8" autocomplete="new-password" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="saved-data-actions">
|
||||
<button type="button" id="savePasswordButton">Uložit nové heslo</button>
|
||||
<button type="button" id="cancelPasswordButton" class="secondary-button">Zrušit</button>
|
||||
<span id="passwordStatus" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="invoice-list-screen" class="invoice-list-screen">
|
||||
<div class="screen-heading">
|
||||
<div>
|
||||
<p class="eyebrow">ARCHIV</p>
|
||||
<h1>Vaše faktury</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="invoiceList" class="invoice-list"></div>
|
||||
</section>
|
||||
|
||||
<div id="invoice-editor-screen" hidden>
|
||||
<form id="main-form">
|
||||
<h2>Fakturovač</h2>
|
||||
<div class="invoice-meta">
|
||||
<span id="invoiceNumberDisplay">Nová faktura</span>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
ico
|
||||
jmeno
|
||||
@@ -30,7 +126,7 @@
|
||||
----------
|
||||
|
||||
polozky
|
||||
+ popis
|
||||
+ položka
|
||||
+ mnozstvi
|
||||
+ merna jednotka MJ
|
||||
+ cena za MJ
|
||||
@@ -43,19 +139,34 @@
|
||||
<div id="dodavatelWrapper" class="form-part">
|
||||
<h3>Dodavatel</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dodavatelTypSubjektu">Typ dodavatele</label>
|
||||
<select id="dodavatelTypSubjektu">
|
||||
<option value="osoba">Jméno a příjmení</option>
|
||||
<option value="spolecnost">Společnost</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dodavatelIco">IČO</label>
|
||||
<input id="dodavatelIco" name="dodavatelIco" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dodavatelJmeno">Jméno</label>
|
||||
<input id="dodavatelJmeno" name="dodavatelJmeno" />
|
||||
<div id="supplierPersonFields" class="person-fields">
|
||||
<div class="form-group">
|
||||
<label for="dodavatelJmeno">Jméno</label>
|
||||
<input id="dodavatelJmeno" name="dodavatelJmeno" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dodavatelPrijmeni">Příjmení</label>
|
||||
<input id="dodavatelPrijmeni" name="dodavatelPrijmeni" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dodavatelPrijmeni">Příjmení</label>
|
||||
<input id="dodavatelPrijmeni" name="dodavatelPrijmeni" />
|
||||
<div id="supplierCompanyFields" class="form-group" hidden>
|
||||
<label for="dodavatelNazevSpolecnosti">Název společnosti</label>
|
||||
<input id="dodavatelNazevSpolecnosti" name="dodavatelNazevSpolecnosti" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -72,24 +183,52 @@
|
||||
<label for="dodavatelMesto">Město</label>
|
||||
<input id="dodavatelMesto" name="dodavatelMesto" />
|
||||
</div>
|
||||
|
||||
<div class="saved-data-actions">
|
||||
<button type="button" id="saveSupplierButton">Uložit dodavatele</button>
|
||||
<button type="button" id="loadSupplierButton" class="secondary-button">Načíst uloženého</button>
|
||||
<span id="supplierSaveStatus" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="odberatelWrapper" class="form-part">
|
||||
<h3>Odběratel</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="clientSelect">Uložený odběratel</label>
|
||||
<select id="clientSelect">
|
||||
<option value="">Vyberte odběratele</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="odberatelIco">IČO</label>
|
||||
<input id="odberatelIco" name="odberatelIco" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="odberatelJmeno">Jméno</label>
|
||||
<input id="odberatelJmeno" name="odberatelJmeno" />
|
||||
<label for="odberatelTypSubjektu">Typ odběratele</label>
|
||||
<select id="odberatelTypSubjektu">
|
||||
<option value="osoba">Jméno a příjmení</option>
|
||||
<option value="spolecnost">Společnost</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="odberatelPrijmeni">Příjmení</label>
|
||||
<input id="odberatelPrijmeni" name="odberatelPrijmeni" />
|
||||
<div id="personFields" class="person-fields">
|
||||
<div class="form-group">
|
||||
<label for="odberatelJmeno">Jméno</label>
|
||||
<input id="odberatelJmeno" name="odberatelJmeno" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="odberatelPrijmeni">Příjmení</label>
|
||||
<input id="odberatelPrijmeni" name="odberatelPrijmeni" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="companyFields" class="form-group" hidden>
|
||||
<label for="odberatelNazevSpolecnosti">Název společnosti</label>
|
||||
<input id="odberatelNazevSpolecnosti" name="odberatelNazevSpolecnosti" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -106,6 +245,13 @@
|
||||
<label for="odberatelMesto">Město</label>
|
||||
<input id="odberatelMesto" name="odberatelMesto" />
|
||||
</div>
|
||||
|
||||
<div class="saved-data-actions client-actions">
|
||||
<button type="button" id="saveClientButton">Uložit odběratele</button>
|
||||
<button type="button" id="deleteClientButton" class="secondary-button" disabled>
|
||||
Smazat vybraného
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,7 +264,27 @@
|
||||
</div>
|
||||
|
||||
<div class="form-chapter third-row">
|
||||
<div class="form-part">
|
||||
<div class="payment-preset-column">
|
||||
<div class="form-group">
|
||||
<label for="paymentPresetSelect">Uložené platební údaje</label>
|
||||
<select id="paymentPresetSelect">
|
||||
<option value="">Vyberte konfiguraci</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group payment-preset-name">
|
||||
<label for="paymentPresetName">Název platebních údajů</label>
|
||||
<input id="paymentPresetName" type="text" placeholder="Např. hlavní účet" />
|
||||
</div>
|
||||
|
||||
<div class="payment-preset-actions">
|
||||
<button type="button" id="savePaymentPresetButton">Uložení platebních údajů</button>
|
||||
<button type="button" id="loadPaymentPresetButton">Načtení platebních údajů</button>
|
||||
<button type="button" id="deletePaymentPresetButton" class="secondary-button" disabled>Smazat</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-account-column">
|
||||
<div class="form-group">
|
||||
<label for="cisloUctu">Číslo účtu</label>
|
||||
<input id="cisloUctu" name="cisloUctu" />
|
||||
@@ -135,15 +301,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="total">Celková částka</label>
|
||||
<output id="total" name="total">0.00</output>
|
||||
<div class="form-group total-amount-group">
|
||||
<label for="total">Celková částka (CZK)</label>
|
||||
<output id="total" name="total">0.00 Kč</output>
|
||||
</div>
|
||||
|
||||
<div class="due-date-group">
|
||||
<div class="form-group">
|
||||
<label for="dueDateOption">Datum splatnosti</label>
|
||||
<select id="dueDateOption">
|
||||
<option value="14">14 dní</option>
|
||||
<option value="30">30 dní</option>
|
||||
<option value="custom">Vlastní počet dní</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group custom-due-days-group" hidden>
|
||||
<label for="customDueDays">Počet dní</label>
|
||||
<input id="customDueDays" type="number" min="1" step="1" value="14" />
|
||||
</div>
|
||||
<output id="dueDateDisplay" class="due-date-display"></output>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="printInvoiceButton">
|
||||
Vygenerovat a vytisknout fakturu
|
||||
</button>
|
||||
<button type="button" id="saveInvoiceButton" class="save-invoice-button">
|
||||
Uložit fakturu
|
||||
</button>
|
||||
<p id="invoiceSaveStatus" class="invoice-save-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+644
-12
@@ -1,8 +1,614 @@
|
||||
const addItemButton = document.getElementById("addItemButton");
|
||||
const itemsContainer = document.getElementById("itemsContainer");
|
||||
const printInvoiceButton = document.getElementById("printInvoiceButton");
|
||||
const saveInvoiceButton = document.getElementById("saveInvoiceButton");
|
||||
const invoiceSaveStatus = document.getElementById("invoiceSaveStatus");
|
||||
const invoiceNumberDisplay = document.getElementById("invoiceNumberDisplay");
|
||||
const invoiceList = document.getElementById("invoiceList");
|
||||
const invoiceListScreen = document.getElementById("invoice-list-screen");
|
||||
const invoiceEditorScreen = document.getElementById("invoice-editor-screen");
|
||||
const invoiceListNavButton = document.getElementById("invoiceListNavButton");
|
||||
const newInvoiceButton = document.getElementById("newInvoiceButton");
|
||||
const exportInvoicesButton = document.getElementById("exportInvoicesButton");
|
||||
const deleteAllInvoicesButton = document.getElementById("deleteAllInvoicesButton");
|
||||
const currentViewLabel = document.getElementById("currentViewLabel");
|
||||
const saveSupplierButton = document.getElementById("saveSupplierButton");
|
||||
const loadSupplierButton = document.getElementById("loadSupplierButton");
|
||||
const supplierSaveStatus = document.getElementById("supplierSaveStatus");
|
||||
const clientSelect = document.getElementById("clientSelect");
|
||||
const clientTypeSelect = document.getElementById("odberatelTypSubjektu");
|
||||
const personFields = document.getElementById("personFields");
|
||||
const companyFields = document.getElementById("companyFields");
|
||||
const supplierTypeSelect = document.getElementById("dodavatelTypSubjektu");
|
||||
const supplierPersonFields = document.getElementById("supplierPersonFields");
|
||||
const supplierCompanyFields = document.getElementById("supplierCompanyFields");
|
||||
const saveClientButton = document.getElementById("saveClientButton");
|
||||
const deleteClientButton = document.getElementById("deleteClientButton");
|
||||
const authScreen = document.getElementById("auth-screen");
|
||||
const authForm = document.getElementById("auth-form");
|
||||
const authEmail = document.getElementById("authEmail");
|
||||
const authPassword = document.getElementById("authPassword");
|
||||
const authSubmitButton = document.getElementById("authSubmitButton");
|
||||
const authModeButton = document.getElementById("authModeButton");
|
||||
const forgotPasswordButton = document.getElementById("forgotPasswordButton");
|
||||
const authStatus = document.getElementById("authStatus");
|
||||
const logoutButton = document.getElementById("logoutButton");
|
||||
const signedInEmail = document.getElementById("navEmail");
|
||||
const passwordButton = document.getElementById("passwordButton");
|
||||
const passwordPanel = document.getElementById("passwordPanel");
|
||||
const passwordForm = document.getElementById("passwordForm");
|
||||
const savePasswordButton = document.getElementById("savePasswordButton");
|
||||
const cancelPasswordButton = document.getElementById("cancelPasswordButton");
|
||||
const passwordStatus = document.getElementById("passwordStatus");
|
||||
const dueDateOption = document.getElementById("dueDateOption");
|
||||
const customDueDays = document.getElementById("customDueDays");
|
||||
const customDueDaysGroup = document.querySelector(".custom-due-days-group");
|
||||
const dueDateDisplay = document.getElementById("dueDateDisplay");
|
||||
const paymentPresetSelect = document.getElementById("paymentPresetSelect");
|
||||
const paymentPresetName = document.getElementById("paymentPresetName");
|
||||
const savePaymentPresetButton = document.getElementById("savePaymentPresetButton");
|
||||
const loadPaymentPresetButton = document.getElementById("loadPaymentPresetButton");
|
||||
const deletePaymentPresetButton = document.getElementById("deletePaymentPresetButton");
|
||||
|
||||
document.querySelectorAll(".nav-menu button").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
button.closest("details").open = false;
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
document.querySelectorAll(".nav-menu[open]").forEach((menu) => {
|
||||
if (!menu.contains(event.target)) {
|
||||
menu.open = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let itemIndex = 0;
|
||||
let clients = [];
|
||||
let invoices = [];
|
||||
let paymentPresets = [];
|
||||
let currentInvoiceId = null;
|
||||
let isRegistrationMode = false;
|
||||
|
||||
const profileFieldMap = {
|
||||
nazevSpolecnosti: "NazevSpolecnosti",
|
||||
ico: "Ico",
|
||||
jmeno: "Jmeno",
|
||||
prijmeni: "Prijmeni",
|
||||
uliceCp: "UliceCp",
|
||||
psc: "Psc",
|
||||
mesto: "Mesto"
|
||||
};
|
||||
|
||||
const readProfileFields = (prefix) => Object.fromEntries(
|
||||
Object.entries(profileFieldMap).map(([key, suffix]) => [
|
||||
key,
|
||||
document.getElementById(`${prefix}${suffix}`).value.trim()
|
||||
])
|
||||
);
|
||||
|
||||
const writeProfileFields = (prefix, fields) => Object.entries(profileFieldMap).forEach(([key, suffix]) => {
|
||||
const field = document.getElementById(`${prefix}${suffix}`);
|
||||
if (field) {
|
||||
field.value = fields[key] || "";
|
||||
}
|
||||
});
|
||||
|
||||
const writeFields = (fields) => Object.entries(fields).forEach(([id, value]) => {
|
||||
const field = document.getElementById(id);
|
||||
if (field) {
|
||||
field.value = value || "";
|
||||
}
|
||||
});
|
||||
|
||||
const getClientLabel = (client) => [
|
||||
client.data.nazevSpolecnosti,
|
||||
[client.data.jmeno, client.data.prijmeni].filter(Boolean).join(" "),
|
||||
client.data.ico && `IČO: ${client.data.ico}`
|
||||
].filter(Boolean).join(" ") || "Bez názvu";
|
||||
|
||||
const formatCzechDate = (dateValue) => new Date(`${dateValue}T00:00:00`).toLocaleDateString("cs-CZ");
|
||||
|
||||
const getDueDays = () => dueDateOption.value === "custom"
|
||||
? Math.max(1, Number.parseInt(customDueDays.value, 10) || 14)
|
||||
: Number.parseInt(dueDateOption.value, 10);
|
||||
|
||||
const getDueDate = () => {
|
||||
const dueDate = new Date();
|
||||
dueDate.setHours(0, 0, 0, 0);
|
||||
dueDate.setDate(dueDate.getDate() + getDueDays());
|
||||
return dueDate.toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
const updateDueDate = () => {
|
||||
customDueDaysGroup.hidden = dueDateOption.value !== "custom";
|
||||
dueDateDisplay.textContent = `Splatnost: ${formatCzechDate(getDueDate())}`;
|
||||
};
|
||||
|
||||
const updateClientTypeFields = () => {
|
||||
const isCompany = clientTypeSelect.value === "spolecnost";
|
||||
personFields.hidden = isCompany;
|
||||
companyFields.hidden = !isCompany;
|
||||
};
|
||||
|
||||
const updateSupplierTypeFields = () => {
|
||||
const isCompany = supplierTypeSelect.value === "spolecnost";
|
||||
supplierPersonFields.hidden = isCompany;
|
||||
supplierCompanyFields.hidden = !isCompany;
|
||||
};
|
||||
|
||||
const apiRequest = async (url, options = {}) => {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(options.headers || {})
|
||||
}
|
||||
});
|
||||
const data = response.status === 204 ? null : await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.error || "Požadavek se nepodařilo dokončit.");
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const showInvoiceScreen = (screen) => {
|
||||
invoiceListScreen.hidden = screen !== "list";
|
||||
invoiceEditorScreen.hidden = screen !== "editor";
|
||||
currentViewLabel.textContent = screen === "list"
|
||||
? "Seznam faktur"
|
||||
: (currentInvoiceId ? "Upravit fakturu" : "Nová faktura");
|
||||
};
|
||||
|
||||
const renderPaymentPresetOptions = (selectedId = paymentPresetSelect.value) => {
|
||||
paymentPresetSelect.innerHTML = "<option value=\"\">Vyberte konfiguraci</option>";
|
||||
paymentPresets.forEach((preset) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = preset.id;
|
||||
option.textContent = preset.name;
|
||||
paymentPresetSelect.appendChild(option);
|
||||
});
|
||||
paymentPresetSelect.value = selectedId;
|
||||
deletePaymentPresetButton.disabled = !paymentPresetSelect.value;
|
||||
};
|
||||
|
||||
const loadPaymentPresets = async () => {
|
||||
const response = await apiRequest("/api/payment-presets");
|
||||
paymentPresets = response.presets;
|
||||
renderPaymentPresetOptions();
|
||||
};
|
||||
|
||||
paymentPresetSelect.addEventListener("change", () => {
|
||||
const preset = paymentPresets.find(({ id }) => String(id) === paymentPresetSelect.value);
|
||||
if (!preset) {
|
||||
deletePaymentPresetButton.disabled = true;
|
||||
return;
|
||||
}
|
||||
paymentPresetName.value = preset.name;
|
||||
deletePaymentPresetButton.disabled = false;
|
||||
});
|
||||
|
||||
loadPaymentPresetButton.addEventListener("click", () => {
|
||||
const preset = paymentPresets.find(({ id }) => String(id) === paymentPresetSelect.value);
|
||||
if (!preset) {
|
||||
window.alert("Nejprve vyberte uložené platební údaje.");
|
||||
return;
|
||||
}
|
||||
document.getElementById("cisloUctu").value = preset.accountNumber;
|
||||
document.getElementById("iban").value = preset.iban;
|
||||
document.getElementById("swift").value = preset.swift;
|
||||
});
|
||||
|
||||
savePaymentPresetButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const response = await apiRequest("/api/payment-presets", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: paymentPresetName.value,
|
||||
accountNumber: getValue("cisloUctu"),
|
||||
iban: getValue("iban"),
|
||||
swift: getValue("swift")
|
||||
})
|
||||
});
|
||||
const preset = response.preset;
|
||||
paymentPresets = [
|
||||
...paymentPresets.filter((entry) => entry.id !== preset.id && entry.name !== preset.name),
|
||||
preset
|
||||
].sort((left, right) => left.name.localeCompare(right.name));
|
||||
renderPaymentPresetOptions(String(preset.id));
|
||||
paymentPresetName.value = preset.name;
|
||||
} catch (error) {
|
||||
window.alert(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
deletePaymentPresetButton.addEventListener("click", async () => {
|
||||
if (!paymentPresetSelect.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiRequest(`/api/payment-presets/${paymentPresetSelect.value}`, { method: "DELETE" });
|
||||
paymentPresets = paymentPresets.filter(({ id }) => String(id) !== paymentPresetSelect.value);
|
||||
paymentPresetName.value = "";
|
||||
renderPaymentPresetOptions();
|
||||
} catch (error) {
|
||||
window.alert(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
const readInvoiceData = () => ({
|
||||
supplier: {
|
||||
...readProfileFields("dodavatel"),
|
||||
typSubjektu: supplierTypeSelect.value
|
||||
},
|
||||
customer: {
|
||||
...readProfileFields("odberatel"),
|
||||
typSubjektu: clientTypeSelect.value
|
||||
},
|
||||
payment: {
|
||||
cisloUctu: getValue("cisloUctu"),
|
||||
iban: getValue("iban"),
|
||||
swift: getValue("swift")
|
||||
},
|
||||
total: Number.parseFloat(getValue("total").replace(",", ".")) || 0,
|
||||
items: Array.from(itemsContainer.querySelectorAll(".invoice-item")).map((item) => {
|
||||
const inputs = item.querySelectorAll("input");
|
||||
return {
|
||||
popis: inputs[0].value.trim(),
|
||||
mnozstvi: Number(inputs[1].value) || 0,
|
||||
mernaJednotka: inputs[2].value.trim(),
|
||||
cenaZaMj: Number(inputs[3].value) || 0
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
const clearInvoiceEditor = () => {
|
||||
currentInvoiceId = null;
|
||||
invoiceNumberDisplay.textContent = "Nová faktura";
|
||||
invoiceSaveStatus.textContent = "";
|
||||
writeProfileFields("odberatel", {});
|
||||
clientSelect.value = "";
|
||||
clientTypeSelect.value = "osoba";
|
||||
updateClientTypeFields();
|
||||
paymentPresetSelect.value = "";
|
||||
paymentPresetName.value = "";
|
||||
deletePaymentPresetButton.disabled = true;
|
||||
["cisloUctu", "iban", "swift"].forEach((id) => {
|
||||
document.getElementById(id).value = "";
|
||||
});
|
||||
itemsContainer.replaceChildren();
|
||||
itemIndex = 0;
|
||||
updateTotal();
|
||||
};
|
||||
|
||||
const loadInvoiceIntoEditor = (invoice) => {
|
||||
currentInvoiceId = invoice.id;
|
||||
invoiceNumberDisplay.textContent = invoice.invoiceNumber;
|
||||
invoiceSaveStatus.textContent = "";
|
||||
writeProfileFields("dodavatel", invoice.data.supplier || {});
|
||||
supplierTypeSelect.value = invoice.data.supplier?.typSubjektu || "osoba";
|
||||
updateSupplierTypeFields();
|
||||
writeProfileFields("odberatel", invoice.data.customer || {});
|
||||
clientTypeSelect.value = invoice.data.customer?.typSubjektu || "osoba";
|
||||
updateClientTypeFields();
|
||||
paymentPresetSelect.value = "";
|
||||
paymentPresetName.value = "";
|
||||
deletePaymentPresetButton.disabled = true;
|
||||
["cisloUctu", "iban", "swift"].forEach((id) => {
|
||||
document.getElementById(id).value = invoice.data.payment?.[id] || "";
|
||||
});
|
||||
itemsContainer.replaceChildren();
|
||||
itemIndex = 0;
|
||||
(invoice.data.items || []).forEach((item) => addInvoiceItem(item));
|
||||
updateTotal();
|
||||
showInvoiceScreen("editor");
|
||||
};
|
||||
|
||||
const getInvoiceLabel = (invoice) => {
|
||||
const customer = invoice.data.customer || {};
|
||||
return customer.typSubjektu === "spolecnost"
|
||||
? customer.nazevSpolecnosti || "Bez odběratele"
|
||||
: [customer.jmeno, customer.prijmeni].filter(Boolean).join(" ") || "Bez odběratele";
|
||||
};
|
||||
|
||||
const renderInvoiceList = () => {
|
||||
if (!invoices.length) {
|
||||
invoiceList.innerHTML = "<p class=\"empty-list\">Zatím nemáte uložené žádné faktury.</p>";
|
||||
return;
|
||||
}
|
||||
invoiceList.innerHTML = invoices.map((invoice) => `
|
||||
<article class="invoice-list-row">
|
||||
<button type="button" class="invoice-open-button" data-invoice-id="${invoice.id}">
|
||||
<strong>${escapeHtml(invoice.invoiceNumber)}</strong>
|
||||
<span>${escapeHtml(getInvoiceLabel(invoice))}</span>
|
||||
<small>${new Date(invoice.updatedAt).toLocaleDateString("cs-CZ")}</small>
|
||||
</button>
|
||||
<button type="button" class="invoice-delete-button" data-delete-invoice-id="${invoice.id}">Smazat</button>
|
||||
</article>
|
||||
`).join("");
|
||||
};
|
||||
|
||||
const loadInvoices = async () => {
|
||||
const response = await apiRequest("/api/invoices");
|
||||
invoices = response.invoices;
|
||||
renderInvoiceList();
|
||||
};
|
||||
|
||||
const openNewInvoice = () => {
|
||||
clearInvoiceEditor();
|
||||
showInvoiceScreen("editor");
|
||||
};
|
||||
|
||||
const saveInvoice = async () => {
|
||||
try {
|
||||
const response = await apiRequest(currentInvoiceId ? `/api/invoices/${currentInvoiceId}` : "/api/invoices", {
|
||||
method: currentInvoiceId ? "PUT" : "POST",
|
||||
body: JSON.stringify({ data: readInvoiceData() })
|
||||
});
|
||||
const invoice = response.invoice;
|
||||
currentInvoiceId = invoice.id;
|
||||
invoiceNumberDisplay.textContent = invoice.invoiceNumber;
|
||||
invoiceSaveStatus.textContent = "Faktura uložena";
|
||||
await loadInvoices();
|
||||
showInvoiceScreen("list");
|
||||
} catch (error) {
|
||||
invoiceSaveStatus.textContent = error.message;
|
||||
}
|
||||
};
|
||||
|
||||
invoiceListNavButton.addEventListener("click", async () => {
|
||||
await loadInvoices();
|
||||
showInvoiceScreen("list");
|
||||
});
|
||||
|
||||
newInvoiceButton.addEventListener("click", openNewInvoice);
|
||||
exportInvoicesButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const response = await fetch("/api/invoices/export", { credentials: "same-origin" });
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || "Export se nepodařil.");
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = downloadUrl;
|
||||
link.download = "faktury.zip";
|
||||
link.click();
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
} catch (error) {
|
||||
window.alert(error.message);
|
||||
}
|
||||
});
|
||||
saveInvoiceButton.addEventListener("click", saveInvoice);
|
||||
deleteAllInvoicesButton.addEventListener("click", async () => {
|
||||
if (!invoices.length || !window.confirm("Opravdu chcete smazat všechny faktury a začít číslovat znovu od 0001?")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await apiRequest("/api/invoices", { method: "DELETE" });
|
||||
invoices = [];
|
||||
renderInvoiceList();
|
||||
clearInvoiceEditor();
|
||||
showInvoiceScreen("list");
|
||||
} catch (error) {
|
||||
window.alert(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
invoiceList.addEventListener("click", async (event) => {
|
||||
const openButton = event.target.closest("[data-invoice-id]");
|
||||
const deleteButton = event.target.closest("[data-delete-invoice-id]");
|
||||
if (deleteButton) {
|
||||
if (!window.confirm("Opravdu chcete tuto fakturu smazat?")) {
|
||||
return;
|
||||
}
|
||||
await apiRequest(`/api/invoices/${deleteButton.dataset.deleteInvoiceId}`, { method: "DELETE" });
|
||||
await loadInvoices();
|
||||
return;
|
||||
}
|
||||
if (openButton) {
|
||||
const invoice = invoices.find(({ id }) => String(id) === openButton.dataset.invoiceId);
|
||||
if (invoice) {
|
||||
loadInvoiceIntoEditor(invoice);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const saveSupplier = async () => {
|
||||
try {
|
||||
const data = readProfileFields("dodavatel");
|
||||
data.typSubjektu = supplierTypeSelect.value;
|
||||
await apiRequest("/api/supplier", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
supplierSaveStatus.textContent = "Uloženo na serveru";
|
||||
} catch (error) {
|
||||
supplierSaveStatus.textContent = error.message;
|
||||
}
|
||||
};
|
||||
|
||||
const loadSupplier = async () => {
|
||||
const { supplier } = await apiRequest("/api/supplier");
|
||||
if (supplier) {
|
||||
writeProfileFields("dodavatel", supplier);
|
||||
supplierTypeSelect.value = supplier.typSubjektu || (supplier.nazevSpolecnosti ? "spolecnost" : "osoba");
|
||||
updateSupplierTypeFields();
|
||||
supplierSaveStatus.textContent = "Uloženo na serveru";
|
||||
return true;
|
||||
}
|
||||
supplierSaveStatus.textContent = "Zatím není uložený dodavatel";
|
||||
return false;
|
||||
};
|
||||
|
||||
const renderClientOptions = (selectedId = clientSelect.value) => {
|
||||
clientSelect.innerHTML = "<option value=\"\">Vyberte odběratele</option>";
|
||||
|
||||
clients.forEach((client) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = client.id;
|
||||
option.textContent = getClientLabel(client);
|
||||
clientSelect.appendChild(option);
|
||||
});
|
||||
|
||||
clientSelect.value = selectedId;
|
||||
deleteClientButton.disabled = !clientSelect.value;
|
||||
};
|
||||
|
||||
const loadClients = async () => {
|
||||
const response = await apiRequest("/api/clients");
|
||||
clients = response.clients;
|
||||
renderClientOptions();
|
||||
};
|
||||
|
||||
const loadSelectedClient = () => {
|
||||
const client = clients.find(({ id }) => String(id) === clientSelect.value);
|
||||
if (client) {
|
||||
writeProfileFields("odberatel", client.data);
|
||||
clientTypeSelect.value = client.typSubjektu || (client.data.nazevSpolecnosti ? "spolecnost" : "osoba");
|
||||
updateClientTypeFields();
|
||||
}
|
||||
deleteClientButton.disabled = !client;
|
||||
};
|
||||
|
||||
saveSupplierButton.addEventListener("click", saveSupplier);
|
||||
supplierTypeSelect.addEventListener("change", updateSupplierTypeFields);
|
||||
loadSupplierButton.addEventListener("click", async () => {
|
||||
try {
|
||||
await loadSupplier();
|
||||
supplierSaveStatus.textContent = "Dodavatel načten";
|
||||
} catch (error) {
|
||||
supplierSaveStatus.textContent = error.message;
|
||||
}
|
||||
});
|
||||
clientTypeSelect.addEventListener("change", updateClientTypeFields);
|
||||
|
||||
clientSelect.addEventListener("change", loadSelectedClient);
|
||||
|
||||
saveClientButton.addEventListener("click", async () => {
|
||||
const data = readProfileFields("odberatel");
|
||||
data.typSubjektu = clientTypeSelect.value;
|
||||
const existingClient = clients.find((client) => String(client.id) === clientSelect.value);
|
||||
try {
|
||||
const response = await apiRequest(existingClient ? `/api/clients/${existingClient.id}` : "/api/clients", {
|
||||
method: existingClient ? "PUT" : "POST",
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const client = response.client;
|
||||
clients = existingClient
|
||||
? clients.map((entry) => entry.id === client.id ? client : entry)
|
||||
: [...clients, client];
|
||||
renderClientOptions(String(client.id));
|
||||
} catch (error) {
|
||||
window.alert(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
deleteClientButton.addEventListener("click", async () => {
|
||||
if (!clientSelect.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/clients/${clientSelect.value}`, { method: "DELETE" });
|
||||
clients = clients.filter(({ id }) => String(id) !== clientSelect.value);
|
||||
renderClientOptions();
|
||||
} catch (error) {
|
||||
window.alert(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
const showInvoiceApp = async () => {
|
||||
authScreen.hidden = true;
|
||||
document.getElementById("main-container").hidden = false;
|
||||
await Promise.all([loadSupplier(), loadClients(), loadInvoices(), loadPaymentPresets()]);
|
||||
showInvoiceScreen("list");
|
||||
};
|
||||
|
||||
const showPasswordPanel = (visible) => {
|
||||
passwordPanel.hidden = !visible;
|
||||
if (!visible) {
|
||||
passwordForm.querySelectorAll("input").forEach((input) => {
|
||||
input.value = "";
|
||||
});
|
||||
passwordStatus.textContent = "";
|
||||
}
|
||||
};
|
||||
|
||||
const setAuthMode = (registrationMode) => {
|
||||
isRegistrationMode = registrationMode;
|
||||
authSubmitButton.textContent = registrationMode ? "Vytvořit účet" : "Přihlásit se";
|
||||
authModeButton.textContent = registrationMode
|
||||
? "Máte účet? Přihlásit se"
|
||||
: "Nemáte účet? Zaregistrovat se";
|
||||
authPassword.autocomplete = registrationMode ? "new-password" : "current-password";
|
||||
authStatus.textContent = "";
|
||||
};
|
||||
|
||||
authModeButton.addEventListener("click", () => setAuthMode(!isRegistrationMode));
|
||||
|
||||
forgotPasswordButton.addEventListener("click", () => {
|
||||
authStatus.textContent = "Obnova hesla e-mailem zatím není nakonfigurovaná. Přihlaste se a použijte Změnit heslo, nebo kontaktujte správce aplikace.";
|
||||
});
|
||||
|
||||
authForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
authStatus.textContent = "";
|
||||
authSubmitButton.disabled = true;
|
||||
try {
|
||||
const { user } = await apiRequest(isRegistrationMode ? "/api/auth/register" : "/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: authEmail.value, password: authPassword.value })
|
||||
});
|
||||
signedInEmail.textContent = user.email;
|
||||
await showInvoiceApp();
|
||||
} catch (error) {
|
||||
authStatus.textContent = error.message;
|
||||
} finally {
|
||||
authSubmitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
logoutButton.addEventListener("click", async () => {
|
||||
await apiRequest("/api/auth/logout", { method: "POST" });
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
passwordButton.addEventListener("click", () => showPasswordPanel(passwordPanel.hidden));
|
||||
cancelPasswordButton.addEventListener("click", () => showPasswordPanel(false));
|
||||
|
||||
savePasswordButton.addEventListener("click", async () => {
|
||||
passwordStatus.textContent = "";
|
||||
try {
|
||||
await apiRequest("/api/auth/password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
currentPassword: document.getElementById("currentPassword").value,
|
||||
newPassword: document.getElementById("newPassword").value
|
||||
})
|
||||
});
|
||||
passwordForm.querySelectorAll("input").forEach((input) => {
|
||||
input.value = "";
|
||||
});
|
||||
passwordStatus.textContent = "Heslo bylo změněno";
|
||||
} catch (error) {
|
||||
passwordStatus.textContent = error.message;
|
||||
}
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const { user } = await apiRequest("/api/auth/me");
|
||||
signedInEmail.textContent = user.email;
|
||||
await showInvoiceApp();
|
||||
} catch {
|
||||
authScreen.hidden = false;
|
||||
document.getElementById("main-container").hidden = true;
|
||||
}
|
||||
})();
|
||||
|
||||
const escapeHtml = (value) => String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
@@ -13,9 +619,19 @@ const escapeHtml = (value) => String(value ?? "")
|
||||
|
||||
const getValue = (id) => {
|
||||
const field = document.getElementById(id);
|
||||
if (!field) {
|
||||
return "";
|
||||
}
|
||||
return ("value" in field ? field.value : field.textContent).trim();
|
||||
};
|
||||
|
||||
const formatCzechNumber = (value) => Number(value || 0).toLocaleString("cs-CZ", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
|
||||
const getTotalValue = () => Number.parseFloat(getValue("total").replace(",", ".")) || 0;
|
||||
|
||||
const updateTotal = () => {
|
||||
const total = Array.from(itemsContainer.querySelectorAll(".invoice-item"))
|
||||
.reduce((sum, item) => {
|
||||
@@ -24,7 +640,7 @@ const updateTotal = () => {
|
||||
return sum + quantity * unitPrice;
|
||||
}, 0);
|
||||
|
||||
document.getElementById("total").textContent = total.toFixed(2);
|
||||
document.getElementById("total").textContent = `${formatCzechNumber(total)} Kč`;
|
||||
};
|
||||
|
||||
const getPersonName = (prefix) => [
|
||||
@@ -38,7 +654,10 @@ const getAddress = (prefix) => [
|
||||
].filter(Boolean).join(", ");
|
||||
|
||||
const renderPerson = (prefix) => [
|
||||
getPersonName(prefix),
|
||||
((prefix === "odberatel" && getValue("odberatelTypSubjektu") === "spolecnost")
|
||||
|| (prefix === "dodavatel" && getValue("dodavatelTypSubjektu") === "spolecnost"))
|
||||
? getValue(`${prefix}NazevSpolecnosti`)
|
||||
: getPersonName(prefix),
|
||||
getValue(`${prefix}Ico`) ? `IČO: ${getValue(`${prefix}Ico`)}` : "",
|
||||
getAddress(prefix)
|
||||
].filter(Boolean).map(escapeHtml).join("<br>");
|
||||
@@ -48,9 +667,9 @@ const renderItems = () => Array.from(itemsContainer.querySelectorAll(".invoice-i
|
||||
const values = Array.from(item.querySelectorAll("input")).map((input) => input.value.trim());
|
||||
return `<tr class="invoice-row">
|
||||
<td>${escapeHtml(values[0])}</td>
|
||||
<td>${escapeHtml(values[1])}</td>
|
||||
<td>${escapeHtml(values[1] ? formatCzechNumber(values[1]) : "0,00")}</td>
|
||||
<td>${escapeHtml(values[2])}</td>
|
||||
<td>${escapeHtml(values[3])}</td>
|
||||
<td>${escapeHtml(values[3] ? formatCzechNumber(values[3]) : "0,00")} Kč</td>
|
||||
</tr>`;
|
||||
}).join("");
|
||||
|
||||
@@ -74,7 +693,7 @@ const domesticAccountToIban = (accountNumber) => {
|
||||
return `CZ${checkDigits}${bban}`;
|
||||
};
|
||||
|
||||
const buildPaymentQrPayload = (accountNumber, iban, swift, total) => {
|
||||
const buildPaymentQrPayload = (accountNumber, iban, swift, total, invoiceNumber = "") => {
|
||||
const preferredAccount = normalizeIban(accountNumber);
|
||||
const fallbackAccount = normalizeIban(iban);
|
||||
const paymentAccount = /^([A-Z]{2})\d{2}[A-Z0-9]{10,32}$/.test(preferredAccount)
|
||||
@@ -90,6 +709,7 @@ const buildPaymentQrPayload = (accountNumber, iban, swift, total) => {
|
||||
`ACC:${paymentAccount}`,
|
||||
`AM:${Number(total || 0).toFixed(2)}`,
|
||||
"CC:CZK",
|
||||
invoiceNumber && `X-VS:${invoiceNumber.replace(/\D/g, "").slice(-10)}`,
|
||||
swift && `X-SWIFT:${normalizeIban(swift)}`
|
||||
].filter(Boolean).join("*");
|
||||
};
|
||||
@@ -102,7 +722,8 @@ const buildInvoiceHtml = () => {
|
||||
accountNumber,
|
||||
iban,
|
||||
swift,
|
||||
getValue("total")
|
||||
Number.parseFloat(getValue("total").replace(",", ".")) || 0,
|
||||
invoiceNumberDisplay.textContent
|
||||
);
|
||||
const qrMarkup = qrPayload
|
||||
? `<img class="qr-code" src="https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(qrPayload)}" alt="QR kód platebních údajů">`
|
||||
@@ -139,16 +760,16 @@ const buildInvoiceHtml = () => {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Faktura</h1>
|
||||
<h1>Faktura ${escapeHtml(invoiceNumberDisplay.textContent)}</h1>
|
||||
<div class="parties">
|
||||
<section class="party"><h2>Dodavatel</h2>${renderPerson("dodavatel") || "Neuvedeno"}</section>
|
||||
<section class="party"><h2>Odběratel</h2>${renderPerson("odberatel") || "Neuvedeno"}</section>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Popis</th><th>Množství</th><th>MJ</th><th>Cena za MJ</th></tr></thead>
|
||||
<thead><tr><th>Položka</th><th>Množství</th><th>MJ</th><th>Cena za MJ</th></tr></thead>
|
||||
<tbody>${renderItems() || "<tr><td colspan=\"4\">Žádné položky</td></tr>"}</tbody>
|
||||
</table>
|
||||
<p class="total">Celková částka: ${escapeHtml(getValue("total")) || "0"}</p>
|
||||
<p class="total">Celková částka: ${formatCzechNumber(getTotalValue())} Kč</p>
|
||||
<div class="payment-row">
|
||||
<section class="payment"><h2>Platební údaje</h2>${paymentDetails || "Neuvedeno"}</section>
|
||||
<div class="qr-placeholder">${qrMarkup}</div>
|
||||
@@ -171,7 +792,7 @@ printInvoiceButton.addEventListener("click", () => {
|
||||
printWindow.addEventListener("load", () => printWindow.print(), { once: true });
|
||||
});
|
||||
|
||||
addItemButton.addEventListener("click", () => {
|
||||
const addInvoiceItem = (values = {}) => {
|
||||
itemIndex++;
|
||||
|
||||
const item = document.createElement("div");
|
||||
@@ -179,7 +800,7 @@ addItemButton.addEventListener("click", () => {
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="form-group">
|
||||
<label for="polozka${itemIndex}Popis">Popis</label>
|
||||
<label for="polozka${itemIndex}Popis">Položka</label>
|
||||
<input
|
||||
id="polozka${itemIndex}Popis"
|
||||
name="polozky[${itemIndex}][popis]"
|
||||
@@ -250,6 +871,12 @@ addItemButton.addEventListener("click", () => {
|
||||
</button>
|
||||
`;
|
||||
|
||||
const inputs = item.querySelectorAll("input");
|
||||
inputs[0].value = values.popis || "";
|
||||
inputs[1].value = values.mnozstvi ?? "";
|
||||
inputs[2].value = values.mernaJednotka || "";
|
||||
inputs[3].value = values.cenaZaMj ?? "";
|
||||
|
||||
const deleteButton = item.querySelector(".delete-item-button");
|
||||
|
||||
deleteButton.addEventListener("click", () => {
|
||||
@@ -259,6 +886,11 @@ addItemButton.addEventListener("click", () => {
|
||||
|
||||
itemsContainer.appendChild(item);
|
||||
updateTotal();
|
||||
});
|
||||
};
|
||||
|
||||
addItemButton.addEventListener("click", () => addInvoiceItem());
|
||||
|
||||
itemsContainer.addEventListener("input", updateTotal);
|
||||
|
||||
updateClientTypeFields();
|
||||
updateSupplierTypeFields();
|
||||
|
||||
+636
-4
@@ -22,6 +22,94 @@ body {
|
||||
font-family: "Trebuchet MS", "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#auth-screen {
|
||||
display: grid;
|
||||
min-height: calc(100vh - 6rem);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: min(100%, 460px);
|
||||
padding: clamp(1.75rem, 5vw, 3.5rem);
|
||||
background: var(--paper);
|
||||
border: 1px solid rgba(24, 59, 86, 0.12);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 0.75rem;
|
||||
color: var(--blue);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
|
||||
.auth-card h1 {
|
||||
margin: 0;
|
||||
color: var(--navy);
|
||||
font-family: Georgia, serif;
|
||||
font-size: clamp(2rem, 6vw, 3.4rem);
|
||||
font-weight: 500;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.auth-intro {
|
||||
margin: 1.25rem 0 2rem;
|
||||
color: var(--muted-ink);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
#auth-form {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#auth-form .form-group {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#authSubmitButton {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.8rem 1rem;
|
||||
color: #fff;
|
||||
background: var(--navy);
|
||||
border: 0;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#authSubmitButton:hover:not(:disabled) {
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.text-button {
|
||||
padding: 0;
|
||||
color: var(--blue);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-button:hover {
|
||||
color: var(--navy);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.auth-status {
|
||||
min-height: 1.25rem;
|
||||
margin: 0;
|
||||
color: #a53c36;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
width: min(100%, 980px);
|
||||
margin: 0 auto;
|
||||
@@ -31,6 +119,303 @@ body {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.top-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 0.9rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-menu summary {
|
||||
list-style: none;
|
||||
padding: 0.65rem 0.9rem;
|
||||
color: var(--navy);
|
||||
background: transparent;
|
||||
border: 1px solid var(--blue);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nav-menu summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-menu summary::after {
|
||||
margin-left: 0.5rem;
|
||||
content: "▾";
|
||||
}
|
||||
|
||||
.nav-menu[open] summary::after {
|
||||
content: "▴";
|
||||
}
|
||||
|
||||
.nav-menu-content {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: calc(100% + 0.45rem);
|
||||
left: 0;
|
||||
display: grid;
|
||||
min-width: 20rem;
|
||||
gap: 0.35rem;
|
||||
padding: 0.55rem;
|
||||
background: var(--paper);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.nav-menu-content .nav-button,
|
||||
.nav-menu-content .text-button {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
display: inline-block;
|
||||
width: 1.35rem;
|
||||
margin-right: 0.35rem;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.menu-icon svg {
|
||||
display: block;
|
||||
width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
margin: 0 auto;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.current-view-label {
|
||||
color: var(--muted-ink);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nav-account {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.nav-account summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.nav-account .nav-menu-content {
|
||||
right: 0;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.danger-nav-button {
|
||||
color: #8c3d36 !important;
|
||||
border-color: #c47a72 !important;
|
||||
}
|
||||
|
||||
.danger-nav-button:hover {
|
||||
color: #fff !important;
|
||||
background: #a53c36 !important;
|
||||
}
|
||||
|
||||
.nav-button,
|
||||
.primary-action {
|
||||
padding: 0.65rem 0.9rem;
|
||||
color: var(--navy);
|
||||
background: transparent;
|
||||
border: 1px solid var(--blue);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.primary-nav-button,
|
||||
.primary-action {
|
||||
color: #fff;
|
||||
background: var(--navy);
|
||||
}
|
||||
|
||||
.nav-button:hover,
|
||||
.primary-action:hover {
|
||||
color: #fff;
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.nav-account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.account-mark {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
border: 1px solid var(--blue);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.account-mark::before {
|
||||
position: absolute;
|
||||
top: 0.28rem;
|
||||
left: 0.52rem;
|
||||
width: 0.42rem;
|
||||
height: 0.42rem;
|
||||
background: var(--blue);
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.account-mark::after {
|
||||
position: absolute;
|
||||
bottom: 0.25rem;
|
||||
left: 0.32rem;
|
||||
width: 0.8rem;
|
||||
height: 0.42rem;
|
||||
background: var(--blue);
|
||||
border-radius: 0.5rem 0.5rem 0.25rem 0.25rem;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.invoice-list-screen {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.screen-heading {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.screen-heading h1 {
|
||||
margin: 0;
|
||||
color: var(--navy);
|
||||
font-family: Georgia, serif;
|
||||
font-size: clamp(2rem, 5vw, 3rem);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.screen-heading .eyebrow {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.invoice-list {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.invoice-list-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: #f4f7fb;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.invoice-open-button {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(8rem, 0.8fr) minmax(10rem, 2fr) auto;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem;
|
||||
color: var(--ink);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.invoice-open-button strong {
|
||||
color: var(--navy);
|
||||
}
|
||||
|
||||
.invoice-open-button small {
|
||||
color: var(--muted-ink);
|
||||
}
|
||||
|
||||
.invoice-delete-button {
|
||||
align-self: center;
|
||||
padding: 0.55rem 0.75rem;
|
||||
color: #8c3d36;
|
||||
background: transparent;
|
||||
border: 1px solid #c47a72;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.invoice-delete-button:hover {
|
||||
color: #fff;
|
||||
background: #a53c36;
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
padding: 2.5rem 1rem;
|
||||
color: var(--muted-ink);
|
||||
background: #f4f7fb;
|
||||
border: 1px dashed var(--line);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invoice-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
max-width: 820px;
|
||||
color: var(--muted-ink);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.invoice-meta span {
|
||||
color: var(--navy);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-invoice-button {
|
||||
align-self: center;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #fff;
|
||||
background: var(--blue);
|
||||
border: 0;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-invoice-button:hover {
|
||||
background: var(--navy);
|
||||
}
|
||||
|
||||
.invoice-save-status {
|
||||
min-height: 1.25rem;
|
||||
margin: -1.25rem 0 0;
|
||||
color: var(--muted-ink);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#main-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -38,8 +423,16 @@ body {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
#main-form > h2 {
|
||||
.app-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.app-heading h2 {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
color: var(--navy);
|
||||
font-family: Georgia, serif;
|
||||
@@ -49,6 +442,109 @@ body {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-heading .text-button {
|
||||
color: var(--muted-ink);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.account-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.account-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
gap: 0.75rem;
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
.signed-in-email {
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
color: var(--muted-ink);
|
||||
font-size: 0.8rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.password-panel {
|
||||
width: 100%;
|
||||
max-width: 820px;
|
||||
padding: 1rem 1.25rem;
|
||||
background: #f4f7fb;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.password-panel h3 {
|
||||
margin: 0 0 1rem;
|
||||
color: var(--navy);
|
||||
font-family: Georgia, serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.password-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.payment-preset-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.payment-preset-name {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.payment-preset-actions button {
|
||||
min-height: 2.6rem;
|
||||
padding: 0.55rem 0.7rem;
|
||||
color: var(--navy);
|
||||
background: transparent;
|
||||
border: 1px solid var(--blue);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.payment-preset-actions button:hover:not(:disabled) {
|
||||
color: #fff;
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.payment-preset-actions .secondary-button {
|
||||
color: #8c3d36;
|
||||
border-color: #c47a72;
|
||||
}
|
||||
|
||||
.payment-preset-actions .secondary-button:hover:not(:disabled) {
|
||||
background: #a53c36;
|
||||
}
|
||||
|
||||
.payment-preset-actions button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.payment-preset-column,
|
||||
.payment-account-column {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.form-chapter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -120,11 +616,16 @@ body {
|
||||
|
||||
.third-row {
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.third-row > .form-part,
|
||||
.third-row > .form-group {
|
||||
flex: 1;
|
||||
.total-amount-group {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.supplier-customer-section {
|
||||
@@ -149,6 +650,91 @@ body {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.65rem 0.7rem;
|
||||
color: var(--ink);
|
||||
background: #fff;
|
||||
border: 1px solid #c8d0d5;
|
||||
border-radius: 0.25rem;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
select:focus {
|
||||
border-color: var(--blue);
|
||||
outline: 2px solid rgba(43, 111, 143, 0.18);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.person-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.saved-data-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.saved-data-actions button {
|
||||
min-height: 2.6rem;
|
||||
}
|
||||
|
||||
.client-actions,
|
||||
#dodavatelWrapper > .saved-data-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.client-actions button,
|
||||
#dodavatelWrapper > .saved-data-actions button {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.saved-data-actions > [role="status"] {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.saved-data-actions button {
|
||||
padding: 0.55rem 0.75rem;
|
||||
color: var(--navy);
|
||||
background: transparent;
|
||||
border: 1px solid var(--blue);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.saved-data-actions button:hover:not(:disabled) {
|
||||
color: #fff;
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.saved-data-actions .secondary-button {
|
||||
color: #8c3d36;
|
||||
border-color: #c47a72;
|
||||
}
|
||||
|
||||
.saved-data-actions .secondary-button:hover:not(:disabled) {
|
||||
background: #a53c36;
|
||||
}
|
||||
|
||||
.saved-data-actions button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
#supplierSaveStatus {
|
||||
color: var(--muted-ink);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#itemsContainer {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -234,6 +820,7 @@ body {
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
padding: 0.6rem 0.85rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
#addItemButton:hover {
|
||||
@@ -241,6 +828,10 @@ body {
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
#printInvoiceButton {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
body {
|
||||
padding: 1rem 0.5rem;
|
||||
@@ -250,11 +841,52 @@ body {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.top-navigation,
|
||||
.nav-account {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-account {
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.screen-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.invoice-open-button {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.supplier-customer-section {
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
.app-heading {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.account-bar {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.password-fields,
|
||||
.person-fields,
|
||||
.third-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.total-amount-group {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.invoice-item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user