From e714580d7715234420627679acb97765b02a7105 Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 15 May 2026 16:06:08 +0200 Subject: [PATCH] Feat: Cashflow auf paid_amount, Differenz-Badge, Kulanz-Abschreibung im Bezahlt-Modal (SW by-v984) --- backend/main.py | 2 +- backend/routes/invoices.py | 24 ++++++++++---- backend/static/js/app.js | 2 +- backend/static/js/pages/admin.js | 54 +++++++++++++++++++++++++++++--- backend/static/sw.js | 2 +- 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/backend/main.py b/backend/main.py index da0e0ae..f41be0b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -408,7 +408,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "983" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "984" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/invoices.py b/backend/routes/invoices.py index dc54a90..53562f0 100644 --- a/backend/routes/invoices.py +++ b/backend/routes/invoices.py @@ -38,6 +38,7 @@ class InvoiceCreate(BaseModel): class PayBody(BaseModel): paid_at: str paid_amount: float + notes: Optional[str] = None class CancelBody(BaseModel): @@ -373,7 +374,9 @@ def get_cashflow(admin=Depends(require_admin)): with db() as conn: monthly = conn.execute(""" SELECT substr(created_at, 1, 7) AS month, - SUM(amount_gross) AS revenue, + SUM(CASE WHEN status='paid' + THEN COALESCE(paid_amount, amount_gross) + ELSE amount_gross END) AS revenue, COUNT(*) AS count FROM invoices WHERE status IN ('sent', 'paid') @@ -383,7 +386,10 @@ def get_cashflow(admin=Depends(require_admin)): year = datetime.now().year total_year = conn.execute( - "SELECT COALESCE(SUM(amount_gross),0) FROM invoices WHERE status IN ('sent','paid') AND created_at LIKE ?", + """SELECT COALESCE(SUM(CASE WHEN status='paid' + THEN COALESCE(paid_amount, amount_gross) + ELSE amount_gross END), 0) + FROM invoices WHERE status IN ('sent','paid') AND created_at LIKE ?""", (f"{year}%",) ).fetchone()[0] @@ -705,10 +711,16 @@ def pay_invoice(invoice_id: int, data: PayBody, admin=Depends(require_admin)): raise HTTPException(404, "Rechnung nicht gefunden.") if row["status"] == "cancelled": raise HTTPException(400, "Stornierte Rechnung kann nicht als bezahlt markiert werden.") - conn.execute( - "UPDATE invoices SET status='paid', paid_at=?, paid_amount=? WHERE id=?", - (data.paid_at, data.paid_amount, invoice_id) - ) + if data.notes: + conn.execute( + "UPDATE invoices SET status='paid', paid_at=?, paid_amount=?, notes=? WHERE id=?", + (data.paid_at, data.paid_amount, data.notes, invoice_id) + ) + else: + conn.execute( + "UPDATE invoices SET status='paid', paid_at=?, paid_amount=? WHERE id=?", + (data.paid_at, data.paid_amount, invoice_id) + ) row = conn.execute("SELECT * FROM invoices WHERE id=?", (invoice_id,)).fetchone() return _row_to_dict(row) diff --git a/backend/static/js/app.js b/backend/static/js/app.js index b33aa1b..a4a7573 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 = '983'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '984'; // ← 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 81dc438..c3ae84f 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -3797,6 +3797,14 @@ window.Page_admin = (() => { ${_fmtEur(inv.amount_gross)} + ${inv.status === 'paid' && inv.paid_amount != null && Math.abs(inv.paid_amount - inv.amount_gross) >= 0.01 + ? `
+ erhalten: ${_fmtEur(inv.paid_amount)} + ${inv.paid_amount < inv.amount_gross + ? `-${_fmtEur(inv.amount_gross - inv.paid_amount)}` + : ''} +
` + : ''} ${_statusBadge(inv.status)} @@ -4091,10 +4099,13 @@ window.Page_admin = (() => {
- - Eingegangener Betrag (€) * +
+ `, footer: ` @@ -4103,18 +4114,51 @@ window.Page_admin = (() => { `, }); + // Differenz live anzeigen + const amtEl = document.getElementById(`${id}-amt`); + const diffEl = document.getElementById(`${id}-diff`); + const _checkDiff = () => { + const entered = parseFloat(amtEl?.value) || 0; + const diff = defaultAmount - entered; + if (Math.abs(diff) < 0.01) { diffEl.style.display = 'none'; return; } + diffEl.style.display = 'block'; + if (diff > 0) { + diffEl.innerHTML = `Differenz: -${diff.toFixed(2)} € weniger als fakturiert.
+ `; + } else { + diffEl.innerHTML = `Überzahlung: +${(-diff).toFixed(2)} € mehr eingegangen.`; + diffEl.style.background = '#f0fff8'; + diffEl.style.borderColor = '#34d399'; + diffEl.style.color = '#065f46'; + } + }; + amtEl?.addEventListener('input', _checkDiff); + document.getElementById(id)?.addEventListener('submit', async e => { e.preventDefault(); - const fd = new FormData(e.target); + const fd = new FormData(e.target); + const paidAmount = parseFloat(fd.get('paid_amount')); + const diff = defaultAmount - paidAmount; + const kulanz = diff > 0.01 && document.getElementById(`${id}-kulanz`)?.checked; + const submitBtn = document.querySelector(`button[form="${id}"]`); if (submitBtn) submitBtn.disabled = true; try { + const kulanzNote = kulanz + ? `Forderungsverlust/Kulanz: ${diff.toFixed(2)} EUR nicht eingegangen (${fd.get('paid_at')}). Als Kulanz abgeschrieben.` + : null; await API.post(`/admin/invoices/${invoiceId}/pay`, { paid_at: fd.get('paid_at'), - paid_amount: parseFloat(fd.get('paid_amount')), + paid_amount: paidAmount, + ...(kulanzNote ? { notes: kulanzNote } : {}), }); UI.modal.close(); - UI.toast.success('Rechnung als bezahlt markiert.'); + UI.toast.success(kulanz + ? `Bezahlt (${paidAmount.toFixed(2)} €) · ${diff.toFixed(2)} € als Kulanz notiert.` + : 'Rechnung als bezahlt markiert.'); reload(); } catch (err) { UI.toast.error(err.message || 'Fehler.'); diff --git a/backend/static/sw.js b/backend/static/sw.js index 6fb938c..4b9fc1f 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v983'; +const CACHE_VERSION = 'by-v984'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache