Feature: Abo & Tarif in Einstellungen — Upgrade-UI für Pro + Züchter (SW by-v919)

- /api/me gibt subscription_tier jetzt zurück (fehlte im SELECT)
- settings.js: "Pro kommt bald" durch echte Abo-Karte ersetzt
  - Zeigt aktuellen Tarif mit farbigem Badge (Kostenlos/Pro/Züchter/Admin)
  - Standard-Nutzer: zwei Upgrade-Buttons (Pro 29€/Jahr, Züchter 49€/Jahr)
  - Pro-Nutzer: Pro-Badge + optionaler Züchter-Upgrade
  - Züchter/Admin: Status-Badge, keine Upgrade-Buttons
- Upgrade-Modal: Features-Liste + ehrlicher Hinweis auf manuelle Freischaltung
  + mailto-Button mit vorausgefülltem Betreff und Account-E-Mail
- SW by-v919, APP_VER 919
This commit is contained in:
rene 2026-05-14 09:48:01 +02:00
parent eaa2e02e88
commit d61fd155c5
6 changed files with 136 additions and 15 deletions

View file

@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request):
raise _HE(404, "Nicht gefunden.") raise _HE(404, "Nicht gefunden.")
return _media_response(filepath) return _media_response(filepath)
APP_VER = "918" # muss mit APP_VER in app.js übereinstimmen APP_VER = "919" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json") @app.get("/.well-known/assetlinks.json")
async def assetlinks(): async def assetlinks():

View file

@ -240,7 +240,7 @@ async def me(user=Depends(get_current_user)):
profil_sichtbarkeit, avatar_url, created_at, profil_sichtbarkeit, avatar_url, created_at,
is_founder, is_partner, founder_number, is_founder_pending, is_founder, is_partner, founder_number, is_founder_pending,
notes_ki_enabled, gassi_stunde_push, notes_ki_enabled, gassi_stunde_push,
preferred_theme preferred_theme, subscription_tier
FROM users WHERE id=?""", FROM users WHERE id=?""",
(user["id"],) (user["id"],)
).fetchone() ).fetchone()

View file

@ -599,10 +599,10 @@
<div id="modal-container"></div> <div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features --> <!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=918"></script> <script src="/js/api.js?v=919"></script>
<script src="/js/ui.js?v=918"></script> <script src="/js/ui.js?v=919"></script>
<script src="/js/app.js?v=918"></script> <script src="/js/app.js?v=919"></script>
<script src="/js/worlds.js?v=918"></script> <script src="/js/worlds.js?v=919"></script>
<!-- Feature-Seiten werden lazy geladen --> <!-- Feature-Seiten werden lazy geladen -->

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. Router, State-Management, Navigation, Initialisierung.
============================================================ */ ============================================================ */
const APP_VER = '918'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VER = '919'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app'; const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen // Cache-Bust-Parameter nach Update-Reload sofort entfernen

View file

