diff --git a/Makefile b/Makefile index 8bc1dc6..2427674 100644 --- a/Makefile +++ b/Makefile @@ -138,7 +138,8 @@ release: check-ssh @git checkout main @git merge develop --no-ff -m "Release v$(VERSION)" @sed -i '' 's/"version": "[^"]*"/"version": "$(VERSION)"/' backend/static/manifest.json - @git add backend/static/manifest.json + @sed -i '' "s/const APP_VERSION = '[^']*'/const APP_VERSION = '$(VERSION)'/" backend/static/js/app.js + @git add backend/static/manifest.json backend/static/js/app.js @git commit --amend --no-edit @git tag "v$(VERSION)" @git push $(GIT_REMOTE) main --tags diff --git a/backend/auth.py b/backend/auth.py index d923c70..942a3f1 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 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 FROM users WHERE id=?", (user_id,) ).fetchone() diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 8be436c..09a4127 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -296,6 +296,7 @@ async def list_users( rows = conn.execute(f""" SELECT u.id, u.name, {_email_col}, u.rolle, u.is_premium, u.is_moderator, u.is_banned, u.ban_reason, + u.is_founder, u.is_partner, u.founder_number, u.created_at, u.last_login, (SELECT COUNT(*) FROM dogs d WHERE d.user_id=u.id) AS dog_count, (SELECT COUNT(*) FROM forum_threads t WHERE t.user_id=u.id AND t.is_deleted=0) AS thread_count, diff --git a/backend/routes/friends.py b/backend/routes/friends.py index df7c96a..cac5f4b 100644 --- a/backend/routes/friends.py +++ b/backend/routes/friends.py @@ -32,6 +32,7 @@ async def list_friends(user=Depends(get_current_user)): u.name AS friend_name, u.bio, u.wohnort, u.erfahrung, u.social_link, u.profil_sichtbarkeit, u.avatar_url, + u.is_founder, u.is_partner, u.founder_number, {dogs_sq} AS dogs_json FROM friendships f JOIN users u ON u.id = CASE WHEN f.requester_id=? THEN f.addressee_id ELSE f.requester_id END @@ -92,6 +93,7 @@ async def search_users(q: str = "", user=Depends(get_current_user)): SELECT u.id, u.name, u.bio, u.wohnort, u.erfahrung, u.social_link, u.profil_sichtbarkeit, u.avatar_url, + u.is_founder, u.is_partner, u.founder_number, (SELECT json_group_array(json_object('name', d.name, 'rasse', d.rasse)) FROM dogs d WHERE d.user_id=u.id AND d.is_public=1) AS dogs_json FROM users u diff --git a/backend/static/js/app.js b/backend/static/js/app.js index c05a7a5..9d526bd 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,8 +3,8 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '522'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen -const APP_VERSION = '1.0.0'; // ← semantische Version, wird bei make release gesetzt +const APP_VER = '533'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VERSION = '1.1.2'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; const App = (() => { diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 4706cee..69ed773 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -19,7 +19,7 @@ window.Page_admin = (() => { { id: 'analytics', label: 'Analytics', icon: 'target' }, { id: 'system', label: 'System', icon: 'gear' }, { id: 'jobs', label: 'Jobs', icon: 'clock' }, - { id: 'partner', label: 'Partner & Codes', icon: 'handshake' }, + { id: 'partner', label: 'Partner', icon: 'handshake' }, { id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' }, ]; @@ -1792,13 +1792,23 @@ window.Page_admin = (() => { // TAB: AUDIT-LOG // ------------------------------------------------------------------ async function _renderPartner(el) { - const [codes] = await Promise.all([ - API.get('/api/admin/partner/codes'), - ]); + const codes = (await API.get('/admin/partner/codes')) || []; el.innerHTML = `
+ +
+

So funktioniert das Partner-System

+
+

1. Partner-Code erstellen — Erstelle einen Code (z. B. HUNDEBLOG) für einen Influencer oder Partner. Der Code wird an die Person weitergegeben.

+

