diff --git a/backend/main.py b/backend/main.py index 2010237..5f2cd38 100644 --- a/backend/main.py +++ b/backend/main.py @@ -410,7 +410,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "1074" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "1075" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index eabd3e5..277a855 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -101,9 +101,9 @@ - - - + + + @@ -616,10 +616,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index a288144..011701e 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '1074'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1075'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen. diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 9f5ee0b..e83dda2 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -3715,7 +3715,7 @@ window.Page_admin = (() => { try { const inv = await API.get(`/admin/invoices/${btn.dataset.invoiceId}`); _openNeueRechnungModal(() => { - _tab = 'rechnungen'; + // Nach Speichern/Stornieren: zurück auf Upgrades-Tab, damit der Button neu rendert _renderTab(); }, { recipient_name: inv.recipient_name, @@ -3725,7 +3725,7 @@ window.Page_admin = (() => { discount_pct: inv.discount_pct || 0, notes: inv.notes || '', items: inv.items.map(it => ({ description: it.description, quantity: it.quantity, unit_price: it.unit_price })), - }, inv.id); + }, inv.id, inv.status, inv.invoice_number); } catch (e) { UI.toast.error(e.message || 'Rechnung konnte nicht geladen werden.'); } @@ -3923,7 +3923,7 @@ window.Page_admin = (() => { discount_pct: inv.discount_pct || 0, notes: inv.notes || '', items: inv.items.map(it => ({ description: it.description, quantity: it.quantity, unit_price: it.unit_price })), - }, inv.id); + }, inv.id, inv.status, inv.invoice_number); }); }); @@ -3943,16 +3943,31 @@ window.Page_admin = (() => { }); } - function _openNeueRechnungModal(reload, prefill = null, invoiceId = null) { + function _openNeueRechnungModal(reload, prefill = null, invoiceId = null, status = null, invoiceNumber = null) { const id = `inv-new-${Date.now()}`; const p = prefill || {}; - const isEdit = !!invoiceId; + const isEdit = !!invoiceId; + const isLocked = isEdit && status && status !== 'draft'; // sent / paid / cancelled → nicht änderbar + const canCancel = isEdit && status !== 'cancelled'; // alles außer schon storniert + const lockedBanner = isLocked ? (() => { + const map = { + sent: ['#fff8f0', '#f0a060', '#c05000', 'Diese Rechnung wurde bereits versendet — Inhalt nicht mehr änderbar. Korrekturen nur per Stornierung.'], + paid: ['#f0fdf4', '#86efac', '#15803d', 'Diese Rechnung ist bezahlt — Inhalt nicht änderbar.'], + cancelled: ['#fef2f2', '#fca5a5', '#b91c1c', 'Diese Rechnung wurde storniert — Inhalt nicht änderbar.'], + }; + const [bg, br, fg, msg] = map[status] || ['#f5f5f5', '#ccc', '#444', `Status: ${status}`]; + return `
${msg}
`; + })() : ''; UI.modal.open({ - title: `${UI.icon('receipt')} ${isEdit ? 'Rechnung bearbeiten' : 'Neue Rechnung erstellen'}`, + title: `${UI.icon('receipt')} ${isEdit ? (isLocked ? 'Rechnung ansehen' : 'Rechnung bearbeiten') : 'Neue Rechnung erstellen'}`, body: `
+ ${lockedBanner} + ${!isEdit && !p.recipient_name ? `
Abbrechen - +
+
+ ${canCancel ? ` + ` : ''} +
+
+ + ${!isLocked ? `` : ''} +
+
`, }); + // Bei gesperrten Rechnungen (sent/paid/cancelled) alle Eingaben & Action-Buttons readonly + if (isLocked) { + setTimeout(() => { + const form = document.getElementById(id); + if (!form) return; + form.querySelectorAll('input, textarea').forEach(inp => { inp.disabled = true; }); + // "+ Position hinzufügen" und Item-Lösch-Buttons verstecken + const addBtn = document.getElementById(`${id}-add-item`); + if (addBtn) addBtn.style.display = 'none'; + form.querySelectorAll('.inv-item-remove').forEach(b => { b.style.display = 'none'; }); + }, 0); + } + + // Stornieren — schließt dieses Modal, öffnet den Storno-Dialog + document.getElementById(`${id}-cancel-invoice`)?.addEventListener('click', () => { + // _openStornoModal ruft intern UI.modal.open() → schließt dieses Modal automatisch + _openStornoModal(invoiceId, invoiceNumber || `#${invoiceId}`, reload); + }); + // Items-Container und Hilfsfunktionen const itemsContainer = document.getElementById(`${id}-items`); const previewEl = document.getElementById(`${id}-preview`); @@ -4100,6 +4147,7 @@ window.Page_admin = (() => { // Form Submit document.getElementById(id)?.addEventListener('submit', async e => { e.preventDefault(); + if (isLocked) return; // gesperrte Rechnung — Submit ignorieren (Button ist eh ausgeblendet) const fd = new FormData(e.target); const items = []; itemsContainer.querySelectorAll('.adm-inv-item-row').forEach(row => { diff --git a/backend/static/sw.js b/backend/static/sw.js index 4eaf361..50cba78 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab -const VER = '1074'; +const VER = '1075'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten