Feature: Upgrade-Anfragen-System — User-Flow + Admin-Panel (SW by-v920)
- DB: upgrade_requests-Tabelle (user_id, tier, message, fulfilled_at)
- POST /api/upgrade-request: Anfrage speichern + Admin-Benachrichtigungsmail
- GET/POST /api/admin/upgrade-requests[/{id}/fulfill]: Admin-Endpunkte
— fulfill setzt subscription_tier + sendet Bestätigungsmail an User
- action-items: upgrades_pending zählt offene Anfragen → Badge im Admin
- Admin-Tab "Upgrades": Tabelle offener/erledigter Anfragen, Freischalten-Button
mit Confirm-Modal, automatischer Tier-Setzung und Bestätigungsmail
- Settings: Upgrade-Modal sendet echte API-Anfrage statt nur mailto
— doppelte Anfrage wird erkannt (already:true → Toast statt Fehler)
- api.js: API.auth.upgradeRequest(tier, message) hinzugefügt
- SW by-v920, APP_VER 920
This commit is contained in:
parent
d61fd155c5
commit
f6b37717b4
9 changed files with 268 additions and 27 deletions
|
|
@ -2341,6 +2341,24 @@ def _migrate(conn_factory):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# upgrade_requests: Abo-Upgrade-Anfragen von Nutzern
|
||||||
|
try:
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS upgrade_requests (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
tier TEXT NOT NULL,
|
||||||
|
message TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
||||||
|
fulfilled_at TEXT,
|
||||||
|
fulfilled_by INTEGER REFERENCES users(id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_upgrade_req_pending ON upgrade_requests(fulfilled_at, created_at DESC)")
|
||||||
|
logger.info("Migration: upgrade_requests bereit.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Migration upgrade_requests: {e}")
|
||||||
|
|
||||||
# route_dogs: bestehende Routen allen Hunden des Users zuweisen
|
# route_dogs: bestehende Routen allen Hunden des Users zuweisen
|
||||||
try:
|
try:
|
||||||
existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0]
|
existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0]
|
||||||
|
|
|
||||||
|
|
@ -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 = "919" # muss mit APP_VER in app.js übereinstimmen
|
APP_VER = "920" # 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():
|
||||||
|
|
|
||||||
|
|
@ -124,13 +124,17 @@ async def action_items(user=Depends(require_mod)):
|
||||||
users_today = conn.execute(
|
users_today = conn.execute(
|
||||||
"SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')"
|
"SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')"
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
|
upgrades_pending = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM upgrade_requests WHERE fulfilled_at IS NULL"
|
||||||
|
).fetchone()[0]
|
||||||
return {
|
return {
|
||||||
"jobs_pending": jobs,
|
"jobs_pending": jobs,
|
||||||
"breeder_pending": breeders,
|
"breeder_pending": breeders,
|
||||||
"reports_open": reports,
|
"reports_open": reports,
|
||||||
"fotos_pending": fotos,
|
"fotos_pending": fotos,
|
||||||
"poi_edits_pending": poi_edits,
|
"poi_edits_pending": poi_edits,
|
||||||
"users_today": users_today,
|
"users_today": users_today,
|
||||||
|
"upgrades_pending": upgrades_pending,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1091,3 +1095,66 @@ async def generate_media_previews(user=Depends(require_admin)):
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
return {"generated": generated, "skipped": skipped, "errors": errors}
|
return {"generated": generated, "skipped": skipped, "errors": errors}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/admin/upgrade-requests — offene Upgrade-Anfragen
|
||||||
|
# POST /api/admin/upgrade-requests/{id}/fulfill — Tier setzen + Mail
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/upgrade-requests")
|
||||||
|
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
|
||||||
|
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
|
||||||
|
LIMIT 100
|
||||||
|
""").fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/upgrade-requests/{req_id}/fulfill")
|
||||||
|
async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)):
|
||||||
|
with db() as conn:
|
||||||
|
req = conn.execute(
|
||||||
|
"SELECT r.*, u.name, u.email FROM upgrade_requests r JOIN users u ON u.id=r.user_id WHERE r.id=?",
|
||||||
|
(req_id,)
|
||||||
|
).fetchone()
|
||||||
|
if not req:
|
||||||
|
raise HTTPException(404, "Anfrage nicht gefunden.")
|
||||||
|
if req["fulfilled_at"]:
|
||||||
|
raise HTTPException(400, "Bereits erledigt.")
|
||||||
|
if req["tier"] not in _VALID_TIERS:
|
||||||
|
raise HTTPException(400, "Ungültiger Tier.")
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE users SET subscription_tier=? WHERE id=?",
|
||||||
|
(req["tier"], req["user_id"])
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE upgrade_requests SET fulfilled_at=strftime('%Y-%m-%dT%H:%M:%fZ','now'), fulfilled_by=? WHERE id=?",
|
||||||
|
(user["id"], req_id)
|
||||||
|
)
|
||||||
|
_audit(conn, user, "fulfill_upgrade", f"user:{req['user_id']}", f"tier={req['tier']}")
|
||||||
|
|
||||||
|
tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}
|
||||||
|
tier_label = tier_labels.get(req["tier"], req["tier"])
|
||||||
|
try:
|
||||||
|
from mailer import send_email, email_html
|
||||||
|
body_html = f"""
|
||||||
|
<p>Hallo {req['name']},</p>
|
||||||
|
<p>dein Account wurde soeben auf <strong>{tier_label}</strong> freigeschaltet.</p>
|
||||||
|
<p>Du kannst alle {tier_label}-Features ab sofort in der App nutzen.
|
||||||
|
Öffne Ban Yaro und lade die App einmal neu — dann ist dein neuer Tarif aktiv.</p>
|
||||||
|
<p>Vielen Dank für dein Vertrauen!</p>
|
||||||
|
<p>Viele Grüße<br>René & das Ban Yaro Team</p>"""
|
||||||
|
html = email_html(body_html, cta_url="https://banyaro.app", cta_label="Ban Yaro öffnen")
|
||||||
|
plain = (f"Hallo {req['name']},\n\ndein Account wurde auf {tier_label} freigeschaltet.\n"
|
||||||
|
f"Öffne Ban Yaro und lade die App neu.\n\nViele Grüße\nRené")
|
||||||
|
await send_email(req["email"], f"Dein {tier_label}-Zugang ist aktiv", html, plain)
|
||||||
|
except Exception as e:
|
||||||
|
import logging
|
||||||
|
logging.getLogger(__name__).warning(f"Bestätigungsmail fehlgeschlagen: {e}")
|
||||||
|
|
||||||
|
return {"ok": True, "tier": req["tier"], "user": req["name"]}
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,46 @@ async def forgot_password(data: ForgotPasswordRequest, request: Request):
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
class UpgradeRequestBody(BaseModel):
|
||||||
|
tier: str
|
||||||
|
message: Optional[str] = None
|
||||||
|
|
||||||
|
@router.post("/upgrade-request")
|
||||||
|
async def create_upgrade_request(data: UpgradeRequestBody, user=Depends(get_current_user)):
|
||||||
|
_VALID = {"pro", "breeder"}
|
||||||
|
if data.tier not in _VALID:
|
||||||
|
raise HTTPException(400, "Ungültiger Tarif.")
|
||||||
|
with db() as conn:
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id FROM upgrade_requests WHERE user_id=? AND tier=? AND fulfilled_at IS NULL",
|
||||||
|
(user["id"], data.tier)
|
||||||
|
).fetchone()
|
||||||
|
if existing:
|
||||||
|
return {"ok": True, "already": True}
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO upgrade_requests (user_id, tier, message) VALUES (?,?,?)",
|
||||||
|
(user["id"], data.tier, data.message or None)
|
||||||
|
)
|
||||||
|
email = conn.execute("SELECT email FROM users WHERE id=?", (user["id"],)).fetchone()["email"]
|
||||||
|
|
||||||
|
tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}
|
||||||
|
tier_label = tier_labels[data.tier]
|
||||||
|
admin_email = os.getenv("ADMIN_EMAIL", "")
|
||||||
|
if admin_email:
|
||||||
|
try:
|
||||||
|
from routes.outreach import _send_smtp
|
||||||
|
subject = f"[Ban Yaro] Upgrade-Anfrage: {tier_label} — {user['name']}"
|
||||||
|
body = (f"Neue Upgrade-Anfrage:\n\n"
|
||||||
|
f"Nutzer: {user['name']} ({email})\n"
|
||||||
|
f"Tarif: {tier_label}\n"
|
||||||
|
f"Nachricht: {data.message or '—'}\n\n"
|
||||||
|
f"Admin-Panel: https://banyaro.app/#admin")
|
||||||
|
_send_smtp(admin_email, subject, body, "support")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/reset-password")
|
@router.post("/reset-password")
|
||||||
async def reset_password(data: ResetPasswordRequest, request: Request):
|
async def reset_password(data: ResetPasswordRequest, request: Request):
|
||||||
rl_check(request, max_requests=5, window_seconds=3600, key="reset_pw")
|
rl_check(request, max_requests=5, window_seconds=3600, key="reset_pw")
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,9 @@ const API = (() => {
|
||||||
return get('/auth/me');
|
return get('/auth/me');
|
||||||
},
|
},
|
||||||
referral: () => get('/auth/referral'),
|
referral: () => get('/auth/referral'),
|
||||||
|
upgradeRequest(tier, message) {
|
||||||
|
return post('/auth/upgrade-request', { tier, message });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '919'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '920'; // ← 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
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ window.Page_admin = (() => {
|
||||||
{ id: 'hilfe', label: 'Hilfe/FAQ', icon: 'question' },
|
{ id: 'hilfe', label: 'Hilfe/FAQ', icon: 'question' },
|
||||||
{ id: 'uebungen_admin', label: 'Übungen', icon: 'barbell' },
|
{ id: 'uebungen_admin', label: 'Übungen', icon: 'barbell' },
|
||||||
{ id: 'referrals', label: 'Referrals', icon: 'share-network' },
|
{ id: 'referrals', label: 'Referrals', icon: 'share-network' },
|
||||||
|
{ id: 'upgrades', label: 'Upgrades', icon: 'crown-simple' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
@ -90,6 +91,7 @@ window.Page_admin = (() => {
|
||||||
try { d = await API.get('/admin/action-items'); } catch { return; }
|
try { d = await API.get('/admin/action-items'); } catch { return; }
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
|
{ key: 'upgrades_pending', label: 'Upgrade-Anfragen', tab: 'upgrades', icon: 'crown-simple' },
|
||||||
{ key: 'jobs_pending', label: 'Bewerbungen', tab: 'bewerbungen', icon: 'user-plus' },
|
{ key: 'jobs_pending', label: 'Bewerbungen', tab: 'bewerbungen', icon: 'user-plus' },
|
||||||
{ key: 'breeder_pending', label: 'Züchter-Anträge', tab: 'zuchter', icon: 'certificate' },
|
{ key: 'breeder_pending', label: 'Züchter-Anträge', tab: 'zuchter', icon: 'certificate' },
|
||||||
{ key: 'reports_open', label: 'Meldungen', tab: 'moderation', icon: 'warning' },
|
{ key: 'reports_open', label: 'Meldungen', tab: 'moderation', icon: 'warning' },
|
||||||
|
|
@ -163,6 +165,7 @@ window.Page_admin = (() => {
|
||||||
case 'hilfe': await _renderHilfe(el); break;
|
case 'hilfe': await _renderHilfe(el); break;
|
||||||
case 'uebungen_admin': await _renderUebungenAdmin(el); break;
|
case 'uebungen_admin': await _renderUebungenAdmin(el); break;
|
||||||
case 'referrals': await _renderReferrals(el); break;
|
case 'referrals': await _renderReferrals(el); break;
|
||||||
|
case 'upgrades': await _renderUpgrades(el); break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
|
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
|
||||||
|
|
@ -3419,6 +3422,104 @@ window.Page_admin = (() => {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TAB: UPGRADES
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
async function _renderUpgrades(el) {
|
||||||
|
const rows = await API.get('/admin/upgrade-requests');
|
||||||
|
|
||||||
|
const tierBadge = t => {
|
||||||
|
const cfg = { pro: ['Pro', '#16a34a'], breeder: ['Züchter', '#C4843A'] };
|
||||||
|
const [label, color] = cfg[t] || [t, '#888'];
|
||||||
|
return `<span style="display:inline-block;padding:1px 8px;border-radius:999px;
|
||||||
|
font-size:11px;font-weight:700;background:${color};color:#fff">${label}</span>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pending = rows.filter(r => !r.fulfilled_at);
|
||||||
|
const done = rows.filter(r => r.fulfilled_at);
|
||||||
|
|
||||||
|
const _row = (r, showBtn) => `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3)">${_esc(r.name)}<br>
|
||||||
|
<span style="font-size:var(--text-xs);color:var(--c-text-muted)">${_esc(r.email)}</span></td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3)">${tierBadge(r.tier)}</td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||||
|
${r.message ? _esc(r.message) : '—'}</td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||||
|
${r.created_at?.slice(0,10) || ''}</td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3)">
|
||||||
|
${showBtn
|
||||||
|
? `<button class="btn btn-sm adm-fulfill-btn" data-id="${r.id}" data-name="${_esc(r.name)}" data-tier="${r.tier}"
|
||||||
|
style="background:#16a34a;color:#fff;border:none;padding:4px 12px;
|
||||||
|
border-radius:var(--radius-md);cursor:pointer;font-size:var(--text-xs);font-weight:600">
|
||||||
|
Freischalten
|
||||||
|
</button>`
|
||||||
|
: `<span style="font-size:var(--text-xs);color:var(--c-success)">✓ ${r.fulfilled_at?.slice(0,10)}</span>`}
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
|
||||||
|
const thead = `<thead><tr>
|
||||||
|
${['Nutzer','Tarif','Nachricht','Datum','Aktion'].map(h =>
|
||||||
|
`<th style="padding:var(--space-2) var(--space-3);text-align:left;font-size:var(--text-xs);
|
||||||
|
color:var(--c-text-muted);font-weight:600;border-bottom:1px solid var(--c-border)">${h}</th>`
|
||||||
|
).join('')}</tr></thead>`;
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="card adm-table-card" style="margin-bottom:var(--space-4)">
|
||||||
|
<div class="by-card-section-header">Offene Anfragen (${pending.length})</div>
|
||||||
|
<div class="adm-table-scroll">
|
||||||
|
<table class="adm-table" style="width:100%;border-collapse:collapse">
|
||||||
|
${thead}
|
||||||
|
<tbody>
|
||||||
|
${pending.length
|
||||||
|
? pending.map(r => _row(r, true)).join('')
|
||||||
|
: `<tr><td colspan="5" style="padding:var(--space-4);text-align:center;color:var(--c-text-muted)">
|
||||||
|
Keine offenen Anfragen
|
||||||
|
</td></tr>`}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${done.length ? `
|
||||||
|
<div class="card adm-table-card">
|
||||||
|
<div class="by-card-section-header">Erledigt (${done.length})</div>
|
||||||
|
<div class="adm-table-scroll">
|
||||||
|
<table class="adm-table" style="width:100%;border-collapse:collapse">
|
||||||
|
${thead}
|
||||||
|
<tbody>${done.map(r => _row(r, false)).join('')}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>` : ''}`;
|
||||||
|
|
||||||
|
el.querySelectorAll('.adm-fulfill-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const { id, name, tier } = btn.dataset;
|
||||||
|
const tierLabel = { pro: 'Pro', breeder: 'Züchter' }[tier] || tier;
|
||||||
|
const ok = await UI.modal.confirm({
|
||||||
|
title: `${name} auf ${tierLabel} freischalten?`,
|
||||||
|
body: `<p style="font-size:var(--text-sm)">
|
||||||
|
Der Account wird auf <strong>${tierLabel}</strong> gesetzt und
|
||||||
|
eine Bestätigungsmail gesendet.
|
||||||
|
</p>`,
|
||||||
|
confirmLabel: 'Freischalten',
|
||||||
|
danger: false,
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '…';
|
||||||
|
try {
|
||||||
|
const res = await API.post(`/admin/upgrade-requests/${id}/fulfill`);
|
||||||
|
UI.toast.success(`${res.user} wurde auf ${tierLabel} freigeschaltet.`);
|
||||||
|
_renderTab();
|
||||||
|
} catch (e) {
|
||||||
|
UI.toast.error(e.message);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Freischalten';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { init, refresh, onDogChange };
|
return { init, refresh, onDogChange };
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,7 @@ window.Page_settings = (() => {
|
||||||
const isPro = tier === 'pro';
|
const isPro = tier === 'pro';
|
||||||
const label = isPro ? 'Ban Yaro Pro' : 'Züchter';
|
const label = isPro ? 'Ban Yaro Pro' : 'Züchter';
|
||||||
const price = isPro ? '29 €/Jahr' : '49 €/Jahr';
|
const price = isPro ? '29 €/Jahr' : '49 €/Jahr';
|
||||||
|
const color = isPro ? '#16a34a' : '#C4843A';
|
||||||
const features = isPro
|
const features = isPro
|
||||||
? ['Mehrere Hunde verwalten', 'Ernährungsbereich mit KI-Berater', 'Erweiterte Karten-Layer', 'Alle künftigen Pro-Features']
|
? ['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'];
|
: ['Vollständige Züchter-Plattform', 'Warteliste, Läufigkeit & Trächtigkeit', 'Wurfverwaltung, Stammbaum, IK-Rechner', 'KI-Züchter-Assistent & Datenexport'];
|
||||||
|
|
@ -151,18 +152,12 @@ window.Page_settings = (() => {
|
||||||
`<li style="padding:var(--space-1) 0;font-size:var(--text-sm)">✓ ${f}</li>`
|
`<li style="padding:var(--space-1) 0;font-size:var(--text-sm)">✓ ${f}</li>`
|
||||||
).join('');
|
).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({
|
UI.modal.open({
|
||||||
title: `${label} freischalten`,
|
title: `${label} freischalten`,
|
||||||
body: `
|
body: `
|
||||||
<div style="padding:var(--space-2) 0">
|
<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="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:2rem;font-weight:800;color:${color}">${price}</div>
|
||||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5">
|
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5">
|
||||||
Einmaliger Jahresbeitrag<br>Kündigung jederzeit möglich
|
Einmaliger Jahresbeitrag<br>Kündigung jederzeit möglich
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -173,9 +168,8 @@ window.Page_settings = (() => {
|
||||||
<div style="padding:var(--space-3);border-radius:var(--radius-md);
|
<div style="padding:var(--space-3);border-radius:var(--radius-md);
|
||||||
background:var(--c-surface-raised,rgba(0,0,0,.04));
|
background:var(--c-surface-raised,rgba(0,0,0,.04));
|
||||||
font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.6">
|
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 manuell frei — innerhalb von 24 Stunden.
|
||||||
wir schalten deinen Account innerhalb von 24 Stunden frei und schicken
|
Wir melden uns mit den Zahlungsdetails per E-Mail.
|
||||||
dir die Bankverbindung.
|
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
footer: `
|
footer: `
|
||||||
|
|
@ -185,14 +179,32 @@ window.Page_settings = (() => {
|
||||||
color:var(--c-text);font-size:var(--text-sm);cursor:pointer">
|
color:var(--c-text);font-size:var(--text-sm);cursor:pointer">
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<a href="${mailHref}"
|
<button id="upgrade-request-send-btn"
|
||||||
style="display:inline-flex;align-items:center;gap:var(--space-2);
|
style="padding:var(--space-2) var(--space-4);border-radius:var(--radius-md);
|
||||||
padding:var(--space-2) var(--space-4);
|
border:none;cursor:pointer;background:${color};color:#fff;
|
||||||
border-radius:var(--radius-md);border:none;cursor:pointer;
|
font-size:var(--text-sm);font-weight:600">
|
||||||
background:var(--c-primary);color:#fff;
|
Anfrage senden
|
||||||
font-size:var(--text-sm);font-weight:600;text-decoration:none">
|
</button>`
|
||||||
E-Mail senden
|
});
|
||||||
</a>`
|
|
||||||
|
document.getElementById('upgrade-request-send-btn')?.addEventListener('click', async () => {
|
||||||
|
const btn = document.getElementById('upgrade-request-send-btn');
|
||||||
|
if (!btn) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Wird gesendet…';
|
||||||
|
try {
|
||||||
|
const res = await API.auth.upgradeRequest(tier);
|
||||||
|
UI.modal.close();
|
||||||
|
if (res.already) {
|
||||||
|
UI.toast.info('Deine Anfrage liegt bereits vor — wir melden uns bald.');
|
||||||
|
} else {
|
||||||
|
UI.toast.success('Anfrage gesendet! Wir melden uns per E-Mail.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Anfrage senden';
|
||||||
|
UI.toast.error(e.message || 'Fehler beim Senden.');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v919';
|
const CACHE_VERSION = 'by-v920';
|
||||||
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue