diff --git a/backend/data_layer/activity_session_metrics.py b/backend/data_layer/activity_session_metrics.py index ed6f335..a2459d2 100644 --- a/backend/data_layer/activity_session_metrics.py +++ b/backend/data_layer/activity_session_metrics.py @@ -356,7 +356,7 @@ def replace_activity_session_metrics( cur, row.get("training_category"), row.get("training_type_id") ) by_key = {s["key"]: s for s in schema} - payload_keys = set() + payload_by_key: Dict[str, Dict[str, Any]] = {} for item in metrics: raw_k = item.get("parameter_key") if raw_k is None or not str(raw_k).strip(): @@ -364,11 +364,15 @@ def replace_activity_session_metrics( k = str(raw_k).strip() if k not in by_key: raise ActivitySessionMetricsError(400, f"Unbekannter oder nicht zugewiesener Parameter: {k}") - payload_keys.add(k) + payload_by_key[k] = item for s in schema: - if s["required"] and s["key"] not in payload_keys: - raise ActivitySessionMetricsError(400, f"Pflichtfeld fehlt: {s['key']}") + if not s["required"]: + continue + itk = s["key"] + hit = payload_by_key.get(itk) + if hit is None or hit.get("value") is None: + raise ActivitySessionMetricsError(400, f"Pflichtfeld fehlt: {itk}") cur.execute( "DELETE FROM activity_session_metrics WHERE activity_log_id = %s", @@ -378,9 +382,14 @@ def replace_activity_session_metrics( for item in metrics: k = str(item["parameter_key"]).strip() spec = by_key[k] + val = item.get("value") + if val is None: + if spec["required"]: + raise ActivitySessionMetricsError(400, f"Pflichtfeld fehlt: {k}") + continue rules = _validation_rules_dict(spec["validation_rules"]) - _validate_single_value(spec["data_type"], item.get("value"), rules) - vn, vi, vt, vb = _row_value_tuple(spec["data_type"], item["value"]) + _validate_single_value(spec["data_type"], val, rules) + vn, vi, vt, vb = _row_value_tuple(spec["data_type"], val) cur.execute( """ INSERT INTO activity_session_metrics ( diff --git a/backend/routers/activity.py b/backend/routers/activity.py index d698c39..8237f1f 100644 --- a/backend/routers/activity.py +++ b/backend/routers/activity.py @@ -64,7 +64,7 @@ def list_activity( WHERE profile_id=%s {quality_filter} AND date >= (CURRENT_DATE - %s::integer) - ORDER BY date DESC, start_time DESC + ORDER BY date DESC, start_time DESC NULLS LAST, id DESC LIMIT %s OFFSET %s """, (pid, days, limit, offset), @@ -75,7 +75,7 @@ def list_activity( SELECT * FROM activity_log WHERE profile_id=%s {quality_filter} - ORDER BY date DESC, start_time DESC + ORDER BY date DESC, start_time DESC NULLS LAST, id DESC LIMIT %s OFFSET %s """, (pid, limit, offset), @@ -192,18 +192,30 @@ def activity_stats( 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 COUNT(*)::bigint AS c FROM activity_log WHERE profile_id=%s {quality_filter}", + (pid,), + ) + total_in_profile = int(cur.fetchone()["c"]) cur.execute( f""" SELECT * FROM activity_log WHERE profile_id=%s {quality_filter} - ORDER BY date DESC + ORDER BY date DESC, start_time DESC NULLS LAST, id DESC LIMIT 30 """, (pid,), ) rows = [r2d(r) for r in cur.fetchall()] if not rows: - return {"count": 0, "total_kcal": 0, "total_min": 0, "by_type": {}} + return { + "count": 0, + "sample_size": 0, + "total_in_profile": total_in_profile, + "total_kcal": 0, + "total_min": 0, + "by_type": {}, + } total_kcal = sum(float(r.get("kcal_active") or 0) for r in rows) total_min = sum(float(r.get("duration_min") or 0) for r in rows) by_type = {} @@ -215,6 +227,8 @@ def activity_stats( by_type[t]["min"] += float(r.get("duration_min") or 0) return { "count": len(rows), + "sample_size": len(rows), + "total_in_profile": total_in_profile, "total_kcal": round(total_kcal), "total_min": round(total_min), "by_type": by_type, diff --git a/frontend/src/pages/ActivityPage.jsx b/frontend/src/pages/ActivityPage.jsx index eeee7d6..ceb9f28 100644 --- a/frontend/src/pages/ActivityPage.jsx +++ b/frontend/src/pages/ActivityPage.jsx @@ -66,6 +66,7 @@ function buildMetricsPayload(schema, draft) { if (s.data_type === 'boolean') { if (raw === '' || raw === null || raw === undefined) { if (s.required) throw new Error(`Pflichtfeld: ${s.name_de}`) + out.push({ parameter_key: s.key, value: null }) continue } out.push({ parameter_key: s.key, value: !!raw }) @@ -73,6 +74,7 @@ function buildMetricsPayload(schema, draft) { } if (raw === '' || raw === null || raw === undefined) { if (s.required) throw new Error(`Pflichtfeld: ${s.name_de}`) + out.push({ parameter_key: s.key, value: null }) continue } let v = raw @@ -335,7 +337,14 @@ export default function ActivityPage() { setListHasMore(false) return } - setEntries((prev) => [...prev, ...more]) + const prev = entriesRef.current + const seen = new Set(prev.map((r) => r.id)) + const add = more.filter((r) => r.id && !seen.has(r.id)) + if (add.length === 0) { + setListHasMore(false) + return + } + setEntries((p) => [...p, ...add]) setListHasMore(more.length === n) } finally { setListLoadingMore(false) @@ -433,7 +442,10 @@ export default function ActivityPage() { if (!col || !ACTIVITY_LOG_PAYLOAD_KEYS.has(col)) continue if (!(s.key in metricDraft)) continue const raw = metricDraft[s.key] - if (raw === '' || raw === null || raw === undefined) continue + if (raw === '' || raw === null || raw === undefined) { + payload[col] = null + continue + } let v = raw if (s.data_type === 'integer') { v = parseInt(String(raw), 10) @@ -506,12 +518,19 @@ export default function ActivityPage() { {/* Übersicht */} - {stats && stats.count>0 && ( + {stats && (stats.total_in_profile > 0 || stats.count > 0) && (