2. Registrierung mit Code — Wenn sich ein neuer User mit diesem Code registriert, wird er automatisch als Gründer markiert (Platz #1–100, lebenslang kostenlos). Du siehst in der Tabelle wie viele Einlösungen jeder Code hat.

+

3. Partner-Status vergeben — Den Influencer selbst suchst du unten bei «Nutzer-Status» und setzt Partner-Badge (blaues Badge im Profil) und Gründer-Lizenz. So ist auch er als Gründer #X sichtbar.

+

Max. 100 Gründer — Ist die Zahl bei einem Code leer, ist sie unbegrenzt. Die globale Grenze über alle Codes hinweg sind 100 Gründer-Plätze.

+

Freunde werben — Jeder eingeloggte User hat einen persönlichen Einladungslink (Einstellungen → Freunde werben). Bei 10 geworbenen Usern gibt es 20 % Rabatt, bei 20 → 30 %, bei 50 → 50 % — lebenslang, sobald Bezahlfunktionen aktiv sind.

+
+
+

Neuen Partner-Code erstellen

@@ -1915,7 +1925,7 @@ window.Page_admin = (() => { const code = (fd.code || '').trim().toUpperCase(); if (!code) return; await UI.asyncButton(btn, async () => { - await API.post('/api/admin/partner/codes', { + await API.post('/admin/partner/codes', { code, label: fd.label || code, grants_founder: e.target.querySelector('[name="grants_founder"]').checked ? 1 : 0, @@ -1932,7 +1942,7 @@ window.Page_admin = (() => { if (!window.confirm(`Code wirklich löschen?`)) return; const id = btn.dataset.id; await UI.asyncButton(btn, async () => { - await API.del(`/api/admin/partner/codes/${id}`); + await API.del(`/admin/partner/codes/${id}`); UI.toast.success('Code gelöscht.'); await _renderPartner(el); }); @@ -1948,22 +1958,24 @@ window.Page_admin = (() => { clearTimeout(_searchTimeout); _grantUserId = null; const q = searchInput.value.trim(); - if (q.length < 2) { grantResult.innerHTML = ''; return; } + if (q.length < 1) { grantResult.innerHTML = ''; return; } _searchTimeout = setTimeout(async () => { try { - const users = await API.get(`/api/admin/users/search?q=${encodeURIComponent(q)}`); - if (!users.length) { + const res = await API.get(`/admin/users?q=${encodeURIComponent(q)}&limit=10`); + const users = res?.users || res || []; + if (!users || !users.length) { grantResult.innerHTML = `

Kein User gefunden.

`; return; } grantResult.innerHTML = users.map(u => `
${u.name} - ${u.is_founder ? '⭐ Gründer ' : ''}${u.is_partner ? '🤝 Partner' : ''} + ${u.rolle}${u.is_founder ? ' · ⭐' : ''}${u.is_partner ? ' · 🤝' : ''}
`).join(''); @@ -1971,10 +1983,18 @@ window.Page_admin = (() => { div.addEventListener('click', () => { _grantUserId = parseInt(div.dataset.id); searchInput.value = div.dataset.name; - grantResult.innerHTML = `

✓ ${div.dataset.name} ausgewählt

`; + // Aktuellen Status in Checkboxen setzen + const form = el.querySelector('#adm-partner-grant'); + if (form) { + form.querySelector('[name="is_founder"]').checked = div.dataset.founder === '1'; + form.querySelector('[name="is_partner"]').checked = div.dataset.partner === '1'; + } + grantResult.innerHTML = `

✓ ${div.dataset.name} ausgewählt${div.dataset.founder==='1' ? ' · ⭐ Gründer' : ''}${div.dataset.partner==='1' ? ' · 🤝 Partner' : ''}

`; }); }); - } catch { grantResult.innerHTML = ''; } + } catch(e) { + grantResult.innerHTML = `

${e.message || 'Suchfehler'}

`; + } }, 400); }); @@ -1985,13 +2005,14 @@ window.Page_admin = (() => { const isFounder = e.target.querySelector('[name="is_founder"]').checked ? 1 : 0; const isPartner = e.target.querySelector('[name="is_partner"]').checked ? 1 : 0; await UI.asyncButton(btn, async () => { - const result = await API.post(`/api/admin/partner/users/${_grantUserId}/grant`, { + const result = await API.post(`/admin/partner/users/${_grantUserId}/grant`, { is_founder: isFounder, is_partner: isPartner, }); + if (!result) throw new Error('Keine Antwort vom Server.'); UI.toast.success(`Status für ${result.name} gesetzt.`); grantResult.innerHTML = `

✓ Gründer: ${result.is_founder ? 'Ja' : 'Nein'} | Partner: ${result.is_partner ? 'Ja' : 'Nein'}

`; - }); + }).catch(e => UI.toast.error(e.message || 'Fehler beim Speichern.')); }); } diff --git a/backend/static/js/pages/friends.js b/backend/static/js/pages/friends.js index 837474c..3f2e690 100644 --- a/backend/static/js/pages/friends.js +++ b/backend/static/js/pages/friends.js @@ -610,10 +610,23 @@ window.Page_friends = (() => { margin-bottom:var(--space-4)">${parts.join('')}
`; })(); + const badgesHTML = (profile.is_founder || profile.is_partner) ? ` +
+ ${profile.is_founder ? ` + + ${profile.founder_number ? `Gründer #${profile.founder_number}` : 'Gründer'} + ` : ''} + ${profile.is_partner ? ` + + Partner + ` : ''} +
` : ''; + UI.modal.open({ title: _esc(friendName), body: `
+ ${badgesHTML} ${profileInfoHTML} ${dogsHTML} @@ -667,10 +680,12 @@ window.Page_friends = (() => { ${i < results.length - 1 ? 'border-bottom:1px solid var(--c-border)' : ''}"> ${_userAvatar(u.name, null, u.avatar_url)}
-
${_esc(u.name)} + ${u.is_founder ? `${u.founder_number ? `Gründer #${u.founder_number}` : 'Gründer'}` : ''} + ${u.is_partner ? `Partner` : ''} ${_erfahrungSpan(u.erfahrung)}
${_wohnortLine(u.wohnort)} diff --git a/backend/static/js/pages/gruender.js b/backend/static/js/pages/gruender.js index e82fc3e..100891e 100644 --- a/backend/static/js/pages/gruender.js +++ b/backend/static/js/pages/gruender.js @@ -39,7 +39,7 @@ window.Page_gruender = (() => { async function _load() { const el = _container.querySelector('#grnd-content'); try { - const d = await API.get('/api/partner/founders/stats'); + const d = await API.get('/partner/founders/stats'); if (!d || typeof d.total === 'undefined') throw new Error('Ungültige Antwort vom Server.'); el.innerHTML = _renderStats(d); } catch (e) { diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 7c5c80e..da7b4a7 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -54,6 +54,14 @@ window.Page_settings = (() => { _container = container; _appState = appState; _render(); + // Frischen User-State laden damit Badges (is_founder, is_partner) aktuell sind + if (_appState.user) { + try { + const fresh = await API.auth.me(); + Object.assign(_appState.user, fresh); + _render(); + } catch {} + } } function refresh() { @@ -1450,7 +1458,7 @@ window.Page_settings = (() => { if (code.length < 3) return; _debounce = setTimeout(async () => { try { - const info = await API.get(`/api/partner/codes/${encodeURIComponent(code)}/info`); + const info = await API.get(`/partner/codes/${encodeURIComponent(code)}/info`); if (info.redeemable) { partnerHint.textContent = info.grants_founder ? `✓ Gültiger Code von "${info.label}" — du erhältst eine lebenslange Gründer-Lizenz!` diff --git a/backend/static/manifest.json b/backend/static/manifest.json index 1fa53ab..e4cfd81 100644 --- a/backend/static/manifest.json +++ b/backend/static/manifest.json @@ -1,6 +1,6 @@ { "id": "/", - "version": "1.1.1", + "version": "1.1.2", "name": "Ban Yaro — Die Hunde-Plattform", "short_name": "Ban Yaro", "description": "Alles rund um deinen Hund. Von Welpe bis Opa.", diff --git a/backend/static/sw.js b/backend/static/sw.js index 88448fe..fb662ef 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v545'; +const CACHE_VERSION = 'by-v556'; 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