Bugfixes: Vitals Import (German columns + decimal values) #23
|
|
@ -310,8 +310,15 @@ async def import_omron_csv(
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(conn)
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
|
# Log available columns for debugging
|
||||||
|
first_row = True
|
||||||
|
|
||||||
for row in reader:
|
for row in reader:
|
||||||
try:
|
try:
|
||||||
|
if first_row:
|
||||||
|
logger.info(f"Omron CSV Columns: {list(row.keys())}")
|
||||||
|
first_row = False
|
||||||
|
|
||||||
# Parse Omron German date format
|
# Parse Omron German date format
|
||||||
date_str = row.get('Datum', row.get('Date'))
|
date_str = row.get('Datum', row.get('Date'))
|
||||||
time_str = row.get('Zeit', row.get('Time', '08:00'))
|
time_str = row.get('Zeit', row.get('Time', '08:00'))
|
||||||
|
|
@ -325,18 +332,27 @@ async def import_omron_csv(
|
||||||
errors += 1
|
errors += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract measurements
|
# Extract measurements (support column names with/without units)
|
||||||
systolic = row.get('Systolisch', row.get('Systolic'))
|
systolic = (row.get('Systolisch (mmHg)') or row.get('Systolisch') or
|
||||||
diastolic = row.get('Diastolisch', row.get('Diastolic'))
|
row.get('Systolic (mmHg)') or row.get('Systolic'))
|
||||||
pulse = row.get('Puls', row.get('Pulse'))
|
diastolic = (row.get('Diastolisch (mmHg)') or row.get('Diastolisch') or
|
||||||
|
row.get('Diastolic (mmHg)') or row.get('Diastolic'))
|
||||||
|
pulse = (row.get('Puls (bpm)') or row.get('Puls') or
|
||||||
|
row.get('Pulse (bpm)') or row.get('Pulse'))
|
||||||
|
|
||||||
if not systolic or not diastolic:
|
if not systolic or not diastolic:
|
||||||
|
logger.warning(f"Skipped row {date_str} {time_str}: Missing BP values (sys={systolic}, dia={diastolic})")
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Parse warning flags
|
# Parse warning flags (support various column names)
|
||||||
irregular = row.get('Unregelmäßiger Herzschlag', row.get('Irregular Heartbeat', ''))
|
irregular = (row.get('Unregelmäßiger Herzschlag festgestellt') or
|
||||||
afib = row.get('Vorhofflimmern', row.get('AFib', ''))
|
row.get('Unregelmäßiger Herzschlag') or
|
||||||
|
row.get('Irregular Heartbeat') or '')
|
||||||
|
afib = (row.get('Mögliches AFib') or
|
||||||
|
row.get('Vorhofflimmern') or
|
||||||
|
row.get('Possible AFib') or
|
||||||
|
row.get('AFib') or '')
|
||||||
|
|
||||||
irregular_heartbeat = irregular.lower() in ['ja', 'yes', 'true', '1']
|
irregular_heartbeat = irregular.lower() in ['ja', 'yes', 'true', '1']
|
||||||
possible_afib = afib.lower() in ['ja', 'yes', 'true', '1']
|
possible_afib = afib.lower() in ['ja', 'yes', 'true', '1']
|
||||||
|
|
@ -376,7 +392,10 @@ async def import_omron_csv(
|
||||||
))
|
))
|
||||||
|
|
||||||
result = cur.fetchone()
|
result = cur.fetchone()
|
||||||
if result and result['inserted']:
|
if result is None:
|
||||||
|
# WHERE clause prevented update (manual entry exists)
|
||||||
|
skipped += 1
|
||||||
|
elif result['inserted']:
|
||||||
inserted += 1
|
inserted += 1
|
||||||
else:
|
else:
|
||||||
updated += 1
|
updated += 1
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,27 @@ def get_baseline_stats(
|
||||||
# Import: Apple Health CSV
|
# Import: Apple Health CSV
|
||||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
def safe_int(value):
|
||||||
|
"""Safely parse string to int, handling decimals."""
|
||||||
|
if not value or value == '':
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
# If it has a decimal point, parse as float first then round to int
|
||||||
|
if '.' in str(value):
|
||||||
|
return int(float(value))
|
||||||
|
return int(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def safe_float(value):
|
||||||
|
"""Safely parse string to float."""
|
||||||
|
if not value or value == '':
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
@router.post("/import/apple-health")
|
@router.post("/import/apple-health")
|
||||||
async def import_apple_health_baseline(
|
async def import_apple_health_baseline(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
|
|
@ -307,26 +328,38 @@ async def import_apple_health_baseline(
|
||||||
updated = 0
|
updated = 0
|
||||||
skipped = 0
|
skipped = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
|
error_details = [] # Collect error messages
|
||||||
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(conn)
|
cur = get_cursor(conn)
|
||||||
|
|
||||||
|
# Log available columns for debugging
|
||||||
|
first_row = True
|
||||||
|
|
||||||
for row in reader:
|
for row in reader:
|
||||||
try:
|
try:
|
||||||
date = row.get('Start')[:10] if row.get('Start') else None
|
if first_row:
|
||||||
|
logger.info(f"CSV Columns: {list(row.keys())}")
|
||||||
|
first_row = False
|
||||||
|
|
||||||
|
# Support both English and German column names
|
||||||
|
date_raw = row.get('Start') or row.get('Datum/Uhrzeit')
|
||||||
|
date = date_raw[:10] if date_raw else None
|
||||||
if not date:
|
if not date:
|
||||||
|
logger.warning(f"Skipped row (no date): Start='{row.get('Start')}', Datum/Uhrzeit='{row.get('Datum/Uhrzeit')}'")
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Extract baseline vitals from Apple Health export
|
# Extract baseline vitals (support English + German column names)
|
||||||
rhr = row.get('Resting Heart Rate')
|
rhr = row.get('Resting Heart Rate') or row.get('Ruhepuls (count/min)')
|
||||||
hrv = row.get('Heart Rate Variability')
|
hrv = row.get('Heart Rate Variability') or row.get('Herzfrequenzvariabilität (ms)')
|
||||||
vo2 = row.get('VO2 Max')
|
vo2 = row.get('VO2 Max') or row.get('VO2 max (ml/(kg·min))')
|
||||||
spo2 = row.get('Oxygen Saturation')
|
spo2 = row.get('Oxygen Saturation') or row.get('Blutsauerstoffsättigung (%)')
|
||||||
resp_rate = row.get('Respiratory Rate')
|
resp_rate = row.get('Respiratory Rate') or row.get('Atemfrequenz (count/min)')
|
||||||
|
|
||||||
# Skip if no baseline vitals
|
# Skip if no baseline vitals
|
||||||
if not any([rhr, hrv, vo2, spo2, resp_rate]):
|
if not any([rhr, hrv, vo2, spo2, resp_rate]):
|
||||||
|
logger.warning(f"Skipped row {date} (no vitals): RHR={rhr}, HRV={hrv}, VO2={vo2}, SpO2={spo2}, RespRate={resp_rate}")
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -349,26 +382,33 @@ async def import_apple_health_baseline(
|
||||||
RETURNING (xmax = 0) AS inserted
|
RETURNING (xmax = 0) AS inserted
|
||||||
""", (
|
""", (
|
||||||
pid, date,
|
pid, date,
|
||||||
int(rhr) if rhr else None,
|
safe_int(rhr),
|
||||||
int(hrv) if hrv else None,
|
safe_int(hrv),
|
||||||
float(vo2) if vo2 else None,
|
safe_float(vo2),
|
||||||
int(spo2) if spo2 else None,
|
safe_int(spo2),
|
||||||
float(resp_rate) if resp_rate else None
|
safe_float(resp_rate)
|
||||||
))
|
))
|
||||||
|
|
||||||
result = cur.fetchone()
|
result = cur.fetchone()
|
||||||
if result and result['inserted']:
|
if result is None:
|
||||||
|
# WHERE clause prevented update (manual entry exists)
|
||||||
|
skipped += 1
|
||||||
|
elif result['inserted']:
|
||||||
inserted += 1
|
inserted += 1
|
||||||
else:
|
else:
|
||||||
updated += 1
|
updated += 1
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error importing row: {e}")
|
import traceback
|
||||||
|
error_msg = f"Row {date if 'date' in locals() else 'unknown'}: {str(e)}"
|
||||||
|
error_details.append(error_msg)
|
||||||
|
logger.error(f"{error_msg}\n{traceback.format_exc()}")
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"inserted": inserted,
|
"inserted": inserted,
|
||||||
"updated": updated,
|
"updated": updated,
|
||||||
"skipped": skipped,
|
"skipped": skipped,
|
||||||
"errors": errors
|
"errors": errors,
|
||||||
|
"error_details": error_details[:10] # Return first 10 errors
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -947,10 +947,18 @@ function ImportTab({ onImportComplete }) {
|
||||||
{error && <div style={{ padding: 10, background: '#FCEBEB', border: '1px solid #D85A30', borderRadius: 8, fontSize: 13, color: '#D85A30', marginBottom: 12 }}>{error}</div>}
|
{error && <div style={{ padding: 10, background: '#FCEBEB', border: '1px solid #D85A30', borderRadius: 8, fontSize: 13, color: '#D85A30', marginBottom: 12 }}>{error}</div>}
|
||||||
|
|
||||||
{result && (
|
{result && (
|
||||||
<div style={{ padding: 12, background: '#E8F7F0', border: '1px solid #1D9E75', borderRadius: 8, fontSize: 13, color: '#085041', marginBottom: 12 }}>
|
<div style={{ padding: 12, background: result.errors > 0 ? '#FCEBEB' : '#E8F7F0', border: `1px solid ${result.errors > 0 ? '#D85A30' : '#1D9E75'}`, borderRadius: 8, fontSize: 13, color: result.errors > 0 ? '#D85A30' : '#085041', marginBottom: 12 }}>
|
||||||
<strong>Import erfolgreich ({result.type === 'omron' ? 'Omron' : 'Apple Health'}):</strong><br />
|
<strong>Import {result.errors > 0 ? 'mit Fehlern' : 'erfolgreich'} ({result.type === 'omron' ? 'Omron' : 'Apple Health'}):</strong><br />
|
||||||
{result.inserted} neu · {result.updated} aktualisiert · {result.skipped} übersprungen
|
{result.inserted} neu · {result.updated} aktualisiert · {result.skipped} übersprungen
|
||||||
{result.errors > 0 && <> · {result.errors} Fehler</>}
|
{result.errors > 0 && <> · {result.errors} Fehler</>}
|
||||||
|
{result.error_details && result.error_details.length > 0 && (
|
||||||
|
<div style={{ marginTop: 8, padding: 8, background: 'rgba(0,0,0,0.05)', borderRadius: 4, fontSize: 12, fontFamily: 'monospace' }}>
|
||||||
|
<strong>Fehler-Details:</strong>
|
||||||
|
{result.error_details.map((err, i) => (
|
||||||
|
<div key={i} style={{ marginTop: 4 }}>• {err}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user