From e9f07a4a39c39ea5cdd0a277286dee48c604c9ca Mon Sep 17 00:00:00 2001 From: BOHA Date: Thu, 2 Apr 2026 20:01:43 +0200 Subject: [PATCH] fix: invoice edit/list improvements - Due date uses days selector in edit mode (same as create) - Overdue invoices fully editable (same as issued) - Overdue status reversed to issued when due date moved to future - Invoice list: edit icon for issued/overdue, eye for paid - Invoice list: PDF opens blob from NAS (removed lang modal) - NAS cleanup: properly scans directories when cleaning old PDFs - Fixed syntax error from leftover else block Co-Authored-By: Claude Opus 4.6 (1M context) --- src/admin/pages/InvoiceDetail.tsx | 65 ++++++----- src/admin/pages/Invoices.tsx | 142 ++++++++----------------- src/services/invoices.service.ts | 9 +- src/services/nas-financials-manager.ts | 8 +- 4 files changed, 91 insertions(+), 133 deletions(-) diff --git a/src/admin/pages/InvoiceDetail.tsx b/src/admin/pages/InvoiceDetail.tsx index db52871..f02196f 100644 --- a/src/admin/pages/InvoiceDetail.tsx +++ b/src/admin/pages/InvoiceDetail.tsx @@ -615,6 +615,16 @@ export default function InvoiceDetail() { bank_account: inv.bank_account || "", }); + // Calculate dueDays from existing dates + if (inv.issue_date && inv.due_date) { + const issue = new Date(inv.issue_date); + const due = new Date(inv.due_date); + const diffDays = Math.round( + (due.getTime() - issue.getTime()) / (1000 * 60 * 60 * 24), + ); + if (diffDays > 0 && diffDays <= 60) setDueDays(diffDays); + } + // Populate items from existing invoice if (inv.items?.length > 0) { setItems( @@ -645,14 +655,13 @@ export default function InvoiceDetail() { if (isEdit) fetchDetail(); }, [isEdit, fetchDetail]); - // ─── Create mode: due date calculation ─── + // ─── Due date calculation from issue date + days ─── useEffect(() => { - if (isEdit) return; if (!form.issue_date) return; const d = new Date(form.issue_date); d.setDate(d.getDate() + dueDays); setForm((prev) => ({ ...prev, due_date: d.toISOString().split("T")[0] })); - }, [isEdit, form.issue_date, dueDays]); + }, [form.issue_date, dueDays]); // ─── Create mode: customer filtering ─── const filteredCustomers = useMemo(() => { @@ -1540,37 +1549,25 @@ export default function InvoiceDetail() { }} /> - {!isEdit ? ( - - - {form.due_date && ( - - Splatnost:{" "} - {new Date(form.due_date).toLocaleDateString("cs-CZ")} - - )} - - ) : ( - - { - setForm((prev) => ({ ...prev, due_date: val })); - }} - /> - - )} + + + {form.due_date && ( + + Splatnost:{" "} + {new Date(form.due_date).toLocaleDateString("cs-CZ")} + + )} + ({ show: false, invoice: null }); const [deleting, setDeleting] = useState(false); const [pdfLoading, setPdfLoading] = useState(null); - const [langModal, setLangModal] = useState(null); const [draft, setDraft] = useState(() => { try { const raw = localStorage.getItem(DRAFT_KEY); @@ -284,29 +283,25 @@ export default function Invoices() { } }; - const handlePdf = async (inv: Invoice, lang = "cs") => { + const handlePdf = async (inv: Invoice) => { if (pdfLoading) return; - setLangModal(null); + const newWindow = window.open("", "_blank"); setPdfLoading(inv.id); try { - const response = await apiFetch( - `${API_BASE}/invoices-pdf/${inv.id}?lang=${encodeURIComponent(lang)}`, - ); - if (response.status === 401) return; - if (!response.ok) { - alert.error("Nepodařilo se vygenerovat PDF"); + const response = await apiFetch(`${API_BASE}/invoices/${inv.id}/file`); + if (response.status === 401) { + newWindow?.close(); return; } - const html = await response.text(); - const w = window.open("", "_blank"); - if (w) { - w.document.open(); - w.document.write(html); - w.document.close(); - w.onload = () => w.print(); - } else { - alert.error("Prohlížeč zablokoval vyskakovací okno"); + if (!response.ok) { + newWindow?.close(); + alert.error("PDF soubor nenalezen — otevřete fakturu a uložte ji"); + return; } + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + if (newWindow) newWindow.location.href = url; + setTimeout(() => URL.revokeObjectURL(url), 60000); } catch { alert.error("Chyba při generování PDF"); } finally { @@ -996,26 +991,44 @@ export default function Invoices() { - - - - + {inv.status === "paid" ? ( + + + + + ) : ( + + + + + )} {hasPermission("invoices.export") && ( - - - - - )} - )} diff --git a/src/services/invoices.service.ts b/src/services/invoices.service.ts index 0132e5d..acad9a1 100644 --- a/src/services/invoices.service.ts +++ b/src/services/invoices.service.ts @@ -70,6 +70,11 @@ export async function markOverdueInvoices() { where: { status: "issued", due_date: { lt: new Date() } }, data: { status: "overdue" }, }); + // Reverse: if due_date was changed to future, set back to issued + await prisma.invoices.updateMany({ + where: { status: "overdue", due_date: { gte: new Date() } }, + data: { status: "issued" }, + }); } catch (err) { console.error("markOverdueInvoices failed:", err); } @@ -350,8 +355,8 @@ export async function updateInvoice(id: number, body: Record) { const data: Record = { modified_at: new Date() }; - // Only allow full editing in 'issued' state - const isDraft = currentStatus === "issued"; + // Allow full editing in 'issued' and 'overdue' states + const isDraft = currentStatus === "issued" || currentStatus === "overdue"; if (isDraft) { const strFields = [ "currency", diff --git a/src/services/nas-financials-manager.ts b/src/services/nas-financials-manager.ts index 586d9d6..957e209 100644 --- a/src/services/nas-financials-manager.ts +++ b/src/services/nas-financials-manager.ts @@ -41,7 +41,13 @@ class NasFinancialsManager { const yearPath = path.join(issuedDir, yearDir); if (!fs.statSync(yearPath).isDirectory()) continue; for (const monthDir of fs.readdirSync(yearPath)) { - const filePath = path.join(yearPath, monthDir, safeName); + const monthPath = path.join(yearPath, monthDir); + try { + if (!fs.statSync(monthPath).isDirectory()) continue; + } catch { + continue; + } + const filePath = path.join(monthPath, safeName); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); }