feat: Refactor activity session metrics handling and enhance activity listing
- Updated the `replace_activity_session_metrics` function to improve validation logic and error handling for required fields. - Enhanced the activity listing query to order results by date, start time, and ID, ensuring consistent output. - Modified the frontend to handle null values in metrics payload and improved the display of activity statistics, including total entries in profile and sample size.
This commit is contained in:
parent
1f51c32521
commit
9fdb02ff8b
|
|
@ -356,7 +356,7 @@ def replace_activity_session_metrics(
|
||||||
cur, row.get("training_category"), row.get("training_type_id")
|
cur, row.get("training_category"), row.get("training_type_id")
|
||||||
)
|
)
|
||||||
by_key = {s["key"]: s for s in schema}
|
by_key = {s["key"]: s for s in schema}
|
||||||
payload_keys = set()
|
payload_by_key: Dict[str, Dict[str, Any]] = {}
|
||||||
for item in metrics:
|
for item in metrics:
|
||||||
raw_k = item.get("parameter_key")
|
raw_k = item.get("parameter_key")
|
||||||
if raw_k is None or not str(raw_k).strip():
|
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()
|
k = str(raw_k).strip()
|
||||||
if k not in by_key:
|
if k not in by_key:
|
||||||
raise ActivitySessionMetricsError(400, f"Unbekannter oder nicht zugewiesener Parameter: {k}")
|
raise ActivitySessionMetricsError(400, f"Unbekannter oder nicht zugewiesener Parameter: {k}")
|
||||||
payload_keys.add(k)
|
payload_by_key[k] = item
|
||||||
|
|
||||||
for s in schema:
|
for s in schema:
|
||||||
if s["required"] and s["key"] not in payload_keys:
|
if not s["required"]:
|
||||||
raise ActivitySessionMetricsError(400, f"Pflichtfeld fehlt: {s['key']}")
|
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(
|
cur.execute(
|
||||||
"DELETE FROM activity_session_metrics WHERE activity_log_id = %s",
|
"DELETE FROM activity_session_metrics WHERE activity_log_id = %s",
|
||||||
|
|
@ -378,9 +382,14 @@ def replace_activity_session_metrics(
|
||||||
for item in metrics:
|
for item in metrics:
|
||||||
k = str(item["parameter_key"]).strip()
|
k = str(item["parameter_key"]).strip()
|
||||||
spec = by_key[k]
|
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"])
|
rules = _validation_rules_dict(spec["validation_rules"])
|
||||||
_validate_single_value(spec["data_type"], item.get("value"), rules)
|
_validate_single_value(spec["data_type"], val, rules)
|
||||||
vn, vi, vt, vb = _row_value_tuple(spec["data_type"], item["value"])
|
vn, vi, vt, vb = _row_value_tuple(spec["data_type"], val)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO activity_session_metrics (
|
INSERT INTO activity_session_metrics (
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ def list_activity(
|
||||||
WHERE profile_id=%s
|
WHERE profile_id=%s
|
||||||
{quality_filter}
|
{quality_filter}
|
||||||
AND date >= (CURRENT_DATE - %s::integer)
|
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
|
LIMIT %s OFFSET %s
|
||||||
""",
|
""",
|
||||||
(pid, days, limit, offset),
|
(pid, days, limit, offset),
|
||||||
|
|
@ -75,7 +75,7 @@ def list_activity(
|
||||||
SELECT * FROM activity_log
|
SELECT * FROM activity_log
|
||||||
WHERE profile_id=%s
|
WHERE profile_id=%s
|
||||||
{quality_filter}
|
{quality_filter}
|
||||||
ORDER BY date DESC, start_time DESC
|
ORDER BY date DESC, start_time DESC NULLS LAST, id DESC
|
||||||
LIMIT %s OFFSET %s
|
LIMIT %s OFFSET %s
|
||||||
""",
|
""",
|
||||||
(pid, limit, offset),
|
(pid, limit, offset),
|
||||||
|
|
@ -192,18 +192,30 @@ def activity_stats(
|
||||||
cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,))
|
cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,))
|
||||||
profile = r2d(cur.fetchone())
|
profile = r2d(cur.fetchone())
|
||||||
quality_filter = get_quality_filter_sql(profile or {})
|
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(
|
cur.execute(
|
||||||
f"""
|
f"""
|
||||||
SELECT * FROM activity_log
|
SELECT * FROM activity_log
|
||||||
WHERE profile_id=%s {quality_filter}
|
WHERE profile_id=%s {quality_filter}
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC, start_time DESC NULLS LAST, id DESC
|
||||||
LIMIT 30
|
LIMIT 30
|
||||||
""",
|
""",
|
||||||
(pid,),
|
(pid,),
|
||||||
)
|
)
|
||||||
rows = [r2d(r) for r in cur.fetchall()]
|
rows = [r2d(r) for r in cur.fetchall()]
|
||||||
if not rows:
|
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_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)
|
total_min = sum(float(r.get("duration_min") or 0) for r in rows)
|
||||||
by_type = {}
|
by_type = {}
|
||||||
|
|
@ -215,6 +227,8 @@ def activity_stats(
|
||||||
by_type[t]["min"] += float(r.get("duration_min") or 0)
|
by_type[t]["min"] += float(r.get("duration_min") or 0)
|
||||||
return {
|
return {
|
||||||
"count": len(rows),
|
"count": len(rows),
|
||||||
|
"sample_size": len(rows),
|
||||||
|
"total_in_profile": total_in_profile,
|
||||||
"total_kcal": round(total_kcal),
|
"total_kcal": round(total_kcal),
|
||||||
"total_min": round(total_min),
|
"total_min": round(total_min),
|
||||||
"by_type": by_type,
|
"by_type": by_type,
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ function buildMetricsPayload(schema, draft) {
|
||||||
if (s.data_type === 'boolean') {
|
if (s.data_type === 'boolean') {
|
||||||
if (raw === '' || raw === null || raw === undefined) {
|
if (raw === '' || raw === null || raw === undefined) {
|
||||||
if (s.required) throw new Error(`Pflichtfeld: ${s.name_de}`)
|
if (s.required) throw new Error(`Pflichtfeld: ${s.name_de}`)
|
||||||
|
out.push({ parameter_key: s.key, value: null })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out.push({ parameter_key: s.key, value: !!raw })
|
out.push({ parameter_key: s.key, value: !!raw })
|
||||||
|
|
@ -73,6 +74,7 @@ function buildMetricsPayload(schema, draft) {
|
||||||
}
|
}
|
||||||
if (raw === '' || raw === null || raw === undefined) {
|
if (raw === '' || raw === null || raw === undefined) {
|
||||||
if (s.required) throw new Error(`Pflichtfeld: ${s.name_de}`)
|
if (s.required) throw new Error(`Pflichtfeld: ${s.name_de}`)
|
||||||
|
out.push({ parameter_key: s.key, value: null })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let v = raw
|
let v = raw
|
||||||
|
|
@ -335,7 +337,14 @@ export default function ActivityPage() {
|
||||||
setListHasMore(false)
|
setListHasMore(false)
|
||||||
return
|
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)
|
setListHasMore(more.length === n)
|
||||||
} finally {
|
} finally {
|
||||||
setListLoadingMore(false)
|
setListLoadingMore(false)
|
||||||
|
|
@ -433,7 +442,10 @@ export default function ActivityPage() {
|
||||||
if (!col || !ACTIVITY_LOG_PAYLOAD_KEYS.has(col)) continue
|
if (!col || !ACTIVITY_LOG_PAYLOAD_KEYS.has(col)) continue
|
||||||
if (!(s.key in metricDraft)) continue
|
if (!(s.key in metricDraft)) continue
|
||||||
const raw = metricDraft[s.key]
|
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
|
let v = raw
|
||||||
if (s.data_type === 'integer') {
|
if (s.data_type === 'integer') {
|
||||||
v = parseInt(String(raw), 10)
|
v = parseInt(String(raw), 10)
|
||||||
|
|
@ -506,12 +518,19 @@ export default function ActivityPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Übersicht */}
|
{/* Übersicht */}
|
||||||
{stats && stats.count>0 && (
|
{stats && (stats.total_in_profile > 0 || stats.count > 0) && (
|
||||||
<div className="card section-gap">
|
<div className="card section-gap">
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 8, lineHeight: 1.45 }}>
|
||||||
|
<strong>{stats.total_in_profile ?? '–'}</strong> Einträge im Profil (gleicher Filter wie diese Seite). Die Summen
|
||||||
|
Kcal/Stunden beziehen sich auf die <strong>neuesten {stats.sample_size ?? stats.count}</strong> Einträge (max.
|
||||||
|
30).
|
||||||
|
</div>
|
||||||
<div style={{display:'flex',gap:8,flexWrap:'wrap'}}>
|
<div style={{display:'flex',gap:8,flexWrap:'wrap'}}>
|
||||||
{[['Trainings',stats.count,'var(--text1)'],
|
{[
|
||||||
['Kcal gesamt',Math.round(stats.total_kcal),'#EF9F27'],
|
['Neueste (max. 30)', stats.count, 'var(--text1)'],
|
||||||
['Stunden',Math.round(stats.total_min/60*10)/10,'#378ADD']].map(([l,v,c])=>(
|
['Kcal (darin)', Math.round(stats.total_kcal), '#EF9F27'],
|
||||||
|
['Stunden (darin)', Math.round(stats.total_min / 60 * 10) / 10, '#378ADD'],
|
||||||
|
].map(([l, v, c]) => (
|
||||||
<div key={l} style={{flex:1,minWidth:80,background:'var(--surface2)',borderRadius:8,padding:'8px 10px',textAlign:'center'}}>
|
<div key={l} style={{flex:1,minWidth:80,background:'var(--surface2)',borderRadius:8,padding:'8px 10px',textAlign:'center'}}>
|
||||||
<div style={{fontSize:18,fontWeight:700,color:c}}>{v}</div>
|
<div style={{fontSize:18,fontWeight:700,color:c}}>{v}</div>
|
||||||
<div style={{fontSize:10,color:'var(--text3)'}}>{l}</div>
|
<div style={{fontSize:10,color:'var(--text3)'}}>{l}</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user