From 0a466ef6ce77a57cd5e42adfed1f0c8519b607db Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 15 May 2026 10:59:12 +0200 Subject: [PATCH] =?UTF-8?q?Feat:=20Rechnungsadresse=20=E2=80=94=20Profil,?= =?UTF-8?q?=20Upgrade-Modal=20Hinweis,=20Rechnung-erstellen-Button=20in=20?= =?UTF-8?q?Upgrade-Cards=20(SW=20by-v967)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/database.py | 6 +++ backend/main.py | 2 +- backend/routes/admin.py | 2 +- backend/routes/auth.py | 3 +- backend/routes/profile.py | 3 +- backend/static/js/app.js | 2 +- backend/static/js/pages/admin.js | 75 +++++++++++++++++++++++------ backend/static/js/pages/settings.js | 14 ++++++ backend/static/sw.js | 2 +- 9 files changed, 89 insertions(+), 20 deletions(-) diff --git a/backend/database.py b/backend/database.py index ec362bc..8ba722f 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2444,6 +2444,12 @@ def _migrate(conn_factory): except Exception as e: logger.warning(f"Migration invoices: {e}") + try: + conn.execute("ALTER TABLE users ADD COLUMN billing_address TEXT") + logger.info("Migration: billing_address bereit.") + except Exception: + pass + def _seed_help_articles(conn): """Befüllt help_articles mit Starter-FAQs — nur wenn die Tabelle noch leer ist.""" diff --git a/backend/main.py b/backend/main.py index 3d79bb8..853e8a4 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 = "966" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "967" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/admin.py b/backend/routes/admin.py index d12f9eb..8eb07ba 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -1137,7 +1137,7 @@ async def list_upgrade_requests(user=Depends(require_admin)): with db() as conn: rows = conn.execute(""" SELECT r.id, r.user_id, r.tier, r.message, r.created_at, r.fulfilled_at, - u.name, u.email + u.name, u.email, u.billing_address FROM upgrade_requests r JOIN users u ON u.id = r.user_id ORDER BY r.fulfilled_at IS NOT NULL, r.created_at DESC diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 895c94c..46e6330 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -241,7 +241,8 @@ async def me(user=Depends(get_current_user)): is_founder, is_partner, founder_number, is_founder_pending, notes_ki_enabled, gassi_stunde_push, preferred_theme, subscription_tier, - subscription_expires_at, subscription_cancelled_at, needs_dog_selection + subscription_expires_at, subscription_cancelled_at, needs_dog_selection, + billing_address FROM users WHERE id=?""", (user["id"],) ).fetchone() diff --git a/backend/routes/profile.py b/backend/routes/profile.py index a6f3a5b..2dfe098 100644 --- a/backend/routes/profile.py +++ b/backend/routes/profile.py @@ -28,6 +28,7 @@ class ProfileUpdate(BaseModel): notes_ki_enabled: Optional[int] = None gassi_stunde_push: Optional[int] = None preferred_theme: Optional[str] = None + billing_address: Optional[str] = None def _load_user(user_id: int) -> dict: @@ -35,7 +36,7 @@ def _load_user(user_id: int) -> dict: row = conn.execute( """SELECT id, name, real_name, email, rolle, is_premium, email_verified, bio, wohnort, erfahrung, social_link, - profil_sichtbarkeit, avatar_url, created_at + profil_sichtbarkeit, avatar_url, created_at, billing_address FROM users WHERE id=?""", (user_id,) ).fetchone() diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 30f3537..b972a93 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 = '966'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '967'; // ← 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 c519e48..fed5b2b 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -3540,12 +3540,22 @@ window.Page_admin = (() => { ` : ''} - +
+ + +
`; // Erledigte als kompakte Tabellenzeilen @@ -3610,6 +3620,31 @@ window.Page_admin = (() => { } }); }); + + // "Rechnung erstellen" — öffnet Invoice-Modal mit vorbefüllten Nutzerdaten + const TIER_ITEMS = { + pro: { description: 'Ban Yaro Pro Jahresabo', unit_price: 29.00 }, + breeder: { description: 'Ban Yaro Züchter Jahresabo', unit_price: 49.00 }, + }; + const _year = new Date().getFullYear(); + const _period = `01.01.${_year} – 31.12.${_year}`; + + el.querySelectorAll('.adm-invoice-btn').forEach(btn => { + btn.addEventListener('click', () => { + const { name, email, tier, address } = btn.dataset; + const tierItem = TIER_ITEMS[tier] || { description: 'Ban Yaro Abo', unit_price: 0 }; + _openNeueRechnungModal(() => { + _tab = 'rechnungen'; + _renderTab(); + }, { + recipient_name: name, + recipient_email: email, + recipient_address: address || '', + service_period: _period, + items: [{ description: tierItem.description, quantity: 1, unit_price: tierItem.unit_price }], + }); + }); + }); } // ------------------------------------------------------------------ @@ -3789,8 +3824,9 @@ window.Page_admin = (() => { }); } - function _openNeueRechnungModal(reload) { + function _openNeueRechnungModal(reload, prefill = null) { const id = `inv-new-${Date.now()}`; + const p = prefill || {}; UI.modal.open({ title: `${UI.icon('receipt')} Neue Rechnung erstellen`, @@ -3801,25 +3837,32 @@ window.Page_admin = (() => {
- +
- +
- + + style="resize:vertical;font-family:inherit">${_esc(p.recipient_address || '')}
+ placeholder="01.01.2026 – 31.12.2026" + value="${_esc(p.service_period || '')}">
@@ -3915,8 +3958,12 @@ window.Page_admin = (() => { `; } - // Erste Position hinzufügen - _addItem('Ban Yaro Pro Jahresabo', 1, 29.00); + // Erste Position — aus Prefill oder Standard + if (p.items && p.items.length) { + p.items.forEach(it => _addItem(it.description, it.quantity ?? 1, it.unit_price ?? 0)); + } else { + _addItem('Ban Yaro Pro Jahresabo', 1, 29.00); + } // Weitere Position document.getElementById(`${id}-add-item`)?.addEventListener('click', () => _addItem()); diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 2d23a96..3cc8ac9 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -307,6 +307,12 @@ window.Page_settings = (() => { Wir schalten deinen Account manuell frei — innerhalb von 24 Stunden. Wir melden uns mit den Zahlungsdetails per E-Mail. + ${!_appState.user?.billing_address ? ` +
+ 💡 Tipp: Trag deine Rechnungsadresse im Profil ein — dann können wir die Rechnung vollständig ausstellen. +
` : ''} ${breederForm} `, footer: ` @@ -1135,6 +1141,13 @@ window.Page_settings = (() => { value="${_esc(u.social_link || '')}" style="${inputStyle}"> +
+ +
Wird auf Rechnungen gedruckt. Straße in Zeile 1, PLZ + Ort in Zeile 2.
+ +
@@ -1161,6 +1174,7 @@ window.Page_settings = (() => { erfahrung: fd.erfahrung || '', social_link: fd.social_link || '', profil_sichtbarkeit: fd.profil_sichtbarkeit || 'public', + billing_address: fd.billing_address || '', }); Object.assign(_appState.user, updated); UI.modal.close?.(); diff --git a/backend/static/sw.js b/backend/static/sw.js index 2293f58..6e8b3b5 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-v966'; +const CACHE_VERSION = 'by-v967'; 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