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")
|
||||
)
|
||||
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 (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</div>
|
||||
|
||||
{/* Übersicht */}
|
||||
{stats && stats.count>0 && (
|
||||
{stats && (stats.total_in_profile > 0 || stats.count > 0) && (
|
||||
<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'}}>
|
||||
{[['Trainings',stats.count,'var(--text1)'],
|
||||
['Kcal gesamt',Math.round(stats.total_kcal),'#EF9F27'],
|
||||
['Stunden',Math.round(stats.total_min/60*10)/10,'#378ADD']].map(([l,v,c])=>(
|
||||
{[
|
||||
['Neueste (max. 30)', stats.count, 'var(--text1)'],
|
||||
['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 style={{fontSize:18,fontWeight:700,color:c}}>{v}</div>
|
||||
<div style={{fontSize:10,color:'var(--text3)'}}>{l}</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user