diff --git a/backend/auth.py b/backend/auth.py index b2736f5..55c63fc 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -87,7 +87,7 @@ def get_current_user( user_id = int(payload["sub"]) with db() as conn: row = conn.execute( - "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner, founder_number, email_verified FROM users WHERE id=?", + "SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner, founder_number, email_verified, luna_trial_until FROM users WHERE id=?", (user_id,) ).fetchone() @@ -131,7 +131,10 @@ def require_admin(user=Depends(get_current_user)): def require_social_media(user=Depends(get_current_user)): - """Dependency: Social-Media-Manager oder Admin.""" - if not (user.get("is_social_media") or user["rolle"] == "admin"): + """Dependency: Social-Media-Manager, Luna-Probezugang oder Admin.""" + from datetime import datetime as _dt + trial = user.get("luna_trial_until") + trial_active = bool(trial and _dt.utcnow().isoformat() < trial) + if not (user.get("is_social_media") or user["rolle"] == "admin" or trial_active): raise HTTPException(status.HTTP_403_FORBIDDEN, "Kein Zugriff.") return user diff --git a/backend/database.py b/backend/database.py index 8238bca..8ef5362 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1582,6 +1582,35 @@ def _migrate(conn_factory): if 'from_account' not in existing_ol: conn.execute("ALTER TABLE outreach_log ADD COLUMN from_account TEXT DEFAULT 'partner'") + # Job-Bewerbungen + Luna-Probezugang + conn.executescript(""" + CREATE TABLE IF NOT EXISTS job_applications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, + name TEXT NOT NULL, + email TEXT NOT NULL, + dog_name TEXT, + dog_rasse TEXT, + social_handle TEXT, + motivation TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + admin_note TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + reviewed_at TEXT + ); + CREATE INDEX IF NOT EXISTS idx_job_apps_status ON job_applications(status, created_at DESC); + CREATE TABLE IF NOT EXISTS job_application_docs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + application_id INTEGER NOT NULL REFERENCES job_applications(id) ON DELETE CASCADE, + filename TEXT NOT NULL, + file_path TEXT NOT NULL, + uploaded_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + """) + existing_u = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()] + if 'luna_trial_until' not in existing_u: + conn.execute("ALTER TABLE users ADD COLUMN luna_trial_until TEXT") + # js_exercise_id zu training_exercises — verbindet training_exercises mit exercise_progress existing_te = [row[1] for row in conn.execute("PRAGMA table_info(training_exercises)").fetchall()] if 'js_exercise_id' not in existing_te: diff --git a/backend/main.py b/backend/main.py index e7db176..d85556a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -188,6 +188,7 @@ from routes.breeder_export import router as breeder_export_router from routes.zucht_ki import router as zucht_ki_router from routes.partner import router as partner_router from routes.outreach import router as outreach_router +from routes.jobs import router as jobs_router app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) @@ -221,6 +222,7 @@ app.include_router(breeder_export_router, prefix="/api", tags=["Export"]) app.include_router(zucht_ki_router, prefix="/api", tags=["Züchter-KI"]) app.include_router(partner_router, prefix="/api", tags=["Partner"]) app.include_router(outreach_router, prefix="/api/outreach", tags=["Outreach"]) +app.include_router(jobs_router, prefix="/api/jobs", tags=["Jobs"]) app.include_router(webcal_router, prefix="/api/webcal", tags=["WebCal"]) app.include_router(profile_router, prefix="/api/profile", tags=["Profil"]) app.include_router(import_router, prefix="/api/import", tags=["Import"]) diff --git a/backend/routes/jobs.py b/backend/routes/jobs.py new file mode 100644 index 0000000..8714ae2 --- /dev/null +++ b/backend/routes/jobs.py @@ -0,0 +1,318 @@ +"""BAN YARO — Social-Media-Job Bewerbungs-System""" + +import os +import uuid +from datetime import datetime, timedelta +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form +from fastapi.responses import FileResponse +from typing import Optional +from database import db +from auth import get_current_user, get_current_user_optional, require_admin +from mailer import send_email, email_html + +router = APIRouter() + +MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") +JOBS_DIR = os.path.join(MEDIA_DIR, "jobs") +TRIAL_DAYS = 14 +MAX_FILES = 3 +MAX_FILE_MB = 10 + +os.makedirs(JOBS_DIR, exist_ok=True) + +_ALLOWED_EXT = {".pdf", ".jpg", ".jpeg", ".png", ".webp", ".mp4", ".mov"} + + +# ------------------------------------------------------------------ +# POST /api/jobs/apply +# ------------------------------------------------------------------ +async def apply( + name: str = Form(...), + email: str = Form(...), + dog_name: str = Form(""), + dog_rasse: str = Form(""), + social_handle: str = Form(...), + motivation: str = Form(...), + files: list[UploadFile] = File(default=[]), + user = Depends(get_current_user_optional), +): + if len(motivation.strip()) < 80: + raise HTTPException(400, "Bitte schreibe etwas mehr über dich (mindestens 80 Zeichen).") + if len(files) > MAX_FILES: + raise HTTPException(400, f"Maximal {MAX_FILES} Dateien erlaubt.") + + user_id = user["id"] if user else None + + # Doppelbewerbung verhindern + if user_id: + with db() as conn: + existing = conn.execute( + "SELECT id FROM job_applications WHERE user_id=? AND status NOT IN ('rejected')", + (user_id,) + ).fetchone() + if existing: + raise HTTPException(400, "Du hast bereits eine aktive Bewerbung eingereicht.") + + with db() as conn: + cur = conn.execute(""" + INSERT INTO job_applications + (user_id, name, email, dog_name, dog_rasse, social_handle, motivation) + VALUES (?,?,?,?,?,?,?) + """, (user_id, name.strip(), email.strip(), dog_name.strip(), + dog_rasse.strip(), social_handle.strip(), motivation.strip())) + app_id = cur.lastrowid + + # Dokumente speichern + app_dir = os.path.join(JOBS_DIR, str(app_id)) + os.makedirs(app_dir, exist_ok=True) + + for f in files: + if not f.filename: + continue + ext = os.path.splitext(f.filename)[1].lower() + if ext not in _ALLOWED_EXT: + continue + size = 0 + safe_name = f"{uuid.uuid4().hex}{ext}" + dest = os.path.join(app_dir, safe_name) + with open(dest, "wb") as out: + while chunk := await f.read(65536): + size += len(chunk) + if size > MAX_FILE_MB * 1024 * 1024: + out.close() + os.remove(dest) + raise HTTPException(400, f"Datei zu groß (max. {MAX_FILE_MB} MB).") + out.write(chunk) + conn.execute(""" + INSERT INTO job_application_docs (application_id, filename, file_path) + VALUES (?,?,?) + """, (app_id, f.filename, dest)) + + # Luna-Probezugang: 14 Tage ab sofort + if user_id: + trial_until = (datetime.utcnow() + timedelta(days=TRIAL_DAYS)).isoformat() + conn.execute( + "UPDATE users SET luna_trial_until=? WHERE id=?", + (trial_until, user_id) + ) + + # Bestätigungs-Mail an Bewerber + try: + body = f""" +

Hallo {name},

+

+ deine Bewerbung als Social-Media-Manager/in bei Ban Yaro ist bei uns eingegangen. + Wir melden uns bald bei dir! +

+ {"

🎉 Luna-Probezugang aktiviert!
Du hast für 14 Tage kostenlos Zugang zu Luna, unserem KI-Social-Media-Assistenten. Logge dich ein und probiere ihn aus.

" if user_id else ""} +

Das Ban Yaro Team

""" + await send_email( + email, + "Deine Bewerbung bei Ban Yaro 🐾", + email_html(body, cta_url="https://banyaro.app", cta_label="Zur App"), + f"Hallo {name}, deine Bewerbung ist eingegangen!", + ) + except Exception: + pass + + # Admin benachrichtigen + try: + admin_email = os.getenv("ADMIN_EMAIL", "") + if admin_email: + admin_body = f""" +

Neue Job-Bewerbung eingegangen:

+ + + + + + +
Name{name}
E-Mail{email}
Hund{dog_name} ({dog_rasse})
Social{social_handle}
Anhänge{len([f for f in files if f.filename])} Datei(en)
+

{motivation[:300]}{"…" if len(motivation)>300 else ""}

""" + await send_email( + admin_email, + f"[Banyaro Jobs] Neue Bewerbung — {name}", + email_html(admin_body, cta_url="https://banyaro.app/#admin", cta_label="Im Admin-Bereich prüfen"), + f"Neue Bewerbung von {name} ({email})", + ) + except Exception: + pass + + return { + "ok": True, + "application_id": app_id, + "luna_trial": user_id is not None, + "trial_days": TRIAL_DAYS, + } + + +# FastAPI braucht expliziten Router-Decorator +router.add_api_route("/apply", apply, methods=["POST"], status_code=201) + + +# ------------------------------------------------------------------ +# GET /api/jobs/my-application +# ------------------------------------------------------------------ +@router.get("/my-application") +async def my_application(user=Depends(get_current_user)): + with db() as conn: + row = conn.execute( + """SELECT id, status, admin_note, created_at + FROM job_applications WHERE user_id=? + ORDER BY created_at DESC LIMIT 1""", + (user["id"],) + ).fetchone() + if not row: + return {"application": None} + return {"application": dict(row)} + + +# ------------------------------------------------------------------ +# GET /api/jobs/luna-trial-status +# ------------------------------------------------------------------ +@router.get("/luna-trial-status") +async def luna_trial_status(user=Depends(get_current_user)): + from datetime import datetime as _dt + trial = user.get("luna_trial_until") + if not trial: + return {"active": False} + remaining = (_dt.fromisoformat(trial) - _dt.utcnow()).days + return {"active": remaining > 0, "until": trial, "remaining_days": max(0, remaining)} + + +# ------------------------------------------------------------------ +# Admin: Bewerbungen verwalten +# ------------------------------------------------------------------ +@router.get("/admin/applications") +async def list_applications( + status: str = "pending", + admin = Depends(require_admin), +): + where = "" if status == "alle" else "WHERE a.status=?" + params = [] if status == "alle" else [status] + with db() as conn: + rows = conn.execute(f""" + SELECT a.*, u.name AS username, + COUNT(d.id) AS doc_count + FROM job_applications a + LEFT JOIN users u ON u.id = a.user_id + LEFT JOIN job_application_docs d ON d.application_id = a.id + {where} + GROUP BY a.id + ORDER BY a.created_at DESC + """, params).fetchall() + return [dict(r) for r in rows] + + +@router.get("/admin/applications/{app_id}") +async def get_application(app_id: int, admin=Depends(require_admin)): + with db() as conn: + row = conn.execute( + """SELECT a.*, u.name AS username, u.email AS user_email + FROM job_applications a + LEFT JOIN users u ON u.id = a.user_id + WHERE a.id=?""", + (app_id,) + ).fetchone() + if not row: + raise HTTPException(404) + docs = conn.execute( + "SELECT id, filename, uploaded_at FROM job_application_docs WHERE application_id=?", + (app_id,) + ).fetchall() + return {**dict(row), "docs": [dict(d) for d in docs]} + + +@router.patch("/admin/applications/{app_id}") +async def update_application( + app_id: int, + status: Optional[str] = None, + admin_note: Optional[str] = None, + admin = Depends(require_admin), +): + valid = {"pending", "reviewing", "accepted", "rejected"} + if status and status not in valid: + raise HTTPException(400, f"Ungültiger Status. Erlaubt: {valid}") + + with db() as conn: + row = conn.execute( + "SELECT user_id, email, name, status FROM job_applications WHERE id=?", + (app_id,) + ).fetchone() + if not row: + raise HTTPException(404) + + updates: dict = {"reviewed_at": datetime.utcnow().isoformat()} + if status: + updates["status"] = status + if admin_note is not None: + updates["admin_note"] = admin_note + + set_clause = ", ".join(f"{k}=?" for k in updates) + conn.execute( + f"UPDATE job_applications SET {set_clause} WHERE id=?", + (*updates.values(), app_id) + ) + + # Bei Annahme: is_social_media aktivieren + Gründer-Status + if status == "accepted" and row["user_id"]: + conn.execute( + "UPDATE users SET is_social_media=1 WHERE id=?", + (row["user_id"],) + ) + founder_count = conn.execute( + "SELECT COUNT(*) FROM users WHERE is_founder=1" + ).fetchone()[0] + if founder_count < 100: + conn.execute( + "UPDATE users SET is_founder=1 WHERE id=? AND is_founder=0", + (row["user_id"],) + ) + + # Status-Mail an Bewerber + try: + if status in ("accepted", "rejected", "reviewing"): + _send_status_mail(row["email"], row["name"], status, admin_note or "") + except Exception: + pass + + return {"ok": True} + + +@router.get("/admin/applications/{app_id}/docs/{doc_id}") +async def download_doc(app_id: int, doc_id: int, admin=Depends(require_admin)): + with db() as conn: + doc = conn.execute( + "SELECT file_path, filename FROM job_application_docs WHERE id=? AND application_id=?", + (doc_id, app_id) + ).fetchone() + if not doc or not os.path.exists(doc["file_path"]): + raise HTTPException(404) + return FileResponse(doc["file_path"], filename=doc["filename"]) + + +def _send_status_mail(email: str, name: str, status: str, note: str): + import asyncio + texts = { + "reviewing": ("Wir schauen uns deine Bewerbung genauer an 🐾", + f"

Hallo {name},

wir schauen uns deine Bewerbung gerade genauer an. Wir melden uns bald!

"), + "accepted": ("Herzlichen Glückwunsch — du bist dabei! 🎉", + f"

Hallo {name},

wir freuen uns, dir mitzuteilen: du bist unser neuer Social-Media-Manager/in für Ban Yaro!
Du erhältst außerdem den Gründer-Status in unserer Community. Willkommen im Team!

"), + "rejected": ("Deine Bewerbung bei Ban Yaro", + f"

Hallo {name},

vielen Dank für deine Bewerbung. Leider hat es diesmal nicht geklappt — aber wir wünschen dir alles Gute!

"), + } + subj, body_start = texts.get(status, ("Update zu deiner Bewerbung", f"

Hallo {name},

")) + note_html = f'
{note}
' if note else "" + body = body_start + note_html + + async def _send(): + await send_email(email, subj, email_html(body, cta_url="https://banyaro.app", cta_label="Zur App"), subj) + + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + asyncio.ensure_future(_send()) + else: + loop.run_until_complete(_send()) + except Exception: + pass diff --git a/backend/static/js/app.js b/backend/static/js/app.js index cdc5231..b75494f 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -70,6 +70,7 @@ const App = (() => { zuchthunde: { title: 'Zuchtkartei', module: null, requiresAuth: true }, 'zucht-profil': { title: 'Hunde-Profil', module: null }, gruender: { title: '100 Gründer', module: null }, + jobs: { title: 'Wir suchen dich', module: null }, }; // ---------------------------------------------------------- diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 1775ccd..37d31ae 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -18,7 +18,8 @@ window.Page_admin = (() => { { id: 'social', label: 'Social Media', icon: 'camera' }, { id: 'analytics', label: 'Analytics', icon: 'target' }, { id: 'system', label: 'System', icon: 'gear' }, - { id: 'jobs', label: 'Jobs', icon: 'clock' }, + { id: 'jobs', label: 'Scheduler', icon: 'clock' }, + { id: 'bewerbungen', label: 'Bewerbungen', icon: 'user-plus' }, { id: 'partner', label: 'Partner', icon: 'handshake' }, { id: 'outreach', label: 'Outreach', icon: 'envelope-simple' }, { id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' }, @@ -93,6 +94,7 @@ window.Page_admin = (() => { case 'partner': await _renderPartner(el); break; case 'outreach': await _renderOutreach(el); break; case 'audit': await _renderAudit(el); break; + case 'bewerbungen': await _renderBewerbungen(el); break; } } catch (e) { el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.'); @@ -2375,6 +2377,125 @@ window.Page_admin = (() => { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } + // ------------------------------------------------------------------ + // BEWERBUNGEN — Social-Media-Job + // ------------------------------------------------------------------ + async function _renderBewerbungen(el) { + let _statusFilter = 'pending'; + + async function _load() { + el.innerHTML = ` +
+ ${['pending','reviewing','accepted','rejected','alle'].map(s => ` + `).join('')} +
+
${UI.skeleton(3)}
`; + + el.querySelectorAll('.adm-bew-filter').forEach(btn => { + btn.addEventListener('click', () => { + _statusFilter = btn.dataset.s; + _load(); + }); + }); + + try { + const rows = await API.get(`/jobs/admin/applications?status=${_statusFilter}`); + const list = el.querySelector('#adm-bew-list'); + if (!rows.length) { + list.innerHTML = _emptyState('user-plus', 'Keine Bewerbungen', 'Noch keine Bewerbungen in diesem Status.'); + return; + } + list.innerHTML = rows.map(r => ` +
+
+
+
${_esc(r.name)} + ${r.username ? `(@${_esc(r.username)})` : ''} +
+
+ ${_esc(r.email)} · @${_esc(r.social_handle||'—')} + ${r.dog_name ? ` · 🐕 ${_esc(r.dog_name)} (${_esc(r.dog_rasse||'')})` : ''} +
+
+ ${r.created_at?.slice(0,16).replace('T',' ')} · ${r.doc_count} Anhang/Anhänge +
+
+ ${_esc((r.motivation||'').slice(0,200))}${(r.motivation||'').length>200?'…':''} +
+
+
+ + +
+
+
`).join(''); + + list.querySelectorAll('.adm-bew-status').forEach(sel => { + sel.addEventListener('change', async () => { + const id = sel.dataset.id; + await API.patch(`/jobs/admin/applications/${id}`, { status: sel.value }); + UI.toast.success('Status aktualisiert.'); + setTimeout(_load, 500); + }); + }); + + list.querySelectorAll('.adm-bew-view').forEach(btn => { + btn.addEventListener('click', async () => { + const id = btn.dataset.id; + const app = await API.get(`/jobs/admin/applications/${id}`); + const docsHtml = app.docs?.length + ? app.docs.map(d => ` + 📎 ${_esc(d.filename)}`).join('') + : 'Keine Anhänge'; + + UI.modal.open({ + title: `Bewerbung — ${_esc(app.name)}`, + body: ` +
+
E-Mail: ${_esc(app.email)}
+
Social: @${_esc(app.social_handle||'—')}
+ ${app.dog_name ? `
Hund: ${_esc(app.dog_name)} (${_esc(app.dog_rasse||'')})
` : ''} +
Motivation:
+
${_esc(app.motivation)}
+
+
Anhänge:
${docsHtml}
+
+ Admin-Notiz: + +
+
`, + footer: ` + + `, + }); + document.getElementById('adm-bew-save-note')?.addEventListener('click', async () => { + const note = document.getElementById('adm-bew-note')?.value || ''; + await API.patch(`/jobs/admin/applications/${id}`, { admin_note: note }); + UI.toast.success('Notiz gespeichert.'); + UI.modal.close(); + }); + }); + }); + } catch (e) { + el.querySelector('#adm-bew-list').innerHTML = _emptyState('warning', 'Fehler', e.message); + } + } + + await _load(); + } + // ------------------------------------------------------------------ return { init, refresh, onDogChange }; diff --git a/backend/static/js/pages/jobs.js b/backend/static/js/pages/jobs.js new file mode 100644 index 0000000..7ad5e68 --- /dev/null +++ b/backend/static/js/pages/jobs.js @@ -0,0 +1,260 @@ +/* ============================================================ + BAN YARO — Social-Media-Job Bewerbung + ============================================================ */ + +window.Page_jobs = (() => { + + let _container = null; + let _appState = null; + + async function init(container, appState) { + _container = container; + _appState = appState; + await _render(); + } + + async function _render() { + // Bestehende Bewerbung prüfen (nur wenn eingeloggt) + let existingApp = null; + let trialStatus = null; + if (_appState.user) { + try { + const r = await API.get('/jobs/my-application'); + existingApp = r.application; + trialStatus = await API.get('/jobs/luna-trial-status'); + } catch { /* ignorieren */ } + } + + _container.innerHTML = ` +
+ + +
+
🐾
+

+ Social-Media-Manager/in gesucht +

+

+ Werde das Gesicht von Ban Yaro auf Instagram & TikTok +

+
+ + +
+
+

Die Stelle

+
+ ${_infoRow('📍', 'Remote', '100 % flexibel — du arbeitest wann und wie du willst')} + ${_infoRow('📅', 'Umfang', '1–2 Posts pro Woche auf Instagram & TikTok')} + ${_infoRow('💶', 'Vergütung', '50 € / Monat — wächst mit der Community')} + ${_infoRow('🤖', 'Luna an deiner Seite', 'Unser KI-Assistent schreibt Captions, generiert Post-Ideen und Hashtags — du wählst aus und postest')} + ${_infoRow('⭐', 'Gründer-Status', 'Du wirst Teil der ersten 100 Gründer — für immer')} +
+
+
+ + +
+
+ 🤖 Luna 14 Tage kostenlos testen +
+

+ Mit deiner Bewerbung schalten wir dir sofort den vollen Zugang zu Luna frei — + unserem KI-Assistenten für Social-Media-Posts. Probiere ihn einfach aus, + bevor du dich entscheidest. +

+ ${trialStatus?.active ? `
+ ✅ Dein Probezugang läuft noch ${trialStatus.remaining_days} Tage
` : ''} +
+ + +
+
+

Wen wir suchen

+
    +
  • Du hast einen Hund — und liebst ihn sehr 🐕
  • +
  • Du bist auf Instagram oder TikTok zuhause (nicht professionell, aber aktiv)
  • +
  • Du schreibst gerne und authentisch auf Deutsch
  • +
  • Du hast Lust, eine junge App bekannt zu machen — aus Überzeugung
  • +
  • Kein Lebenslauf nötig. Kein Bewerbungs-Anschreiben. Einfach du.
  • +
+
+
+ + + ${existingApp ? _renderStatus(existingApp) : _renderForm()} + +
+ `; + + if (!existingApp) { + _bindForm(); + } + } + + function _infoRow(icon, label, text) { + return ` +
+ ${icon} +
+
${label}
+
${text}
+
+
`; + } + + function _renderStatus(app) { + const statusMap = { + pending: { icon: '⏳', text: 'Deine Bewerbung ist eingegangen — wir melden uns bald!', color: 'var(--c-text-secondary)' }, + reviewing: { icon: '🔍', text: 'Wir schauen uns deine Bewerbung gerade genauer an.', color: '#0284c7' }, + accepted: { icon: '🎉', text: 'Herzlichen Glückwunsch — du bist dabei!', color: 'var(--c-success)' }, + rejected: { icon: '😔', text: 'Es hat diesmal leider nicht geklappt.', color: 'var(--c-danger)' }, + }; + const s = statusMap[app.status] || statusMap.pending; + return ` +
+
${s.icon}
+
${s.text}
+
+ Bewerbung eingereicht: ${app.created_at?.slice(0,10)} +
+ ${app.admin_note ? `
${UI.esc(app.admin_note)}
` : ''} +
`; + } + + function _renderForm() { + const u = _appState.user; + return ` +
+
+

+ Jetzt bewerben +

+
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ @ + +
+

+ Dein öffentliches Profil auf Instagram oder TikTok +

+
+ +
+ + +

+ Mindestens 80 Zeichen +

+
+ +
+ + +

+ Beispiel-Posts, Portfolio, kurzes Video von dir und deinem Hund — max. 3 Dateien, je 10 MB. + PDF, Bild oder Video. +

+
+ + ${!u ? `
+ 💡 Tipp: Wenn du dich vorher + anmeldest oder registrierst, + bekommst du sofort den 14-tägigen Luna-Probezugang. +
` : ''} + + + +
+
+
`; + } + + function _bindForm() { + document.getElementById('jobs-login-link')?.addEventListener('click', e => { + e.preventDefault(); + if (window.App) App.navigate('settings'); + }); + + document.getElementById('jobs-form')?.addEventListener('submit', async e => { + e.preventDefault(); + const btn = e.target.querySelector('[type="submit"]'); + const fd = new FormData(e.target); + + // Dateien aus file-input übernehmen + const fileInput = document.getElementById('jobs-files'); + if (fileInput?.files?.length) { + fd.delete('files'); + for (const f of fileInput.files) fd.append('files', f); + } + + await UI.asyncButton(btn, async () => { + const resp = await fetch('/api/jobs/apply', { + method: 'POST', + body: fd, + headers: { 'Authorization': `Bearer ${localStorage.getItem('by_token') || ''}` }, + credentials: 'include', + }); + if (!resp.ok) { + const err = await resp.json().catch(() => ({})); + throw new Error(err.detail || 'Fehler beim Absenden.'); + } + const result = await resp.json(); + + if (result.luna_trial) { + UI.toast.success('🎉 Bewerbung eingegangen! Dein Luna-Probezugang ist jetzt aktiv.'); + // User-State aktualisieren damit Luna sofort zugänglich ist + if (_appState.user && window.API) { + try { _appState.user = await API.auth.me(); } catch { /* ignore */ } + } + } else { + UI.toast.success('Bewerbung eingegangen! Wir melden uns bald.'); + } + + await _render(); + }); + }); + } + + return { init }; +})();