diff --git a/backend/routers/activity.py b/backend/routers/activity.py index 7d8eaea..d698c39 100644 --- a/backend/routers/activity.py +++ b/backend/routers/activity.py @@ -35,7 +35,12 @@ except Exception as e: @router.get("") def list_activity( limit: int = Query(200, ge=1, le=50_000), + offset: int = Query(0, ge=0, le=100_000, description="SQL OFFSET für Pagination"), days: Optional[int] = Query(None, ge=1, le=4000, description="Nur Einträge mit date >= HEUTE − days (Kalendertage)"), + skip_quality_filter: bool = Query( + False, + description="True = alle Einträge des Profils (ohne quality_label-Filter). Für /activity Erfassung.", + ), session: dict = Depends(require_auth), ): """Get activity entries for current profile. Optional *days* filter by calendar window (not the same as *limit*).""" @@ -44,10 +49,13 @@ def list_activity( with get_db() as conn: cur = get_cursor(conn) - # Issue #31: Apply global quality filter (profile from DB = saved level) - cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) - profile = r2d(cur.fetchone()) - quality_filter = get_quality_filter_sql(profile or {}) + # Issue #31: Qualitätsfilter — auf der Erfassungsseite /activity abschaltbar (skip_quality_filter) + if skip_quality_filter: + quality_filter = "" + else: + cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) + profile = r2d(cur.fetchone()) + quality_filter = get_quality_filter_sql(profile or {}) if days is not None: cur.execute( @@ -57,9 +65,9 @@ def list_activity( {quality_filter} AND date >= (CURRENT_DATE - %s::integer) ORDER BY date DESC, start_time DESC - LIMIT %s + LIMIT %s OFFSET %s """, - (pid, days, limit), + (pid, days, limit, offset), ) else: cur.execute( @@ -68,9 +76,9 @@ def list_activity( WHERE profile_id=%s {quality_filter} ORDER BY date DESC, start_time DESC - LIMIT %s + LIMIT %s OFFSET %s """, - (pid, limit), + (pid, limit, offset), ) return [r2d(r) for r in cur.fetchall()] @@ -167,14 +175,23 @@ def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default @router.get("/stats") -def activity_stats(session: dict = Depends(require_auth)): +def activity_stats( + skip_quality_filter: bool = Query( + False, + description="True = Statistik-Kacheln ohne Profil-Qualitätsfilter (passend zur /activity-Liste).", + ), + session: dict = Depends(require_auth), +): """Get activity statistics (last 30 entries).""" pid = str(session["profile_id"]) with get_db() as conn: cur = get_cursor(conn) - cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) - profile = r2d(cur.fetchone()) - quality_filter = get_quality_filter_sql(profile or {}) + if skip_quality_filter: + quality_filter = "" + else: + cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) + profile = r2d(cur.fetchone()) + quality_filter = get_quality_filter_sql(profile or {}) cur.execute( f""" SELECT * FROM activity_log diff --git a/frontend/src/pages/ActivityPage.jsx b/frontend/src/pages/ActivityPage.jsx index 9b4b845..eeee7d6 100644 --- a/frontend/src/pages/ActivityPage.jsx +++ b/frontend/src/pages/ActivityPage.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, startTransition } from 'react' +import { useState, useEffect, useRef, useCallback, startTransition } from 'react' import { Upload, Pencil, Trash2, Check, X, CheckCircle } from 'lucide-react' import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts' import { api } from '../utils/api' @@ -9,6 +9,9 @@ import dayjs from 'dayjs' import 'dayjs/locale/de' dayjs.locale('de') +/** Erfassungsseite /activity: erste Ladung + „Mehr laden“ (ohne Qualitätsfilter, siehe Backend). */ +const ACTIVITY_LIST_PAGE_SIZE = 40 + const ACTIVITY_TYPES = [ 'Traditionelles Krafttraining','Matrial Arts','Outdoor Spaziergang', 'Innenräume Spaziergang','Laufen','Radfahren','Schwimmen', @@ -299,12 +302,48 @@ export default function ActivityPage() { const [metricDraft, setMetricDraft] = useState({}) const [sessionLoadError, setSessionLoadError] = useState(null) const [savingEdit, setSavingEdit] = useState(false) + const [listHasMore, setListHasMore] = useState(false) + const [listLoadingMore, setListLoadingMore] = useState(false) - const load = async () => { - const [e, s] = await Promise.all([api.listActivity(), api.activityStats()]) - setEntries(e); setStats(s) + const loadFirstPage = useCallback(async () => { + const n = ACTIVITY_LIST_PAGE_SIZE + const [e, s] = await Promise.all([ + api.listActivity(n, undefined, { skipQualityFilter: true, offset: 0 }), + api.activityStats({ skipQualityFilter: true }), + ]) + setEntries(e) + setStats(s) + setListHasMore(e.length === n) + }, []) + + const entriesRef = useRef(entries) + useEffect(() => { + entriesRef.current = entries + }, [entries]) + + const loadMore = async () => { + if (listLoadingMore || !listHasMore) return + setListLoadingMore(true) + try { + const n = ACTIVITY_LIST_PAGE_SIZE + const offset = entriesRef.current.length + const more = await api.listActivity(n, undefined, { + skipQualityFilter: true, + offset, + }) + if (more.length === 0) { + setListHasMore(false) + return + } + setEntries((prev) => [...prev, ...more]) + setListHasMore(more.length === n) + } finally { + setListLoadingMore(false) + } } + const load = loadFirstPage + const loadUsage = () => { api.getFeatureUsage().then(features => { const activityFeature = features.find(f => f.feature_id === 'activity_entries') @@ -312,11 +351,11 @@ export default function ActivityPage() { }).catch(err => console.error('Failed to load usage:', err)) } - useEffect(()=>{ - load() + useEffect(() => { + void loadFirstPage() loadUsage() api.getTrainingCategories().then(setCategories).catch(err => console.error('Failed to load categories:', err)) - },[]) + }, [loadFirstPage]) useEffect(() => { if (!editing?.id) { @@ -536,6 +575,11 @@ export default function ActivityPage() { {tab==='list' && (
+ Hier sind alle Trainings sichtbar (Profil-Qualitätsfilter aus — auch ohne Bewertung oder bei + abweichender Einordnung). Unter „Verlauf“ / Auswertung bleibt der Filter aktiv. Es werden jeweils bis zu{' '} + {ACTIVITY_LIST_PAGE_SIZE} Einträge nachgeladen. +
{entries.length===0 && (