Author SHA1 Message Date
odwetaandCopilot App 51e0182399 Anchor PDF payment card to footer and size QR to text height
Move the payment info card (account number, IBAN, SWIFT) to the
bottom of the invoice page so it sits flush with the footer, and
size the QR code to match the combined height of the three payment
lines instead of a fixed size.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-10 17:06:13 +02:00
5 changed files with 15 additions and 113 deletions
+14 -43
View File
@@ -227,33 +227,6 @@ app.put('/api/supplier', requireAuth, async (req, res, next) => {
}
});
app.get('/api/settings', requireAuth, async (req, res, next) => {
try {
const result = await pool.query('SELECT invoice_starting_number FROM user_settings WHERE user_id = $1', [req.userId]);
res.json({ settings: { invoiceStartingNumber: result.rowCount ? result.rows[0].invoice_starting_number : 1 } });
} catch (error) {
next(error);
}
});
app.put('/api/settings', requireAuth, async (req, res, next) => {
try {
const invoiceStartingNumber = Number.parseInt(req.body.invoiceStartingNumber, 10);
if (!Number.isInteger(invoiceStartingNumber) || invoiceStartingNumber < 1) {
res.status(400).json({ error: 'Počáteční číslo faktury musí být kladné celé číslo.' });
return;
}
const result = await pool.query(`
INSERT INTO user_settings (user_id, invoice_starting_number) VALUES ($1, $2)
ON CONFLICT (user_id) DO UPDATE SET invoice_starting_number = EXCLUDED.invoice_starting_number
RETURNING invoice_starting_number
`, [req.userId, invoiceStartingNumber]);
res.json({ settings: { invoiceStartingNumber: result.rows[0].invoice_starting_number } });
} catch (error) {
next(error);
}
});
app.get('/api/clients', requireAuth, async (req, res, next) => {
try {
const result = await pool.query('SELECT * FROM clients WHERE user_id = $1 ORDER BY created_at, id', [req.userId]);
@@ -403,21 +376,13 @@ app.post('/api/invoices', requireAuth, async (req, res, next) => {
const invoiceYear = new Date().getFullYear();
await client.query('BEGIN');
const settingsResult = await client.query(
'SELECT invoice_starting_number FROM user_settings WHERE user_id = $1',
[req.userId]
);
const invoiceStartingNumber = settingsResult.rowCount ? settingsResult.rows[0].invoice_starting_number : 1;
await client.query(`
INSERT INTO invoice_counters (user_id, invoice_year, last_number)
SELECT $1, $2, GREATEST(
COALESCE(MAX((substring(invoice_number FROM '^F-[0-9]{4}-([0-9]+)$'))::integer), 0),
$4 - 1
)
SELECT $1, $2, COALESCE(MAX((substring(invoice_number FROM '^F-[0-9]{4}-([0-9]+)$'))::integer), 0)
FROM invoices
WHERE user_id = $1 AND invoice_number LIKE $3
ON CONFLICT (user_id, invoice_year) DO NOTHING
`, [req.userId, invoiceYear, `F-${invoiceYear}-%`, invoiceStartingNumber]);
`, [req.userId, invoiceYear, `F-${invoiceYear}-%`]);
const counterResult = await client.query(`
UPDATE invoice_counters
SET last_number = last_number + 1
@@ -607,16 +572,22 @@ const renderInvoicePdf = async (document, invoice) => {
document.y += 35;
document.moveTo(document.page.margins.left, document.y).lineTo(document.page.margins.left + pageWidth, document.y)
.strokeColor(navy).lineWidth(1.5).stroke();
document.y += 12;
const paymentTop = document.y;
document.roundedRect(document.page.margins.left, paymentTop, pageWidth, 165, 4).fillAndStroke('#f4f7fb', line);
const paymentCardHeight = 105;
const paymentTop = document.page.height - document.page.margins.bottom - paymentCardHeight;
const paymentLineHeight = 18;
const qrSize = paymentLineHeight * 3;
document.roundedRect(document.page.margins.left, paymentTop, pageWidth, paymentCardHeight, 4)
.fillAndStroke('#f4f7fb', line);
document.fillColor(navy).font(boldFont).fontSize(11).text('Platební údaje', document.page.margins.left + 10, paymentTop + 10);
document.fillColor(bodyText).font(regularFont).fontSize(10)
.text(`Číslo účtu: ${pdfText(payment.cisloUctu)}`, document.page.margins.left + 10, paymentTop + 35)
.text(`IBAN: ${pdfText(payment.iban)}`, document.page.margins.left + 10, paymentTop + 55)
.text(`SWIFT: ${pdfText(payment.swift)}`, document.page.margins.left + 10, paymentTop + 75);
.text(`IBAN: ${pdfText(payment.iban)}`, document.page.margins.left + 10, paymentTop + 35 + paymentLineHeight)
.text(`SWIFT: ${pdfText(payment.swift)}`, document.page.margins.left + 10, paymentTop + 35 + paymentLineHeight * 2);
if (qrBuffer) {
document.image(qrBuffer, document.page.margins.left + pageWidth - 155, paymentTop + 10, { width: 145, height: 145 });
document.image(qrBuffer, document.page.margins.left + pageWidth - qrSize - 10, paymentTop + 30, {
width: qrSize,
height: qrSize
});
}
document.end();
};
-5
View File
@@ -65,11 +65,6 @@ const initializeDatabase = () => {
PRIMARY KEY (user_id, invoice_year)
);
CREATE TABLE IF NOT EXISTS user_settings (
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
invoice_starting_number INTEGER NOT NULL DEFAULT 1 CHECK (invoice_starting_number > 0)
);
CREATE TABLE IF NOT EXISTS payment_presets (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-17
View File
@@ -65,28 +65,11 @@
<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="numberingSettingsButton" class="text-button">Číslování faktur</button>
<button type="button" id="logoutButton" class="text-button">Odhlásit se</button>
</div>
</details>
</nav>
<section id="numberingSettingsPanel" class="password-panel" hidden>
<div id="numberingSettingsForm">
<h3>Číslování faktur</h3>
<div class="form-group">
<label for="invoiceStartingNumber">Počáteční číslo faktury (pro aktuální rok)</label>
<input id="invoiceStartingNumber" type="number" min="1" step="1" placeholder="1" />
<p class="field-hint">Určuje, od jakého čísla se budou generovat nové faktury v roce, kdy zatím žádnou nemáte. Výchozí hodnota je 1.</p>
</div>
<div class="saved-data-actions">
<button type="button" id="saveNumberingSettingsButton">Uložit</button>
<button type="button" id="cancelNumberingSettingsButton" class="secondary-button">Zrušit</button>
<span id="numberingSettingsStatus" role="status" aria-live="polite"></span>
</div>
</div>
</section>
<section id="passwordPanel" class="password-panel" hidden>
<div id="passwordForm">
<h3>Změnit heslo</h3>
+1 -42
View File
@@ -40,12 +40,6 @@ const passwordForm = document.getElementById("passwordForm");
const savePasswordButton = document.getElementById("savePasswordButton");
const cancelPasswordButton = document.getElementById("cancelPasswordButton");
const passwordStatus = document.getElementById("passwordStatus");
const numberingSettingsButton = document.getElementById("numberingSettingsButton");
const numberingSettingsPanel = document.getElementById("numberingSettingsPanel");
const invoiceStartingNumberInput = document.getElementById("invoiceStartingNumber");
const saveNumberingSettingsButton = document.getElementById("saveNumberingSettingsButton");
const cancelNumberingSettingsButton = document.getElementById("cancelNumberingSettingsButton");
const numberingSettingsStatus = document.getElementById("numberingSettingsStatus");
const dueDateOption = document.getElementById("dueDateOption");
const dueDateDisplay = document.getElementById("dueDateDisplay");
const paymentPresetSelect = document.getElementById("paymentPresetSelect");
@@ -525,27 +519,10 @@ deleteClientButton.addEventListener("click", async () => {
const showInvoiceApp = async () => {
authScreen.hidden = true;
document.getElementById("main-container").hidden = false;
await Promise.all([loadSupplier(), loadClients(), loadInvoices(), loadPaymentPresets(), loadNumberingSettings()]);
await Promise.all([loadSupplier(), loadClients(), loadInvoices(), loadPaymentPresets()]);
showInvoiceScreen("list");
};
const loadNumberingSettings = async () => {
try {
const { settings } = await apiRequest("/api/settings");
invoiceStartingNumberInput.value = settings.invoiceStartingNumber;
invoiceStartingNumberInput.placeholder = String(settings.invoiceStartingNumber);
} catch {
// Keep the placeholder default if settings cannot be loaded.
}
};
const showNumberingSettingsPanel = (visible) => {
numberingSettingsPanel.hidden = !visible;
if (!visible) {
numberingSettingsStatus.textContent = "";
}
};
const showPasswordPanel = (visible) => {
passwordPanel.hidden = !visible;
if (!visible) {
@@ -598,24 +575,6 @@ logoutButton.addEventListener("click", async () => {
passwordButton.addEventListener("click", () => showPasswordPanel(passwordPanel.hidden));
cancelPasswordButton.addEventListener("click", () => showPasswordPanel(false));
numberingSettingsButton.addEventListener("click", () => showNumberingSettingsPanel(numberingSettingsPanel.hidden));
cancelNumberingSettingsButton.addEventListener("click", () => showNumberingSettingsPanel(false));
saveNumberingSettingsButton.addEventListener("click", async () => {
numberingSettingsStatus.textContent = "";
const invoiceStartingNumber = Number.parseInt(invoiceStartingNumberInput.value, 10) || 1;
try {
const { settings } = await apiRequest("/api/settings", {
method: "PUT",
body: JSON.stringify({ invoiceStartingNumber })
});
invoiceStartingNumberInput.value = settings.invoiceStartingNumber;
numberingSettingsStatus.textContent = "Uloženo";
} catch (error) {
numberingSettingsStatus.textContent = error.message;
}
});
savePasswordButton.addEventListener("click", async () => {
passwordStatus.textContent = "";
try {
-6
View File
@@ -595,12 +595,6 @@ body {
outline-offset: 1px;
}
.field-hint {
margin: 0.35rem 0 0;
color: var(--muted-ink);
font-size: 0.8rem;
}
.form-group output {
min-height: 1.5rem;
padding: 0.25rem 0;