Refactor: Züchter-Antrag in Upgrade-Flow integriert (SW by-v925)

- /breeder/apply: Dokument jetzt optional (File(None)), kann per Mail nachgereicht werden
- _showUpgradeModal('breeder'): enthält jetzt Zwinger-Formular (Zwingername*, Rasse*,
  Verein, Stadt, VDH-Checkbox, optionales Dokument)
  → sendet /breeder/apply + /auth/upgrade-request in einem Schritt
- Züchter-Profil-Karte in Settings: 'Züchter werden'-Button entfernt
  → für neue User ohne Antrag wird die Card vollständig ausgeblendet
  → 'Neu beantragen' bei Ablehnung öffnet jetzt _showUpgradeModal('breeder')
- Verifizierte Züchter: Card unverändert (Profil, Edit, KI-Settings)
This commit is contained in:
rene 2026-05-14 11:17:49 +02:00
parent a27b8ea5b4
commit 4332b1195e
5 changed files with 118 additions and 38 deletions

View file

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

View file

@ -87,7 +87,7 @@ async def breeder_apply(
stadt: str = Form(...),
website: str = Form(""),
beschreibung: str = Form(""),
dokument: UploadFile = File(...),
dokument: UploadFile = File(None),
user=Depends(get_current_user),
):
with db() as conn:
@ -103,28 +103,27 @@ async def breeder_apply(
if row["breeder_status"] == "pending":
raise HTTPException(400, "Du hast bereits einen offenen Antrag.")
# Dokument validieren und speichern
data = await dokument.read()
if len(data) > 10 * 1024 * 1024:
raise HTTPException(400, "Dokument zu groß (max. 10 MB).")
ext = os.path.splitext(dokument.filename or "")[1].lower()
if ext not in (".pdf", ".jpg", ".jpeg", ".png", ".webp"):
raise HTTPException(400, "Nur PDF, JPG, PNG oder WebP erlaubt.")
user_doc_dir = os.path.join(BREEDER_DOCS_DIR, str(user["id"]))
os.makedirs(user_doc_dir, exist_ok=True)
filename = f"antrag_{datetime.now(_TZ).strftime('%Y%m%d_%H%M%S')}{ext}"
filepath = os.path.join(user_doc_dir, filename)
with open(filepath, "wb") as f:
f.write(data)
# Dokument optional speichern
filepath = None
if dokument and dokument.filename:
data = await dokument.read()
if len(data) > 10 * 1024 * 1024:
raise HTTPException(400, "Dokument zu groß (max. 10 MB).")
ext = os.path.splitext(dokument.filename)[1].lower()
if ext not in (".pdf", ".jpg", ".jpeg", ".png", ".webp"):
raise HTTPException(400, "Nur PDF, JPG, PNG oder WebP erlaubt.")
user_doc_dir = os.path.join(BREEDER_DOCS_DIR, str(user["id"]))
os.makedirs(user_doc_dir, exist_ok=True)
filename = f"antrag_{datetime.now(_TZ).strftime('%Y%m%d_%H%M%S')}{ext}"
filepath = os.path.join(user_doc_dir, filename)
with open(filepath, "wb") as f:
f.write(data)
with db() as conn:
conn.execute(
"UPDATE users SET breeder_status='pending' WHERE id=?",
(user["id"],)
)
# Profil-Entwurf anlegen (oder überschreiben wenn rejected)
conn.execute(
"INSERT INTO breeder_profiles (user_id, zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung) "
"VALUES (?,?,?,?,?,?,?,?) "
@ -135,10 +134,11 @@ async def breeder_apply(
"verified_at=NULL",
(user["id"], zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung)
)
conn.execute(
"INSERT INTO breeder_documents (user_id, dokument_typ, file_path) VALUES (?,?,?)",
(user["id"], "antrag", filepath)
)
if filepath:
conn.execute(
"INSERT INTO breeder_documents (user_id, dokument_typ, file_path) VALUES (?,?,?)",
(user["id"], "antrag", filepath)
)
# Admin benachrichtigen
admin_body = f"""

View file

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

View file

@ -152,6 +152,61 @@ window.Page_settings = (() => {
`<li style="padding:var(--space-1) 0;font-size:var(--text-sm)">✓ ${f}</li>`
).join('');
const inputStyle = `width:100%;box-sizing:border-box;padding:var(--space-2) var(--space-3);
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
font-size:var(--text-sm);font-family:inherit;background:var(--c-surface);color:var(--c-text)`;
const breederForm = isPro ? '' : `
<div style="margin-top:var(--space-4);padding-top:var(--space-4);border-top:1px solid var(--c-border)">
<div style="font-size:var(--text-xs);font-weight:700;color:var(--c-text-muted);
text-transform:uppercase;letter-spacing:.05em;margin-bottom:var(--space-3)">
Dein Zwinger
</div>
<form id="breeder-upgrade-form" style="display:flex;flex-direction:column;gap:var(--space-3)">
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:4px">
Zwingername <span style="color:var(--c-danger)">*</span>
</label>
<input name="zwingername" type="text" maxlength="100" required
placeholder="z. B. vom Sonnenfeld" style="${inputStyle}">
</div>
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:4px">
Rasse <span style="color:var(--c-danger)">*</span>
</label>
<input name="rasse_text" type="text" maxlength="100" required
placeholder="z. B. Labrador Retriever" style="${inputStyle}">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)">
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:4px">Zuchtverein</label>
<input name="verein" type="text" maxlength="100"
placeholder="z. B. VDH, BCD" style="${inputStyle}">
</div>
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:4px">Stadt</label>
<input name="stadt" type="text" maxlength="80"
placeholder="z. B. München" style="${inputStyle}">
</div>
</div>
<div style="display:flex;align-items:center;gap:var(--space-2)">
<input name="vdh_mitglied" type="checkbox" id="upg-breeder-vdh"
style="width:16px;height:16px;cursor:pointer;flex-shrink:0">
<label for="upg-breeder-vdh" style="font-size:var(--text-sm);cursor:pointer">VDH-Mitglied</label>
</div>
<div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:4px">
Dokument hochladen <span style="font-weight:400;color:var(--c-text-muted)">(optional)</span>
</label>
<input name="dokument" type="file" accept=".pdf,.jpg,.jpeg,.png,.webp"
style="font-size:var(--text-sm);width:100%;box-sizing:border-box">
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:4px">
Zuchtbuch, Vereinsausweis o.ä. kann auch per E-Mail nachgereicht werden
</div>
</div>
</form>
</div>`;
UI.modal.open({
title: `${label} freischalten`,
body: `
@ -162,7 +217,7 @@ window.Page_settings = (() => {
Einmaliger Jahresbeitrag<br>Kündigung jederzeit möglich
</div>
</div>
<ul style="list-style:none;padding:0;margin:0 0 var(--space-4)">
<ul style="list-style:none;padding:0;margin:0 0 var(--space-3)">
${featureList}
</ul>
<div style="padding:var(--space-3);border-radius:var(--radius-md);
@ -171,6 +226,7 @@ window.Page_settings = (() => {
Wir schalten deinen Account manuell frei innerhalb von 24 Stunden.
Wir melden uns mit den Zahlungsdetails per E-Mail.
</div>
${breederForm}
</div>`,
footer: `
<button data-modal-close
@ -190,8 +246,35 @@ window.Page_settings = (() => {
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…';
// Züchter: Formular validieren + als FormData senden
if (!isPro) {
const form = document.getElementById('breeder-upgrade-form');
if (form && !form.reportValidity()) return;
if (form) {
const fd = new FormData(form);
fd.set('vdh_mitglied', form.querySelector('[name="vdh_mitglied"]').checked ? '1' : '0');
// Pflichtfelder aus Form übernehmen falls leer → leere Strings senden
if (!fd.get('verein')) fd.set('verein', '');
if (!fd.get('stadt')) fd.set('stadt', '');
btn.disabled = true;
btn.textContent = 'Wird gesendet…';
try {
await API.breeder.apply(fd);
} catch (e) {
if (!e.message?.includes('bereits')) {
btn.disabled = false;
btn.textContent = 'Anfrage senden';
UI.toast.error(e.message || 'Fehler beim Einreichen.');
return;
}
}
}
} else {
btn.disabled = true;
btn.textContent = 'Wird gesendet…';
}
try {
const res = await API.auth.upgradeRequest(tier);
UI.modal.close();
@ -200,6 +283,7 @@ window.Page_settings = (() => {
} else {
UI.toast.success('Anfrage gesendet! Wir melden uns per E-Mail.');
}
if (!isPro) _loadBreederCard();
} catch (e) {
btn.disabled = false;
btn.textContent = 'Anfrage senden';
@ -1300,17 +1384,17 @@ window.Page_settings = (() => {
</span>`;
actionBlock = `
<div style="margin-top:var(--space-3)">
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-2)">
Du kannst einen neuen Antrag stellen.
</p>
<button class="btn btn-secondary btn-sm" id="breeder-reapply-btn">
${UI.icon('arrow-counter-clockwise')} Neu beantragen
</button>
</div>`;
} else {
actionBlock = `
<div style="margin-top:var(--space-3)">
<button class="btn btn-primary btn-sm" id="breeder-apply-btn">
${UI.icon('certificate')} Züchter werden
</button>
</div>`;
// Kein Antrag, kein Profil — Card ausblenden (Upgrade-Flow läuft über Abo & Tarif)
slot.innerHTML = '';
return;
}
slot.innerHTML = `
@ -1323,11 +1407,7 @@ window.Page_settings = (() => {
</div>`;
// Button-Handler binden
const applyBtn = slot.querySelector('#breeder-apply-btn');
const reapplyBtn = slot.querySelector('#breeder-reapply-btn');
if (applyBtn || reapplyBtn) {
(applyBtn || reapplyBtn).addEventListener('click', () => _openBreederApplyModal());
}
slot.querySelector('#breeder-reapply-btn')?.addEventListener('click', () => _showUpgradeModal('breeder'));
slot.querySelector('#breeder-edit-profile-btn')?.addEventListener('click', () =>
_openBreederEditModal(profile)

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
const CACHE_VERSION = 'by-v924';
const CACHE_VERSION = 'by-v925';
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