diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 0b09ae2..a4ff043 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -8,7 +8,7 @@ from typing import Optional from database import db from auth import get_current_user from routes.push import send_push_to_user -from media_utils import safe_media_path +from media_utils import safe_media_path, preview_url_from router = APIRouter() MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") @@ -96,6 +96,74 @@ async def create_dog(data: DogCreate, user=Depends(get_current_user)): return dict(dog) +@router.get("/{dog_id}/welcome-dashboard") +async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)): + """Liefert kompakte Dashboard-Daten für die Welcome-Ansicht eines Hundes.""" + import random as _random + with db() as conn: + # Besitz prüfen + 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.") + + # Zufälliges Foto aus den letzten 100 Tagebuchbildern + photos = conn.execute( + """SELECT dm.url FROM diary_media dm + JOIN diary d ON d.id = dm.diary_id + WHERE d.dog_id=? AND dm.media_type='image' + ORDER BY d.datum DESC LIMIT 100""", + (dog_id,) + ).fetchall() + random_photo = None + if photos: + chosen_url = _random.choice(photos)["url"] + random_photo = { + "url": chosen_url, + "preview_url": preview_url_from(chosen_url), + } + + # Neuester Tagebucheintrag + last_diary_row = conn.execute( + "SELECT titel, datum FROM diary WHERE dog_id=? ORDER BY datum DESC LIMIT 1", + (dog_id,) + ).fetchone() + last_diary = dict(last_diary_row) if last_diary_row else None + + # Nächster Termin (kein Gewicht) + next_appt_row = conn.execute( + """SELECT bezeichnung, naechstes, typ FROM health + WHERE dog_id=? AND naechstes IS NOT NULL AND naechstes >= date('now') + AND typ != 'gewicht' + ORDER BY naechstes ASC LIMIT 1""", + (dog_id,) + ).fetchone() + next_appointment = dict(next_appt_row) if next_appt_row else None + + # Letztes Gewicht + last_weight_row = conn.execute( + """SELECT wert, einheit, datum FROM health + WHERE dog_id=? AND typ='gewicht' + ORDER BY datum DESC LIMIT 1""", + (dog_id,) + ).fetchone() + last_weight = dict(last_weight_row) if last_weight_row else None + + # Anzahl Tagebucheinträge + diary_count = conn.execute( + "SELECT COUNT(*) AS n FROM diary WHERE dog_id=?", (dog_id,) + ).fetchone()["n"] + + return { + "random_photo": random_photo, + "last_diary": last_diary, + "next_appointment": next_appointment, + "last_weight": last_weight, + "diary_count": diary_count, + } + + @router.get("/{dog_id}") async def get_dog(dog_id: int, user=Depends(get_current_user)): with db() as conn: diff --git a/backend/static/js/api.js b/backend/static/js/api.js index acc1e60..96a3f46 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -108,6 +108,7 @@ const API = (() => { }, deletePhoto(id) { return del(`/dogs/${id}/photo`); }, getSkills(id) { return get(`/dogs/${id}/skills`); }, + welcomeDashboard(dogId) { return get(`/dogs/${dogId}/welcome-dashboard`); }, }; // ---------------------------------------------------------- diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 7a1d0d4..bc72295 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '451'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '452'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/welcome.js b/backend/static/js/pages/welcome.js index aadd918..76ab1cb 100644 --- a/backend/static/js/pages/welcome.js +++ b/backend/static/js/pages/welcome.js @@ -255,24 +255,140 @@ window.Page_welcome = (() => { } // ---------------------------------------------------------- - // EINGELOGGTE ANSICHT — kompakter Überblick + // HILFSFUNKTIONEN (eingeloggte Ansicht) + // ---------------------------------------------------------- + function _relDate(dateStr) { + if (!dateStr) return null; + const diff = Math.floor((Date.now() - new Date(dateStr)) / 86400000); + if (diff === 0) return 'Heute'; + if (diff === 1) return 'Gestern'; + if (diff > 1 && diff < 14) return `vor ${diff} Tagen`; + if (diff === -1) return 'Morgen'; + if (diff < 0 && diff > -14) return `in ${-diff} Tagen`; + return dateStr; + } + + function _greeting() { + const h = new Date().getHours(); + if (h >= 5 && h < 11) return 'Guten Morgen'; + if (h >= 11 && h < 18) return 'Moin'; + if (h >= 18 && h < 22) return 'Guten Abend'; + return 'Hey'; + } + + function _appointmentIcon(typ) { + if (!typ) return 'calendar-check'; + const t = typ.toLowerCase(); + if (t === 'impfung') return 'syringe'; + if (t === 'tierarzt') return 'stethoscope'; + if (t === 'medikament') return 'pill'; + return 'calendar-check'; + } + + // Die 4 Feature-Karten für die eingeloggte Ansicht + const FEATURE_CARDS = [ + { icon: 'book-open', label: 'Tagebuch', desc: 'Fotos, Notizen, Erinnerungen', page: 'diary' }, + { icon: 'first-aid', label: 'Gesundheit', desc: 'Impfungen, Gewicht, Termine', page: 'health' }, + { icon: 'map-trifold', label: 'Karte', desc: 'Spots & Alerts in deiner Nähe', page: 'map' }, + { icon: 'target', label: 'Training', desc: 'Übungen mit KI-Trainer', page: 'uebungen' }, + ]; + const FEATURE_CARD_PAGES = new Set(FEATURE_CARDS.map(f => f.page)); + + function _heroHTML(dog, dashData) { + const photoUrl = dashData?.random_photo?.url || null; + const avatarUrl = dog?.foto_url || null; + const dogName = dog ? UI.escape(dog.name) : 'Dein Hund'; + const dogRasse = dog?.rasse ? UI.escape(dog.rasse) : ''; + const greeting = _greeting(); + + if (photoUrl) { + return ` +
${dogRasse}
` : ''} +${dogRasse}
` : ''} +- Schön, dass du wieder da bist${_appState?.user?.name ? ', ' + UI.escape(_appState.user.name) + '' : ''}! -
-${dogRasse}
` : ''} +