Author SHA1 Message Date
odwetaandCopilot App 3081b2a99b Default issue date field to today's date
Prefill 'Datum vystaveni' with the current local date when opening a
new invoice, instead of leaving it blank. Also switch date formatting
to use local date components instead of toISOString() to avoid UTC
timezone shifting the displayed date.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-10 17:42:31 +02:00
odweta 3e5a0813b0 Merge pull request #8 from odweta/odweta-stunning-engine
Make invoice due date optional with 14-day default
2026-09-10 17:31:02 +02:00
odweta 65c11ba86e Merge origin/main into odweta-stunning-engine 2026-09-10 17:29:53 +02:00
odwetaandCopilot App d440788d0d Make invoice due date optional with 14-day default
Datum vystaveni defaults to today and Datum splatnosti defaults to
14 days after the issue date. Both fields are now optional date
inputs and only override the default when a user enters a custom
value. Applies to the invoice editor and generated PDF.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-10 17:29:19 +02:00
odweta 1c5c074fa1 Merge pull request #7 from odweta/odweta-urban-goggles
Add optional signature upload to invoice PDF
2026-09-10 17:27:55 +02:00
odweta d2d3292a73 Merge pull request #5 from odweta/odweta-decrease-pdf-line-height
Decrease PDF invoice item row height by ~10%
2026-09-10 17:27:36 +02:00
odwetaandCopilot App a14500c7ab Decrease PDF invoice item row height by ~10%
Reduce the item table row height from 25pt to 22.5pt and re-center
the cell text within the shorter rows. The header row height is
unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-09-10 17:22:31 +02:00
3 changed files with 63 additions and 26 deletions
+36 -9
View File
@@ -480,6 +480,20 @@ app.delete('/api/invoices', requireAuth, async (req, res, next) => {
});
const pdfText = (value) => String(value || 'Neuvedeno');
const formatCzechDate = (value) => {
const date = new Date(`${value}T00:00:00`);
return Number.isNaN(date.getTime()) ? '' : date.toLocaleDateString('cs-CZ');
};
const toDateInputValue = (date) => date.toISOString().slice(0, 10);
const resolveIssueDate = (issueDate) => issueDate || toDateInputValue(new Date());
const resolveDueDate = (dueDate, issueDate) => {
if (dueDate) {
return dueDate;
}
const due = new Date(`${resolveIssueDate(issueDate)}T00:00:00`);
due.setDate(due.getDate() + 14);
return toDateInputValue(due);
};
const formatCzechNumber = (value) => Number(value || 0).toLocaleString('cs-CZ', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
@@ -554,13 +568,25 @@ const renderInvoicePdf = async (document, invoice) => {
const partyWidth = (pageWidth - 24) / 2;
const customerX = document.page.margins.left + partyWidth + 24;
const issueDate = resolveIssueDate(data.issueDate);
const dueDate = resolveDueDate(data.dueDate, data.issueDate);
const headerY = document.y;
document.fillColor(navy).font(boldFont).fontSize(25).text('Faktura', document.page.margins.left, headerY);
document.fillColor(navy).font(boldFont).fontSize(16).text(invoice.invoice_number, customerX, headerY, {
width: partyWidth,
align: 'right'
});
document.y = headerY + 40;
document.fillColor(bodyText).font(regularFont).fontSize(9)
.text(`Datum vystavení: ${formatCzechDate(issueDate)}`, customerX, headerY + 22, {
width: partyWidth,
align: 'right'
})
.text(`Datum splatnosti: ${formatCzechDate(dueDate)}`, customerX, headerY + 35, {
width: partyWidth,
align: 'right'
});
document.y = headerY + 52;
const partyTop = document.y;
const drawParty = (x, title, profile, name) => {
@@ -575,28 +601,29 @@ const renderInvoicePdf = async (document, invoice) => {
const tableTop = document.y + 12;
const columns = [0, pageWidth * 0.52, pageWidth * 0.68, pageWidth * 0.82, pageWidth];
const rowHeight = 25;
document.rect(document.page.margins.left, tableTop, pageWidth, rowHeight).fill('#eef1f3');
const headerRowHeight = 25;
const itemRowHeight = 22.5;
document.rect(document.page.margins.left, tableTop, pageWidth, headerRowHeight).fill('#eef1f3');
document.fillColor(navy).font(boldFont).fontSize(9);
['Položka', 'Množství', 'MJ', 'Cena za MJ'].forEach((heading, index) => {
document.text(heading, document.page.margins.left + columns[index] + 6, tableTop + 8, {
width: columns[index + 1] - columns[index] - 12
});
});
document.y = tableTop + rowHeight;
document.y = tableTop + headerRowHeight;
(data.items || []).forEach((item, index) => {
const rowTop = document.y;
document.rect(document.page.margins.left, rowTop, pageWidth, rowHeight)
document.rect(document.page.margins.left, rowTop, pageWidth, itemRowHeight)
.fill(index % 2 ? lightBlue : warm);
document.moveTo(document.page.margins.left, rowTop + rowHeight)
.lineTo(document.page.margins.left + pageWidth, rowTop + rowHeight)
document.moveTo(document.page.margins.left, rowTop + itemRowHeight)
.lineTo(document.page.margins.left + pageWidth, rowTop + itemRowHeight)
.strokeColor(line).lineWidth(0.7).stroke();
document.fillColor(bodyText).font(regularFont).fontSize(9);
[pdfText(item.popis), formatCzechNumber(item.mnozstvi), pdfText(item.mernaJednotka), `${formatCzechNumber(item.cenaZaMj)}`]
.forEach((value, columnIndex) => document.text(value, document.page.margins.left + columns[columnIndex] + 6, rowTop + 8, {
.forEach((value, columnIndex) => document.text(value, document.page.margins.left + columns[columnIndex] + 6, rowTop + 6, {
width: columns[columnIndex + 1] - columns[columnIndex] - 12
}));
document.y = rowTop + rowHeight;
document.y = rowTop + itemRowHeight;
});
document.y += 18;
+6 -6
View File
@@ -325,13 +325,13 @@
<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>
</select>
<label for="issueDate">Datum vystavení</label>
<input type="date" id="issueDate" name="issueDate" placeholder="Dnešní datum" />
</div>
<div class="form-group">
<label for="dueDate">Datum splatnosti</label>
<input type="date" id="dueDate" name="dueDate" placeholder="14 dní od vystavení" />
</div>
<output id="dueDateDisplay" class="due-date-display"></output>
</div>
</div>
+21 -11
View File
@@ -46,8 +46,8 @@ const invoiceStartingNumberInput = document.getElementById("invoiceStartingNumbe
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 issueDateInput = document.getElementById("issueDate");
const dueDateInput = document.getElementById("dueDate");
const paymentPresetSelect = document.getElementById("paymentPresetSelect");
const paymentPresetName = document.getElementById("paymentPresetName");
const savePaymentPresetButton = document.getElementById("savePaymentPresetButton");
@@ -207,17 +207,21 @@ const getClientLabel = (client) => [
const formatCzechDate = (dateValue) => new Date(`${dateValue}T00:00:00`).toLocaleDateString("cs-CZ");
const getDueDays = () => Number.parseInt(dueDateOption.value, 10) || 14;
const toDateInputValue = (date) => [
date.getFullYear(),
String(date.getMonth() + 1).padStart(2, "0"),
String(date.getDate()).padStart(2, "0")
].join("-");
const getIssueDate = () => issueDateInput.value || toDateInputValue(new Date());
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 = () => {
dueDateDisplay.textContent = `Splatnost: ${formatCzechDate(getDueDate())}`;
if (dueDateInput.value) {
return dueDateInput.value;
}
const dueDate = new Date(`${getIssueDate()}T00:00:00`);
dueDate.setDate(dueDate.getDate() + 14);
return toDateInputValue(dueDate);
};
const updateClientTypeFields = () => {
@@ -333,6 +337,8 @@ deletePaymentPresetButton.addEventListener("click", async () => {
});
const readInvoiceData = () => ({
issueDate: getIssueDate(),
dueDate: getDueDate(),
supplier: {
...readProfileFields("dodavatel"),
typSubjektu: supplierTypeSelect.value
@@ -363,6 +369,8 @@ const clearInvoiceEditor = () => {
currentInvoiceId = null;
invoiceNumberDisplay.textContent = "Nová faktura";
invoiceSaveStatus.textContent = "";
issueDateInput.value = toDateInputValue(new Date());
dueDateInput.value = "";
writeProfileFields("odberatel", {});
clientSelect.value = "";
clientTypeSelect.value = "osoba";
@@ -384,6 +392,8 @@ const loadInvoiceIntoEditor = (invoice) => {
currentInvoiceId = invoice.id;
invoiceNumberDisplay.textContent = invoice.invoiceNumber;
invoiceSaveStatus.textContent = "";
issueDateInput.value = invoice.data.issueDate || "";
dueDateInput.value = invoice.data.dueDate || "";
writeProfileFields("dodavatel", invoice.data.supplier || {});
supplierTypeSelect.value = invoice.data.supplier?.typSubjektu || "osoba";
updateSupplierTypeFields();