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,
|
||||
end_time=end_str or None,
|
||||
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_category=training_category,
|
||||
training_subcategory=training_subcategory,
|
||||
|
|
@ -915,7 +921,14 @@ def _import_activity(
|
|||
cur,
|
||||
profile_id,
|
||||
str(aid),
|
||||
workout_date=iso,
|
||||
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(
|
||||
cur,
|
||||
|
|
|
|||
|
|
@ -216,17 +216,18 @@ def insert_activity_csv_minimal(
|
|||
start_time: Any,
|
||||
end_time: Any,
|
||||
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_category: Any,
|
||||
training_subcategory: Any,
|
||||
source: str,
|
||||
) -> None:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
"""INSERT activity_log-Zeile (Universal-CSV): Kernspalten im INSERT; optional zusätzliches PATCH."""
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO activity_log (
|
||||
|
|
@ -234,7 +235,7 @@ def insert_activity_csv_minimal(
|
|||
kcal_active, kcal_resting, hr_avg, hr_max, distance_km,
|
||||
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,
|
||||
|
|
@ -243,6 +244,12 @@ def insert_activity_csv_minimal(
|
|||
start_time,
|
||||
end_time,
|
||||
activity_type,
|
||||
duration_min,
|
||||
kcal_active,
|
||||
kcal_resting,
|
||||
hr_avg,
|
||||
hr_max,
|
||||
distance_km,
|
||||
source,
|
||||
training_type_id,
|
||||
training_category,
|
||||
|
|
@ -280,32 +287,37 @@ def run_activity_post_write_hooks_import(
|
|||
profile_id: 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:
|
||||
"""Auto-Eval nach Import — liest die Session aus der DB (gleiche Felder wie REST-Hook)."""
|
||||
if not _EVALUATION_AVAILABLE or not _evaluate_and_save_activity:
|
||||
return
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, profile_id, date, training_type_id, duration_min,
|
||||
hr_avg, hr_max, distance_km, kcal_active, kcal_resting,
|
||||
rpe, pace_min_per_km, cadence, elevation_gain
|
||||
FROM activity_log
|
||||
WHERE id = %s AND profile_id = %s
|
||||
""",
|
||||
(eid, profile_id),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return
|
||||
activity_dict = dict(row)
|
||||
tid = training_type_id if training_type_id is not None else activity_dict.get("training_type_id")
|
||||
if not tid:
|
||||
return
|
||||
try:
|
||||
_evaluate_and_save_activity(cur, eid, activity_dict, int(tid), profile_id)
|
||||
except Exception as eval_err:
|
||||
logger.warning("[activity import] Auto-Eval fehlgeschlagen: %s", eval_err)
|
||||
"""Auto-Eval nach Import (gleiche Transaktion wie Schreibpfad — keine Abhängigkeit vom DB-Read-Timing)."""
|
||||
if _EVALUATION_AVAILABLE and training_type_id and _evaluate_and_save_activity:
|
||||
try:
|
||||
activity_dict = {
|
||||
"id": eid,
|
||||
"profile_id": profile_id,
|
||||
"date": workout_date,
|
||||
"training_type_id": training_type_id,
|
||||
"duration_min": duration_min,
|
||||
"hr_avg": hr_avg,
|
||||
"hr_max": hr_max,
|
||||
"distance_km": distance_km,
|
||||
"kcal_active": kcal_active,
|
||||
"kcal_resting": kcal_resting,
|
||||
"rpe": None,
|
||||
"pace_min_per_km": None,
|
||||
"cadence": None,
|
||||
"elevation_gain": None,
|
||||
}
|
||||
_evaluate_and_save_activity(cur, eid, activity_dict, training_type_id, profile_id)
|
||||
except Exception as eval_err:
|
||||
logger.warning("[activity import] Auto-Eval fehlgeschlagen: %s", eval_err)
|
||||
|
||||
|
||||
def merge_activity_csv_module_fields(
|
||||
|
|
|
|||
|
|
@ -641,7 +641,14 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional
|
|||
cur,
|
||||
pid,
|
||||
str(existing_id),
|
||||
workout_date=workout_date,
|
||||
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:
|
||||
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,
|
||||
end_time=row.get("End", "") or None,
|
||||
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_category=training_category,
|
||||
training_subcategory=training_subcategory,
|
||||
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
|
||||
run_activity_post_write_hooks_import(
|
||||
cur,
|
||||
pid,
|
||||
new_id,
|
||||
workout_date=workout_date,
|
||||
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:
|
||||
logger.warning(f"Import row failed: {e}")
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ const ACTIVITY_LOG_PAYLOAD_KEYS = new Set([
|
|||
'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([
|
||||
'duration_min',
|
||||
'kcal_active',
|
||||
|
|
@ -568,6 +568,27 @@ export default function ActivityPage() {
|
|||
setMetricDraft(m)
|
||||
}, [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 () => {
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
|
|
@ -612,7 +633,16 @@ export default function ActivityPage() {
|
|||
if (!col || !ACTIVITY_LOG_PAYLOAD_KEYS.has(col)) continue
|
||||
const useFormColumn = ACTIVITY_ENTRY_FORM_COLUMNS.has(col)
|
||||
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()
|
||||
if (rawStr === '') {
|
||||
payload[col] = null
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user