feat: Enhance activity import functionality with additional metrics
- Updated the `_import_activity` function to include new metrics: duration_min, kcal_active, kcal_resting, hr_avg, hr_max, and distance_km during CSV imports. - Modified the `insert_activity_csv_minimal` function to accept and store these additional metrics in the activity log. - Enhanced the `run_activity_post_write_hooks_import` function to utilize the new metrics for auto-evaluation after activity imports. - Updated the activity import router to pass the new metrics from the CSV file to the database functions, ensuring comprehensive data handling. - Improved frontend handling of activity entry forms to accommodate the new metrics, enhancing user experience during activity log edits.
This commit is contained in:
parent
5cda485458
commit
7d6fdab812
|
|
@ -899,6 +899,12 @@ def _import_activity(
|
||||||
start_time=workout_start_t,
|
start_time=workout_start_t,
|
||||||
end_time=end_str or None,
|
end_time=end_str or None,
|
||||||
activity_type=wtype,
|
activity_type=wtype,
|
||||||
|
duration_min=registry_updates.get("duration_min"),
|
||||||
|
kcal_active=registry_updates.get("kcal_active"),
|
||||||
|
kcal_resting=registry_updates.get("kcal_resting"),
|
||||||
|
hr_avg=registry_updates.get("hr_avg"),
|
||||||
|
hr_max=registry_updates.get("hr_max"),
|
||||||
|
distance_km=registry_updates.get("distance_km"),
|
||||||
training_type_id=training_type_id,
|
training_type_id=training_type_id,
|
||||||
training_category=training_category,
|
training_category=training_category,
|
||||||
training_subcategory=training_subcategory,
|
training_subcategory=training_subcategory,
|
||||||
|
|
@ -915,7 +921,14 @@ def _import_activity(
|
||||||
cur,
|
cur,
|
||||||
profile_id,
|
profile_id,
|
||||||
str(aid),
|
str(aid),
|
||||||
|
workout_date=iso,
|
||||||
training_type_id=training_type_id,
|
training_type_id=training_type_id,
|
||||||
|
duration_min=registry_updates.get("duration_min"),
|
||||||
|
hr_avg=registry_updates.get("hr_avg"),
|
||||||
|
hr_max=registry_updates.get("hr_max"),
|
||||||
|
distance_km=registry_updates.get("distance_km"),
|
||||||
|
kcal_active=registry_updates.get("kcal_active"),
|
||||||
|
kcal_resting=registry_updates.get("kcal_resting"),
|
||||||
)
|
)
|
||||||
upsert_session_metrics_from_csv_mapped(
|
upsert_session_metrics_from_csv_mapped(
|
||||||
cur,
|
cur,
|
||||||
|
|
|
||||||
|
|
@ -216,17 +216,18 @@ def insert_activity_csv_minimal(
|
||||||
start_time: Any,
|
start_time: Any,
|
||||||
end_time: Any,
|
end_time: Any,
|
||||||
activity_type: str,
|
activity_type: str,
|
||||||
|
duration_min: Any,
|
||||||
|
kcal_active: Any,
|
||||||
|
kcal_resting: Any,
|
||||||
|
hr_avg: Any,
|
||||||
|
hr_max: Any,
|
||||||
|
distance_km: Any,
|
||||||
training_type_id: Any,
|
training_type_id: Any,
|
||||||
training_category: Any,
|
training_category: Any,
|
||||||
training_subcategory: Any,
|
training_subcategory: Any,
|
||||||
source: str,
|
source: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""INSERT activity_log-Zeile (Universal-CSV): Kernspalten im INSERT; optional zusätzliches PATCH."""
|
||||||
INSERT Kopfzeile für Universal-CSV / Legacy-Import.
|
|
||||||
|
|
||||||
Metriken aus ``activity_csv_registry_updates_from_mapped`` (oder manuelles Dict) —
|
|
||||||
ausschließlich via ``update_activity_columns``; keine fest verdrahteten hr_avg-Parameter.
|
|
||||||
"""
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO activity_log (
|
INSERT INTO activity_log (
|
||||||
|
|
@ -234,7 +235,7 @@ def insert_activity_csv_minimal(
|
||||||
kcal_active, kcal_resting, hr_avg, hr_max, distance_km,
|
kcal_active, kcal_resting, hr_avg, hr_max, distance_km,
|
||||||
source, training_type_id, training_category, training_subcategory, created
|
source, training_type_id, training_category, training_subcategory, created
|
||||||
)
|
)
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,NULL,NULL,NULL,NULL,NULL,NULL,%s,%s,%s,%s,CURRENT_TIMESTAMP)
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
eid,
|
eid,
|
||||||
|
|
@ -243,6 +244,12 @@ def insert_activity_csv_minimal(
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
activity_type,
|
activity_type,
|
||||||
|
duration_min,
|
||||||
|
kcal_active,
|
||||||
|
kcal_resting,
|
||||||
|
hr_avg,
|
||||||
|
hr_max,
|
||||||
|
distance_km,
|
||||||
source,
|
source,
|
||||||
training_type_id,
|
training_type_id,
|
||||||
training_category,
|
training_category,
|
||||||
|
|
@ -280,32 +287,37 @@ def run_activity_post_write_hooks_import(
|
||||||
profile_id: str,
|
profile_id: str,
|
||||||
eid: str,
|
eid: str,
|
||||||
*,
|
*,
|
||||||
training_type_id: Optional[int] = None,
|
workout_date: str,
|
||||||
|
training_type_id: Optional[int],
|
||||||
|
duration_min: Any,
|
||||||
|
hr_avg: Any,
|
||||||
|
hr_max: Any,
|
||||||
|
distance_km: Any,
|
||||||
|
kcal_active: Any,
|
||||||
|
kcal_resting: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Auto-Eval nach Import — liest die Session aus der DB (gleiche Felder wie REST-Hook)."""
|
"""Auto-Eval nach Import (gleiche Transaktion wie Schreibpfad — keine Abhängigkeit vom DB-Read-Timing)."""
|
||||||
if not _EVALUATION_AVAILABLE or not _evaluate_and_save_activity:
|
if _EVALUATION_AVAILABLE and training_type_id and _evaluate_and_save_activity:
|
||||||
return
|
try:
|
||||||
cur.execute(
|
activity_dict = {
|
||||||
"""
|
"id": eid,
|
||||||
SELECT id, profile_id, date, training_type_id, duration_min,
|
"profile_id": profile_id,
|
||||||
hr_avg, hr_max, distance_km, kcal_active, kcal_resting,
|
"date": workout_date,
|
||||||
rpe, pace_min_per_km, cadence, elevation_gain
|
"training_type_id": training_type_id,
|
||||||
FROM activity_log
|
"duration_min": duration_min,
|
||||||
WHERE id = %s AND profile_id = %s
|
"hr_avg": hr_avg,
|
||||||
""",
|
"hr_max": hr_max,
|
||||||
(eid, profile_id),
|
"distance_km": distance_km,
|
||||||
)
|
"kcal_active": kcal_active,
|
||||||
row = cur.fetchone()
|
"kcal_resting": kcal_resting,
|
||||||
if not row:
|
"rpe": None,
|
||||||
return
|
"pace_min_per_km": None,
|
||||||
activity_dict = dict(row)
|
"cadence": None,
|
||||||
tid = training_type_id if training_type_id is not None else activity_dict.get("training_type_id")
|
"elevation_gain": None,
|
||||||
if not tid:
|
}
|
||||||
return
|
_evaluate_and_save_activity(cur, eid, activity_dict, training_type_id, profile_id)
|
||||||
try:
|
except Exception as eval_err:
|
||||||
_evaluate_and_save_activity(cur, eid, activity_dict, int(tid), profile_id)
|
logger.warning("[activity import] Auto-Eval fehlgeschlagen: %s", eval_err)
|
||||||
except Exception as eval_err:
|
|
||||||
logger.warning("[activity import] Auto-Eval fehlgeschlagen: %s", eval_err)
|
|
||||||
|
|
||||||
|
|
||||||
def merge_activity_csv_module_fields(
|
def merge_activity_csv_module_fields(
|
||||||
|
|
|
||||||
|
|
@ -641,7 +641,14 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional
|
||||||
cur,
|
cur,
|
||||||
pid,
|
pid,
|
||||||
str(existing_id),
|
str(existing_id),
|
||||||
|
workout_date=workout_date,
|
||||||
training_type_id=training_type_id,
|
training_type_id=training_type_id,
|
||||||
|
duration_min=duration_min,
|
||||||
|
hr_avg=hr_av,
|
||||||
|
hr_max=hr_mx,
|
||||||
|
distance_km=dist_km,
|
||||||
|
kcal_active=kcal_a,
|
||||||
|
kcal_resting=kcal_r,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
new_id = new_activity_id()
|
new_id = new_activity_id()
|
||||||
|
|
@ -653,31 +660,30 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional
|
||||||
start_time=workout_start_t,
|
start_time=workout_start_t,
|
||||||
end_time=row.get("End", "") or None,
|
end_time=row.get("End", "") or None,
|
||||||
activity_type=wtype,
|
activity_type=wtype,
|
||||||
|
duration_min=duration_min,
|
||||||
|
kcal_active=kcal_a,
|
||||||
|
kcal_resting=kcal_r,
|
||||||
|
hr_avg=hr_av,
|
||||||
|
hr_max=hr_mx,
|
||||||
|
distance_km=dist_km,
|
||||||
training_type_id=training_type_id,
|
training_type_id=training_type_id,
|
||||||
training_category=training_category,
|
training_category=training_category,
|
||||||
training_subcategory=training_subcategory,
|
training_subcategory=training_subcategory,
|
||||||
source="apple_health",
|
source="apple_health",
|
||||||
)
|
)
|
||||||
apple_metrics = {
|
|
||||||
k: v
|
|
||||||
for k, v in {
|
|
||||||
"duration_min": duration_min,
|
|
||||||
"kcal_active": kcal_a,
|
|
||||||
"kcal_resting": kcal_r,
|
|
||||||
"hr_avg": hr_av,
|
|
||||||
"hr_max": hr_mx,
|
|
||||||
"distance_km": dist_km,
|
|
||||||
}.items()
|
|
||||||
if v is not None
|
|
||||||
}
|
|
||||||
if apple_metrics:
|
|
||||||
update_activity_columns(cur, pid, new_id, apple_metrics)
|
|
||||||
inserted += 1
|
inserted += 1
|
||||||
run_activity_post_write_hooks_import(
|
run_activity_post_write_hooks_import(
|
||||||
cur,
|
cur,
|
||||||
pid,
|
pid,
|
||||||
new_id,
|
new_id,
|
||||||
|
workout_date=workout_date,
|
||||||
training_type_id=training_type_id,
|
training_type_id=training_type_id,
|
||||||
|
duration_min=duration_min,
|
||||||
|
hr_avg=hr_av,
|
||||||
|
hr_max=hr_mx,
|
||||||
|
distance_km=dist_km,
|
||||||
|
kcal_active=kcal_a,
|
||||||
|
kcal_resting=kcal_r,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Import row failed: {e}")
|
logger.warning(f"Import row failed: {e}")
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ const ACTIVITY_LOG_PAYLOAD_KEYS = new Set([
|
||||||
'training_subcategory',
|
'training_subcategory',
|
||||||
])
|
])
|
||||||
|
|
||||||
/** activity_log-Spalten, die im EntryForm editiert werden (nicht aus metricDraft überschreiben). */
|
/** activity_log-Spalten, die im EntryForm editiert werden (Kurzform). */
|
||||||
const ACTIVITY_ENTRY_FORM_COLUMNS = new Set([
|
const ACTIVITY_ENTRY_FORM_COLUMNS = new Set([
|
||||||
'duration_min',
|
'duration_min',
|
||||||
'kcal_active',
|
'kcal_active',
|
||||||
|
|
@ -568,6 +568,27 @@ export default function ActivityPage() {
|
||||||
setMetricDraft(m)
|
setMetricDraft(m)
|
||||||
}, [sessionDetail])
|
}, [sessionDetail])
|
||||||
|
|
||||||
|
/** Nach GET /activity/:id: Kopf-Spalten ins EntryForm, wenn die Listenzeile leer war (Parameter-Keys vs. Spalten). */
|
||||||
|
useEffect(() => {
|
||||||
|
const h = sessionDetail?.header
|
||||||
|
if (!h?.id || !editing?.id) return
|
||||||
|
if (String(h.id) !== String(editing.id)) return
|
||||||
|
setEditing((prev) => {
|
||||||
|
if (String(h.id) !== String(prev.id)) return prev
|
||||||
|
let changed = false
|
||||||
|
const next = { ...prev }
|
||||||
|
for (const col of ACTIVITY_ENTRY_FORM_COLUMNS) {
|
||||||
|
const cur = next[col]
|
||||||
|
const empty = cur === null || cur === undefined || cur === ''
|
||||||
|
if (empty && h[col] != null && h[col] !== '') {
|
||||||
|
next[col] = h[col]
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed ? next : prev
|
||||||
|
})
|
||||||
|
}, [sessionDetail, editing?.id])
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
@ -612,7 +633,16 @@ export default function ActivityPage() {
|
||||||
if (!col || !ACTIVITY_LOG_PAYLOAD_KEYS.has(col)) continue
|
if (!col || !ACTIVITY_LOG_PAYLOAD_KEYS.has(col)) continue
|
||||||
const useFormColumn = ACTIVITY_ENTRY_FORM_COLUMNS.has(col)
|
const useFormColumn = ACTIVITY_ENTRY_FORM_COLUMNS.has(col)
|
||||||
if (!useFormColumn && !(s.key in metricDraft)) continue
|
if (!useFormColumn && !(s.key in metricDraft)) continue
|
||||||
const raw = useFormColumn ? editing[col] : metricDraft[s.key]
|
let raw
|
||||||
|
if (useFormColumn) {
|
||||||
|
const fromForm = editing[col]
|
||||||
|
const fromDraft = metricDraft[s.key]
|
||||||
|
const formEmpty = fromForm === null || fromForm === undefined || fromForm === ''
|
||||||
|
const draftEmpty = fromDraft === null || fromDraft === undefined || fromDraft === ''
|
||||||
|
raw = !formEmpty ? fromForm : !draftEmpty ? fromDraft : fromForm
|
||||||
|
} else {
|
||||||
|
raw = metricDraft[s.key]
|
||||||
|
}
|
||||||
const rawStr = raw === null || raw === undefined ? '' : String(raw).trim()
|
const rawStr = raw === null || raw === undefined ? '' : String(raw).trim()
|
||||||
if (rawStr === '') {
|
if (rawStr === '') {
|
||||||
payload[col] = null
|
payload[col] = null
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user