Compare commits
No commits in common. "ea655590f71e951bdcd64bd89b4f05f6e87ad613" and "0069454b24621ede854e8985a634c30e1cee7c80" have entirely different histories.
ea655590f7
...
0069454b24
10 changed files with 11 additions and 347 deletions
|
|
@ -2145,31 +2145,6 @@ def _migrate(conn_factory):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Spalte existiert bereits
|
pass # Spalte existiert bereits
|
||||||
|
|
||||||
# ---- Feature: is_active für Hunde (nach Abo-Downgrade) ----
|
|
||||||
try:
|
|
||||||
conn.execute("ALTER TABLE dogs ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1")
|
|
||||||
logger.info("Migration: dogs.is_active hinzugefügt.")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# ---- Feature: Subscription-Laufzeit & Kündigung ----
|
|
||||||
for col, typedef in [
|
|
||||||
("subscription_expires_at", "TEXT DEFAULT NULL"),
|
|
||||||
("subscription_cancelled_at", "TEXT DEFAULT NULL"),
|
|
||||||
("needs_dog_selection", "INTEGER DEFAULT 0"),
|
|
||||||
]:
|
|
||||||
try:
|
|
||||||
conn.execute(f"ALTER TABLE users ADD COLUMN {col} {typedef}")
|
|
||||||
logger.info(f"Migration: {col} hinzugefügt.")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# Bestehende TEXT-Werte für needs_dog_selection auf 0/1 normalisieren
|
|
||||||
try:
|
|
||||||
conn.execute("UPDATE users SET needs_dog_selection=0 WHERE needs_dog_selection='0' OR needs_dog_selection IS NULL")
|
|
||||||
conn.execute("UPDATE users SET needs_dog_selection=1 WHERE needs_dog_selection='1'")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# exercise_progress + training_plan_progress: dog_id ergänzen
|
# exercise_progress + training_plan_progress: dog_id ergänzen
|
||||||
existing_ep = [r[1] for r in conn.execute("PRAGMA table_info(exercise_progress)").fetchall()]
|
existing_ep = [r[1] for r in conn.execute("PRAGMA table_info(exercise_progress)").fetchall()]
|
||||||
if 'dog_id' not in existing_ep:
|
if 'dog_id' not in existing_ep:
|
||||||
|
|
|
||||||
|
|
@ -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 = "946" # muss mit APP_VER in app.js übereinstimmen
|
APP_VER = "944" # 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():
|
||||||
|
|
|
||||||
|
|
@ -1138,12 +1138,9 @@ async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)):
|
||||||
raise HTTPException(400, "Bereits erledigt.")
|
raise HTTPException(400, "Bereits erledigt.")
|
||||||
if req["tier"] not in _VALID_TIERS:
|
if req["tier"] not in _VALID_TIERS:
|
||||||
raise HTTPException(400, "Ungültiger Tier.")
|
raise HTTPException(400, "Ungültiger Tier.")
|
||||||
from datetime import timedelta
|
|
||||||
expires_at = (datetime.now(_TZ) + timedelta(days=365)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""UPDATE users SET subscription_tier=?, subscription_expires_at=?,
|
"UPDATE users SET subscription_tier=? WHERE id=?",
|
||||||
subscription_cancelled_at=NULL WHERE id=?""",
|
(req["tier"], req["user_id"])
|
||||||
(req["tier"], expires_at, req["user_id"])
|
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE upgrade_requests SET fulfilled_at=strftime('%Y-%m-%dT%H:%M:%fZ','now'), fulfilled_by=? WHERE id=?",
|
"UPDATE upgrade_requests SET fulfilled_at=strftime('%Y-%m-%dT%H:%M:%fZ','now'), fulfilled_by=? WHERE id=?",
|
||||||
|
|
|
||||||
|
|
@ -240,8 +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, subscription_tier,
|
preferred_theme, subscription_tier
|
||||||
subscription_expires_at, subscription_cancelled_at, needs_dog_selection
|
|
||||||
FROM users WHERE id=?""",
|
FROM users WHERE id=?""",
|
||||||
(user["id"],)
|
(user["id"],)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
@ -395,70 +394,3 @@ async def reset_password(data: ResetPasswordRequest, request: Request):
|
||||||
(hash_password(data.password), user["id"])
|
(hash_password(data.password), user["id"])
|
||||||
)
|
)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/subscription/cancel")
|
|
||||||
async def cancel_subscription(user=Depends(get_current_user)):
|
|
||||||
with db() as conn:
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT subscription_tier, subscription_expires_at, subscription_cancelled_at FROM users WHERE id=?",
|
|
||||||
(user["id"],)
|
|
||||||
).fetchone()
|
|
||||||
if not row or row["subscription_tier"] in ("standard", "standard_test"):
|
|
||||||
raise HTTPException(400, "Kein aktives Abo vorhanden.")
|
|
||||||
if row["subscription_cancelled_at"]:
|
|
||||||
raise HTTPException(400, "Abo ist bereits gekündigt.")
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE users SET subscription_cancelled_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id=?",
|
|
||||||
(user["id"],)
|
|
||||||
)
|
|
||||||
expires = row["subscription_expires_at"]
|
|
||||||
|
|
||||||
# Bestätigungsmail
|
|
||||||
try:
|
|
||||||
from mailer import send_email, email_html
|
|
||||||
import html as _html
|
|
||||||
tier_label = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}.get(row["subscription_tier"], row["subscription_tier"])
|
|
||||||
expires_fmt = expires[:10] if expires else "—"
|
|
||||||
body_html = f"""
|
|
||||||
<p>Hallo {_html.escape(user['name'])},</p>
|
|
||||||
<p>deine Kündigung für <strong>{tier_label}</strong> wurde bestätigt.</p>
|
|
||||||
<p>Dein Abo ist weiterhin aktiv bis zum <strong>{expires_fmt}</strong>.
|
|
||||||
Ab diesem Datum wirst du automatisch auf den kostenlosen Tarif gesetzt.</p>
|
|
||||||
<p>Deine Daten (Tagebuch, Gesundheit, Notizen) bleiben vollständig erhalten.
|
|
||||||
Wenn du mehrere Hunde hast, kannst du vor dem Ablauf einen als Haupthund festlegen.</p>
|
|
||||||
<p>Wir hoffen, dich bald wieder begrüßen zu dürfen!</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 {user['name']},\n\nKündigung bestätigt für {tier_label}.\n"
|
|
||||||
f"Aktiv bis: {expires_fmt}\n\nAlle Daten bleiben erhalten.\n\nViele Grüße\nRené")
|
|
||||||
await send_email(user["email"], f"Kündigung bestätigt — {tier_label}", html, plain)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {"ok": True, "expires_at": expires}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/subscription/select-dog")
|
|
||||||
async def select_primary_dog(body: dict, user=Depends(get_current_user)):
|
|
||||||
"""Nach Downgrade: Haupthund auswählen, Rest bleibt erhalten aber inaktiv."""
|
|
||||||
dog_id = body.get("dog_id")
|
|
||||||
if not dog_id:
|
|
||||||
raise HTTPException(400, "dog_id fehlt.")
|
|
||||||
with db() as conn:
|
|
||||||
dog = conn.execute(
|
|
||||||
"SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"])
|
|
||||||
).fetchone()
|
|
||||||
if not dog:
|
|
||||||
raise HTTPException(404, "Hund nicht gefunden.")
|
|
||||||
# Alle anderen Hunde deaktivieren
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE dogs SET is_active=0 WHERE user_id=? AND id!=?", (user["id"], dog_id)
|
|
||||||
)
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE dogs SET is_active=1 WHERE id=?", (dog_id,)
|
|
||||||
)
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE users SET needs_dog_selection=0 WHERE id=?", (user["id"],)
|
|
||||||
)
|
|
||||||
return {"ok": True}
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class DogUpdate(BaseModel):
|
||||||
async def list_dogs(user=Depends(get_current_user)):
|
async def list_dogs(user=Depends(get_current_user)):
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
own = conn.execute(
|
own = conn.execute(
|
||||||
"SELECT *, NULL AS shared_by, NULL AS share_role FROM dogs WHERE user_id=? AND (verstorben_am IS NULL) AND (is_active IS NULL OR is_active=1) ORDER BY id",
|
"SELECT *, NULL AS shared_by, NULL AS share_role FROM dogs WHERE user_id=? AND (verstorben_am IS NULL) ORDER BY id",
|
||||||
(user["id"],)
|
(user["id"],)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
shared = conn.execute(
|
shared = conn.execute(
|
||||||
|
|
|
||||||
|
|
@ -188,15 +188,8 @@ def start():
|
||||||
replace_existing=True,
|
replace_existing=True,
|
||||||
misfire_grace_time=3600,
|
misfire_grace_time=3600,
|
||||||
)
|
)
|
||||||
_scheduler.add_job(
|
|
||||||
_job_subscription_check,
|
|
||||||
CronTrigger(hour=3, minute=0),
|
|
||||||
id="subscription_check",
|
|
||||||
replace_existing=True,
|
|
||||||
misfire_grace_time=3600,
|
|
||||||
)
|
|
||||||
_scheduler.start()
|
_scheduler.start()
|
||||||
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00, Goldene-Gassi-Stunde 07:00, Jahrestags-Erinnerungen 09:00, Monatlicher-Rückblick 1. des Monats 10:00, Foto-Challenge Mo 08:00, Abo-Check 03:00. OSM-Cache: on-demand (kein Prewarm).")
|
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00, Goldene-Gassi-Stunde 07:00, Jahrestags-Erinnerungen 09:00, Monatlicher-Rückblick 1. des Monats 10:00, Foto-Challenge Mo 08:00. OSM-Cache: on-demand (kein Prewarm).")
|
||||||
|
|
||||||
|
|
||||||
def stop():
|
def stop():
|
||||||
|
|
@ -204,82 +197,6 @@ def stop():
|
||||||
logger.info("Scheduler gestoppt.")
|
logger.info("Scheduler gestoppt.")
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# JOB: Abo-Ablauf prüfen (täglich 03:00)
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
async def _job_subscription_check():
|
|
||||||
"""Abgelaufene Abos auf Standard setzen; Warnmails 30 und 7 Tage vorher."""
|
|
||||||
from database import db as _db
|
|
||||||
from mailer import send_email, email_html
|
|
||||||
import html as _html
|
|
||||||
now = datetime.now(_TZ)
|
|
||||||
today = now.date()
|
|
||||||
|
|
||||||
with _db() as conn:
|
|
||||||
users = conn.execute(
|
|
||||||
"""SELECT id, name, email, subscription_tier, subscription_expires_at
|
|
||||||
FROM users
|
|
||||||
WHERE subscription_tier IN ('pro','breeder')
|
|
||||||
AND subscription_expires_at IS NOT NULL"""
|
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
for u in users:
|
|
||||||
try:
|
|
||||||
expires = datetime.fromisoformat(u["subscription_expires_at"].replace('Z', '+00:00')).date()
|
|
||||||
days_left = (expires - today).days
|
|
||||||
tier_label = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}.get(u["subscription_tier"], u["subscription_tier"])
|
|
||||||
|
|
||||||
# Abgelaufen → auf Standard setzen
|
|
||||||
if days_left < 0:
|
|
||||||
with _db() as conn:
|
|
||||||
dog_count = conn.execute(
|
|
||||||
"SELECT COUNT(*) FROM dogs WHERE user_id=? AND is_active!=0", (u["id"],)
|
|
||||||
).fetchone()[0]
|
|
||||||
needs_sel = 1 if dog_count > 1 else 0
|
|
||||||
conn.execute(
|
|
||||||
"""UPDATE users SET subscription_tier='standard',
|
|
||||||
needs_dog_selection=? WHERE id=?""",
|
|
||||||
(needs_sel, u["id"])
|
|
||||||
)
|
|
||||||
logger.info(f"Abo abgelaufen: {u['email']} → standard (needs_dog_selection={needs_sel})")
|
|
||||||
body = f"""
|
|
||||||
<p>Hallo {_html.escape(u['name'])},</p>
|
|
||||||
<p>dein <strong>{tier_label}</strong>-Abo ist heute abgelaufen.
|
|
||||||
Dein Account wurde auf den kostenlosen Tarif gesetzt.</p>
|
|
||||||
<p>Deine Daten sind vollständig erhalten. Du kannst jederzeit wieder upgraden.</p>"""
|
|
||||||
if needs_sel:
|
|
||||||
body += "<p><strong>Wichtig:</strong> Du hattest mehrere Hunde. Öffne die App und wähle deinen Haupthund aus — alle anderen Profile bleiben gespeichert.</p>"
|
|
||||||
html = email_html(body, cta_url="https://banyaro.app", cta_label="Ban Yaro öffnen")
|
|
||||||
await send_email(u["email"], f"Dein {tier_label}-Abo ist abgelaufen", html,
|
|
||||||
f"Hallo {u['name']},\ndein {tier_label}-Abo ist abgelaufen. Daten bleiben erhalten.")
|
|
||||||
|
|
||||||
# 30 Tage Warnung
|
|
||||||
elif days_left == 30:
|
|
||||||
body = f"""
|
|
||||||
<p>Hallo {_html.escape(u['name'])},</p>
|
|
||||||
<p>dein <strong>{tier_label}</strong>-Abo läuft in <strong>30 Tagen</strong>
|
|
||||||
(am {expires.strftime('%d.%m.%Y')}) ab.</p>
|
|
||||||
<p>Um weiterzumachen, überweise einfach den Jahresbetrag und schreib uns kurz —
|
|
||||||
wir verlängern deinen Zugang sofort.</p>"""
|
|
||||||
html = email_html(body, cta_url="https://banyaro.app", cta_label="Abo verlängern")
|
|
||||||
await send_email(u["email"], f"Dein {tier_label}-Abo läuft in 30 Tagen ab", html,
|
|
||||||
f"Hallo {u['name']},\ndein {tier_label}-Abo läuft in 30 Tagen ab ({expires}).")
|
|
||||||
|
|
||||||
# 7 Tage Warnung
|
|
||||||
elif days_left == 7:
|
|
||||||
body = f"""
|
|
||||||
<p>Hallo {_html.escape(u['name'])},</p>
|
|
||||||
<p>dein <strong>{tier_label}</strong>-Abo läuft in <strong>7 Tagen</strong>
|
|
||||||
(am {expires.strftime('%d.%m.%Y')}) ab.</p>
|
|
||||||
<p>Jetzt verlängern und nahtlos weitermachen!</p>"""
|
|
||||||
html = email_html(body, cta_url="https://banyaro.app", cta_label="Abo verlängern")
|
|
||||||
await send_email(u["email"], f"Nur noch 7 Tage — {tier_label}-Abo läuft ab", html,
|
|
||||||
f"Hallo {u['name']},\nnur noch 7 Tage für dein {tier_label}-Abo.")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"subscription_check Fehler für {u['email']}: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# JOB: Gesundheits-Erinnerungen
|
# JOB: Gesundheits-Erinnerungen
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -127,12 +127,6 @@ const API = (() => {
|
||||||
upgradeRequest(tier, message) {
|
upgradeRequest(tier, message) {
|
||||||
return post('/auth/upgrade-request', { tier, message });
|
return post('/auth/upgrade-request', { tier, message });
|
||||||
},
|
},
|
||||||
cancelSubscription() {
|
|
||||||
return post('/auth/subscription/cancel', {});
|
|
||||||
},
|
|
||||||
selectPrimaryDog(dog_id) {
|
|
||||||
return post('/auth/subscription/select-dog', { dog_id });
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '946'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '944'; // ← 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
|
||||||
|
|
@ -567,11 +567,6 @@ const App = (() => {
|
||||||
navigate('onboarding');
|
navigate('onboarding');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abo abgelaufen mit mehreren Hunden → Haupthund auswählen (nur wenn explizit 1, nicht "0" string)
|
|
||||||
if (state.user.needs_dog_selection === 1 && state.dogs.length > 1) {
|
|
||||||
_showDogSelectionModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme aus DB-Profil übernehmen (überschreibt localStorage-Wert)
|
// Theme aus DB-Profil übernehmen (überschreibt localStorage-Wert)
|
||||||
_applyUserTheme(state.user);
|
_applyUserTheme(state.user);
|
||||||
|
|
||||||
|
|
@ -673,57 +668,6 @@ const App = (() => {
|
||||||
document.getElementById('meta-theme-color')?.setAttribute('content', isDark ? '#0f1623' : '#C4843A');
|
document.getElementById('meta-theme-color')?.setAttribute('content', isDark ? '#0f1623' : '#C4843A');
|
||||||
}
|
}
|
||||||
|
|
||||||
function _showDogSelectionModal() {
|
|
||||||
const dogs = state.dogs;
|
|
||||||
const optionHtml = dogs.map(d => `
|
|
||||||
<label style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3);
|
|
||||||
border-radius:var(--radius-md);border:1.5px solid var(--c-border);
|
|
||||||
cursor:pointer;margin-bottom:var(--space-2)">
|
|
||||||
<input type="radio" name="select-dog" value="${d.id}" style="width:18px;height:18px">
|
|
||||||
${d.foto_url
|
|
||||||
? `<img src="${UI.escape(d.foto_url)}" style="width:40px;height:40px;border-radius:50%;object-fit:cover">`
|
|
||||||
: `<div style="width:40px;height:40px;border-radius:50%;background:var(--c-border);display:flex;align-items:center;justify-content:center">🐕</div>`}
|
|
||||||
<div>
|
|
||||||
<div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(d.name)}</div>
|
|
||||||
${d.rasse ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted)">${UI.escape(d.rasse)}</div>` : ''}
|
|
||||||
</div>
|
|
||||||
</label>`).join('');
|
|
||||||
|
|
||||||
UI.modal.open({
|
|
||||||
title: 'Haupthund auswählen',
|
|
||||||
body: `
|
|
||||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
|
|
||||||
Dein Abo ist ausgelaufen. Wähle einen Haupthund für deinen kostenlosen Account.
|
|
||||||
Alle anderen Hunde-Profile bleiben vollständig gespeichert — du kannst sie nach
|
|
||||||
einem erneuten Upgrade wieder aktivieren.
|
|
||||||
</p>
|
|
||||||
<form id="dog-select-form">${optionHtml}</form>`,
|
|
||||||
footer: `
|
|
||||||
<button id="dog-select-confirm" class="btn btn-primary" style="width:100%">
|
|
||||||
Auswahl bestätigen
|
|
||||||
</button>`
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('dog-select-confirm')?.addEventListener('click', async () => {
|
|
||||||
const chosen = document.querySelector('[name="select-dog"]:checked')?.value;
|
|
||||||
if (!chosen) { UI.toast.warning('Bitte einen Hund auswählen.'); return; }
|
|
||||||
const btn = document.getElementById('dog-select-confirm');
|
|
||||||
btn.disabled = true; btn.textContent = '…';
|
|
||||||
try {
|
|
||||||
await API.auth.selectPrimaryDog(parseInt(chosen));
|
|
||||||
state.user.needs_dog_selection = 0;
|
|
||||||
state.activeDog = state.dogs.find(d => String(d.id) === chosen) || state.dogs[0];
|
|
||||||
localStorage.setItem('by_active_dog', String(state.activeDog.id));
|
|
||||||
UI.modal.close();
|
|
||||||
UI.toast.success('Haupthund festgelegt.');
|
|
||||||
_renderDogSwitcher();
|
|
||||||
} catch (e) {
|
|
||||||
btn.disabled = false; btn.textContent = 'Auswahl bestätigen';
|
|
||||||
UI.toast.error(e.message || 'Fehler.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _showAndroidBetaBanner() {
|
function _showAndroidBetaBanner() {
|
||||||
// Nur auf Android, nur einmalig, nur für eingeloggte Nutzer
|
// Nur auf Android, nur einmalig, nur für eingeloggte Nutzer
|
||||||
if (!/android/i.test(navigator.userAgent)) return;
|
if (!/android/i.test(navigator.userAgent)) return;
|
||||||
|
|
|
||||||
|
|
@ -104,43 +104,18 @@ window.Page_settings = (() => {
|
||||||
<span style="font-size:var(--text-xs);font-weight:400;opacity:.9">${price}</span>
|
<span style="font-size:var(--text-xs);font-weight:400;opacity:.9">${price}</span>
|
||||||
</button>`;
|
</button>`;
|
||||||
|
|
||||||
const expires = u.subscription_expires_at;
|
|
||||||
const cancelled = u.subscription_cancelled_at;
|
|
||||||
const expiresDate = expires ? new Date(expires).toLocaleDateString('de-DE', {day:'numeric',month:'long',year:'numeric'}) : null;
|
|
||||||
const isPaid = (isPro || isBreeder) && !tier.endsWith('_test') && !isAdmin;
|
|
||||||
|
|
||||||
const _expiryInfo = () => {
|
|
||||||
if (!isPaid || !expiresDate) return '';
|
|
||||||
const color = cancelled ? '#e65100' : 'var(--c-text-secondary)';
|
|
||||||
const text = cancelled
|
|
||||||
? `Gekündigt — läuft bis ${expiresDate}`
|
|
||||||
: `Aktiv bis ${expiresDate}`;
|
|
||||||
return `<div style="font-size:var(--text-xs);color:${color};margin-top:var(--space-1)">${text}</div>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _cancelBtn = () => {
|
|
||||||
if (!isPaid || cancelled) return '';
|
|
||||||
return `<button id="settings-cancel-sub-btn"
|
|
||||||
style="margin-top:var(--space-3);padding:var(--space-2) var(--space-3);
|
|
||||||
border-radius:var(--radius-md);border:1px solid var(--c-border);
|
|
||||||
background:transparent;color:var(--c-text-secondary);
|
|
||||||
font-size:var(--text-xs);cursor:pointer">
|
|
||||||
Abo kündigen
|
|
||||||
</button>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
let statusHtml = '';
|
let statusHtml = '';
|
||||||
let actionsHtml = '';
|
let actionsHtml = '';
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
statusHtml = _badge('Admin', '#6366f1');
|
statusHtml = _badge('Admin', '#6366f1');
|
||||||
} else if (isBreeder) {
|
} else if (isBreeder) {
|
||||||
statusHtml = _badge(cancelled ? 'Züchter (gekündigt)' : 'Züchter aktiv', '#C4843A');
|
statusHtml = _badge('Züchter aktiv', '#C4843A');
|
||||||
} else if (isPro) {
|
} else if (isPro) {
|
||||||
statusHtml = _badge(cancelled ? 'Pro (gekündigt)' : 'Pro aktiv', '#16a34a');
|
statusHtml = _badge('Pro aktiv', '#16a34a');
|
||||||
actionsHtml = `
|
actionsHtml = `
|
||||||
<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);flex-wrap:wrap">
|
<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);flex-wrap:wrap">
|
||||||
${!cancelled ? _upgradeBtn('settings-upgrade-breeder-btn','Züchter werden','49 €/Jahr','#C4843A') : ''}
|
${_upgradeBtn('settings-upgrade-breeder-btn','Züchter werden','49 €/Jahr','#C4843A')}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
statusHtml = _badge('Kostenlos', '#888');
|
statusHtml = _badge('Kostenlos', '#888');
|
||||||
|
|
@ -159,9 +134,7 @@ window.Page_settings = (() => {
|
||||||
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">Aktueller Tarif:</span>
|
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">Aktueller Tarif:</span>
|
||||||
${statusHtml}
|
${statusHtml}
|
||||||
</div>
|
</div>
|
||||||
${_expiryInfo()}
|
|
||||||
${actionsHtml}
|
${actionsHtml}
|
||||||
${_cancelBtn()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -367,71 +340,6 @@ window.Page_settings = (() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _showCancelModal() {
|
|
||||||
const u = _appState.user;
|
|
||||||
const tier = u?.subscription_tier || 'standard';
|
|
||||||
const label = { pro: 'Ban Yaro Pro', breeder: 'Züchter' }[tier] || tier;
|
|
||||||
const expires = u?.subscription_expires_at;
|
|
||||||
const expiresDate = expires
|
|
||||||
? new Date(expires).toLocaleDateString('de-DE', {day:'numeric',month:'long',year:'numeric'})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
UI.modal.open({
|
|
||||||
title: `${label} kündigen`,
|
|
||||||
body: `
|
|
||||||
<div style="padding:var(--space-2) 0">
|
|
||||||
${expiresDate ? `
|
|
||||||
<div style="padding:var(--space-3);border-radius:var(--radius-md);
|
|
||||||
background:rgba(234,88,12,.08);border:1px solid rgba(234,88,12,.2);
|
|
||||||
margin-bottom:var(--space-4);font-size:var(--text-sm)">
|
|
||||||
Dein Abo läuft noch bis <strong>${expiresDate}</strong> — du hast bis dahin vollen Zugriff.
|
|
||||||
</div>` : ''}
|
|
||||||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.6;
|
|
||||||
display:flex;flex-direction:column;gap:var(--space-2)">
|
|
||||||
<div>✓ Alle deine Daten (Tagebuch, Gesundheit, Notizen) bleiben vollständig erhalten</div>
|
|
||||||
<div>✓ Deine Hunde-Profile bleiben gespeichert</div>
|
|
||||||
<div>✓ Du kannst jederzeit wieder upgraden</div>
|
|
||||||
${_appState.dogs?.length > 1
|
|
||||||
? `<div style="color:var(--c-warning,#f59e0b)">⚠ Du hast mehrere Hunde — nach dem Ablauf wählst du einen als Haupthund</div>`
|
|
||||||
: ''}
|
|
||||||
</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>
|
|
||||||
<button id="cancel-sub-confirm-btn"
|
|
||||||
style="padding:var(--space-2) var(--space-4);border-radius:var(--radius-md);
|
|
||||||
border:none;cursor:pointer;background:var(--c-danger);color:#fff;
|
|
||||||
font-size:var(--text-sm);font-weight:600">
|
|
||||||
Jetzt kündigen
|
|
||||||
</button>`
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('cancel-sub-confirm-btn')?.addEventListener('click', async () => {
|
|
||||||
const btn = document.getElementById('cancel-sub-confirm-btn');
|
|
||||||
if (!btn) return;
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = '…';
|
|
||||||
try {
|
|
||||||
await API.auth.cancelSubscription();
|
|
||||||
// User-State aktualisieren
|
|
||||||
const fresh = await API.auth.me();
|
|
||||||
Object.assign(_appState.user, fresh);
|
|
||||||
UI.modal.close();
|
|
||||||
UI.toast.success('Kündigung bestätigt. Eine Bestätigungsmail wurde gesendet.');
|
|
||||||
_render();
|
|
||||||
} catch (e) {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Jetzt kündigen';
|
|
||||||
UI.toast.error(e.message || 'Fehler beim Kündigen.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// RENDER
|
// RENDER
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -1194,9 +1102,6 @@ window.Page_settings = (() => {
|
||||||
document.getElementById('settings-upgrade-breeder-btn')?.addEventListener('click', () => {
|
document.getElementById('settings-upgrade-breeder-btn')?.addEventListener('click', () => {
|
||||||
_showUpgradeModal('breeder');
|
_showUpgradeModal('breeder');
|
||||||
});
|
});
|
||||||
document.getElementById('settings-cancel-sub-btn')?.addEventListener('click', () => {
|
|
||||||
_showCancelModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v946';
|
const CACHE_VERSION = 'by-v944';
|
||||||
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