@ -77,6 +77,125 @@ window.Page_settings = (() => {
} }
} }
// ----------------------------------------------------------
// ABO & TARIF
// ----------------------------------------------------------
function _tierCard(u) {
const tier = u.subscription_tier || 'standard';
const rolle = u.rolle || 'user';
const isAdmin = rolle === 'admin' || rolle === 'moderator';
const isPro = ['pro','pro_test'].includes(tier);
const isBreeder = ['breeder','breeder_test'].includes(tier) || rolle === 'breeder';
const isStandard = !isAdmin && !isPro && !isBreeder;
const _badge = (label, color) =>
`<span style="display:inline-block;padding:2px 10px;border-radius:20px;
font-size:var(--text-xs);font-weight:700;letter-spacing:.03em;
background:${color};color:#fff">${label}</span>`;
const _upgradeBtn = (id, label, price, color) =>
`<button id="${id}"
style="flex:1;min-width:130px;padding:var(--space-3) var(--space-2);
border-radius:var(--radius-md);border:none;cursor:pointer;
background:${color};color:#fff;
font-size:var(--text-sm);font-weight:600;
display:flex;flex-direction:column;align-items:center;gap:2px">
<span>${label}</span>
<span style="font-size:var(--text-xs);font-weight:400;opacity:.9">${price}</span>
</button>`;
let statusHtml = '';
let actionsHtml = '';
if (isAdmin) {
statusHtml = _badge('Admin', '#6366f1');
} else if (isBreeder) {
statusHtml = _badge('Züchter aktiv', '#C4843A');
} else if (isPro) {
statusHtml = _badge('Pro aktiv', '#16a34a');
actionsHtml = `
<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);flex-wrap:wrap">
${_upgradeBtn('settings-upgrade-breeder-btn','Züchter werden','49 €/Jahr','#C4843A')}
</div>`;
} else {
statusHtml = _badge('Kostenlos', '#888');
actionsHtml = `
<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);flex-wrap:wrap">
${_upgradeBtn('settings-upgrade-pro-btn','Ban Yaro Pro','29 €/Jahr','#16a34a')}
${_upgradeBtn('settings-upgrade-breeder-btn','Züchter','49 €/Jahr','#C4843A')}
</div>`;
}
return `
<div class="card" style="margin-bottom:var(--space-4)">
<div class="by-card-section-header">Abo &amp; Tarif</div>
<div style="padding:var(--space-4)">
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">Aktueller Tarif:</span>
${statusHtml}
</div>
${actionsHtml}
</div>
</div>`;
}
function _showUpgradeModal(tier) {
const isPro = tier === 'pro';
const label = isPro ? 'Ban Yaro Pro' : 'Züchter';
const price = isPro ? '29 €/Jahr' : '49 €/Jahr';
const features = isPro
? ['Mehrere Hunde verwalten', 'Ernährungsbereich mit KI-Berater', 'Erweiterte Karten-Layer', 'Alle künftigen Pro-Features']
: ['Vollständige Züchter-Plattform', 'Warteliste, Läufigkeit & Trächtigkeit', 'Wurfverwaltung, Stammbaum, IK-Rechner', 'KI-Züchter-Assistent & Datenexport'];
const featureList = features.map(f =>
`<li style="padding:var(--space-1) 0;font-size:var(--text-sm)">✓ ${f}</li>`
).join('');
const subject = encodeURIComponent(`Upgrade auf ${label} — Ban Yaro`);
const body = encodeURIComponent(
`Hallo,\n\nich möchte meinen Account auf ${label} upgraden.\n\nMein Account: ${_appState.user?.email || ''}\n\nBitte schickt mir die Zahlungsinformationen.\n\nViele Grüße`
);
const mailHref = `mailto:hallo@banyaro.app?subject=${subject}&body=${body}`;
UI.modal.open({
title: `${label} freischalten`,
body: `
<div style="padding:var(--space-2) 0">
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-4)">
<div style="font-size:2rem;font-weight:800;color:var(--c-primary)">${price}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5">
Einmaliger Jahresbeitrag<br>Kündigung jederzeit möglich
</div>
</div>
<ul style="list-style:none;padding:0;margin:0 0 var(--space-4)">
${featureList}
</ul>
<div style="padding:var(--space-3);border-radius:var(--radius-md);
background:var(--c-surface-raised,rgba(0,0,0,.04));
font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.6">
Aktuell läuft die Freischaltung noch manuell. Schreib uns kurz eine E-Mail
wir schalten deinen Account innerhalb von 24 Stunden frei und schicken
dir die Bankverbindung.
</div>
</div>`,
footer: `
<button data-modal-close
style="padding:var(--space-2) var(--space-4);border-radius:var(--radius-md);
border:1.5px solid var(--c-border);background:transparent;
color:var(--c-text);font-size:var(--text-sm);cursor:pointer">
Abbrechen
</button>
<a href="${mailHref}"
style="display:inline-flex;align-items:center;gap:var(--space-2);
padding:var(--space-2) var(--space-4);
border-radius:var(--radius-md);border:none;cursor:pointer;
background:var(--c-primary);color:#fff;
font-size:var(--text-sm);font-weight:600;text-decoration:none">
E-Mail senden
</a>`
});
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// RENDER // RENDER
// ---------------------------------------------------------- // ----------------------------------------------------------
@ -276,13 +395,6 @@ window.Page_settings = (() => {
<span>Feedback geben</span> <span>Feedback geben</span>
<span style="margin-left:auto;color:var(--c-text-secondary)"></span> <span style="margin-left:auto;color:var(--c-text-secondary)"></span>
</div> </div>
${!_appState.user?.subscription_tier || _appState.user.subscription_tier === 'standard' || _appState.user.subscription_tier === 'standard_test' ? `
<div style="margin:var(--space-3) 0;padding:var(--space-3) var(--space-4);
background:rgba(196,132,58,0.1);border-radius:var(--radius-md);
font-size:var(--text-xs);color:var(--c-text-secondary)">
<strong>Ban Yaro Pro</strong> kommt bald mehr Features, mehrere Hunde.
</div>
` : ''}
<div style="padding:var(--space-3) var(--space-4);border-top:1px solid var(--c-border)"> <div style="padding:var(--space-3) var(--space-4);border-top:1px solid var(--c-border)">
<button id="settings-logout-btn" <button id="settings-logout-btn"
style="width:100%;display:flex;align-items:center;justify-content:center; style="width:100%;display:flex;align-items:center;justify-content:center;
@ -316,6 +428,8 @@ window.Page_settings = (() => {
</div> </div>
</div> </div>
${_tierCard(u)}
<div class="card" style="margin-bottom:var(--space-4)"> <div class="card" style="margin-bottom:var(--space-4)">
<div class="by-card-section-header"> <div class="by-card-section-header">
App-Einstellungen App-Einstellungen
@ -829,6 +943,13 @@ window.Page_settings = (() => {
} }
}); });
document.getElementById('settings-upgrade-pro-btn')?.addEventListener('click', () => {
_showUpgradeModal('pro');
});
document.getElementById('settings-upgrade-breeder-btn')?.addEventListener('click', () => {
_showUpgradeModal('breeder');
});
document.getElementById('settings-worlds-btn')?.addEventListener('click', () => { document.getElementById('settings-worlds-btn')?.addEventListener('click', () => {
if (window.Worlds?._openConfigModal) window.Worlds._openConfigModal(); if (window.Worlds?._openConfigModal) window.Worlds._openConfigModal();
else if (window.Worlds) window.Worlds.openConfig?.(); else if (window.Worlds) window.Worlds.openConfig?.();

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache Offline-Cache + Push Notifications + Tile-Cache
============================================================ */ ============================================================ */
const CACHE_VERSION = 'by-v918'; const CACHE_VERSION = 'by-v919';
const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache