Erste Version - Universal CSV Importer für EAV und activity_log #85
|
|
@ -391,8 +391,8 @@ def replace_activity_session_metrics(
|
|||
(activity_log_id, spec["training_parameter_id"], vn, vi, vt, vb),
|
||||
)
|
||||
|
||||
# Übergang: Spalten in activity_log sind maßgeblich — EAV für alle source_field-Parameter angleichen
|
||||
sync_column_backed_session_metrics(cur, profile_id, activity_log_id)
|
||||
# Kein sync_column_backed nach PUT /metrics: der Request ist maßgeblich für EAV. Ein Spalten-Sync würde
|
||||
# Werte aus nicht mitgeschriebenen activity_log-Spalten wieder verwerfen.
|
||||
|
||||
return fetch_activity_session_metrics(cur, activity_log_id)
|
||||
|
||||
|
|
@ -408,10 +408,40 @@ def get_activity_session_logical_unit(cur, profile_id: str, activity_log_id: str
|
|||
cur, header.get("training_category"), header.get("training_type_id")
|
||||
)
|
||||
metrics = fetch_activity_session_metrics(cur, activity_log_id)
|
||||
by_key = {m["key"]: m for m in metrics}
|
||||
merged_metrics: List[Dict[str, Any]] = list(metrics)
|
||||
for s in schema:
|
||||
k = s["key"]
|
||||
if k in by_key:
|
||||
continue
|
||||
sf = s.get("source_field")
|
||||
if not sf or (isinstance(sf, str) and not str(sf).strip()):
|
||||
continue
|
||||
col = str(sf).strip()
|
||||
if col not in header:
|
||||
continue
|
||||
raw = header.get(col)
|
||||
if raw is None:
|
||||
continue
|
||||
dt = s["data_type"]
|
||||
try:
|
||||
val = _coerce_raw_value_for_parameter(dt, raw)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
merged_metrics.append(
|
||||
{
|
||||
"training_parameter_id": s["training_parameter_id"],
|
||||
"key": k,
|
||||
"data_type": dt,
|
||||
"unit": s.get("unit"),
|
||||
"value": val,
|
||||
}
|
||||
)
|
||||
merged_metrics.sort(key=lambda x: x["key"])
|
||||
return {
|
||||
"header": header,
|
||||
"schema": schema,
|
||||
"metrics": metrics,
|
||||
"metrics": merged_metrics,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -83,8 +83,17 @@ class ActivityEntry(BaseModel):
|
|||
kcal_resting: Optional[float] = None
|
||||
hr_avg: Optional[float] = None
|
||||
hr_max: Optional[float] = None
|
||||
hr_min: Optional[int] = None # DB-Spalte hr_min (Parameter min_hr)
|
||||
distance_km: Optional[float] = None
|
||||
rpe: Optional[int] = None
|
||||
pace_min_per_km: Optional[float] = None
|
||||
cadence: Optional[int] = None
|
||||
avg_power: Optional[int] = None
|
||||
elevation_gain: Optional[int] = None
|
||||
temperature_celsius: Optional[float] = None
|
||||
humidity_percent: Optional[int] = None
|
||||
avg_hr_percent: Optional[float] = None
|
||||
kcal_per_km: Optional[float] = None
|
||||
source: Optional[str] = 'manual'
|
||||
notes: Optional[str] = None
|
||||
training_type_id: Optional[int] = None # v9d: Training type categorization
|
||||
|
|
|
|||
|
|
@ -102,9 +102,10 @@ def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default
|
|||
cur.execute(
|
||||
"""INSERT INTO activity_log
|
||||
(id,profile_id,date,start_time,end_time,activity_type,duration_min,kcal_active,kcal_resting,
|
||||
hr_avg,hr_max,distance_km,rpe,source,notes,
|
||||
hr_avg,hr_max,hr_min,distance_km,pace_min_per_km,cadence,avg_power,elevation_gain,
|
||||
temperature_celsius,humidity_percent,avg_hr_percent,kcal_per_km,rpe,source,notes,
|
||||
training_type_id,training_category,training_subcategory,created)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP)""",
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP)""",
|
||||
(
|
||||
eid,
|
||||
pid,
|
||||
|
|
@ -117,7 +118,16 @@ def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default
|
|||
d["kcal_resting"],
|
||||
d["hr_avg"],
|
||||
d["hr_max"],
|
||||
d.get("hr_min"),
|
||||
d["distance_km"],
|
||||
d.get("pace_min_per_km"),
|
||||
d.get("cadence"),
|
||||
d.get("avg_power"),
|
||||
d.get("elevation_gain"),
|
||||
d.get("temperature_celsius"),
|
||||
d.get("humidity_percent"),
|
||||
d.get("avg_hr_percent"),
|
||||
d.get("kcal_per_km"),
|
||||
d["rpe"],
|
||||
d["source"],
|
||||
d["notes"],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, 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'
|
||||
|
|
@ -26,7 +26,16 @@ const ACTIVITY_LOG_PAYLOAD_KEYS = new Set([
|
|||
'kcal_resting',
|
||||
'hr_avg',
|
||||
'hr_max',
|
||||
'hr_min',
|
||||
'distance_km',
|
||||
'pace_min_per_km',
|
||||
'cadence',
|
||||
'avg_power',
|
||||
'elevation_gain',
|
||||
'temperature_celsius',
|
||||
'humidity_percent',
|
||||
'avg_hr_percent',
|
||||
'kcal_per_km',
|
||||
'rpe',
|
||||
'source',
|
||||
'notes',
|
||||
|
|
@ -412,7 +421,9 @@ export default function ActivityPage() {
|
|||
}
|
||||
setEditing(null)
|
||||
setSessionDetail(null)
|
||||
await load()
|
||||
startTransition(() => {
|
||||
void load()
|
||||
})
|
||||
} catch (err) {
|
||||
setError(err.message || 'Speichern fehlgeschlagen')
|
||||
setTimeout(() => setError(null), 6000)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user