import type { BiomarkersResponse, MarkerGroup, Night, SleepApiResponse, SleepCycleExport, Week, Workout, } from './types'; export interface HcSleepRow { id: string; started_at: string; ended_at: string; total_seconds: number; sleep_seconds: number; deep_seconds: number; rem_seconds: number; light_seconds: number; awake_seconds: number; } export function shapeSleep( api: SleepApiResponse, sc: SleepCycleExport, fitbit: HcSleepRow[] = [], ): Night[] { const byDate = new Map(); (sc.sessions || []).forEach((r) => { const o: Record = Object.fromEntries(sc.fields.map((f, i) => [f, r[i]])); const date = String(o.end || '').slice(0, 10); if (!date) return; byDate.set(date, { date, total: o.inbed_s || o.asleep_s, asleep: o.asleep_s, deep: o.deep_s, rem: o.rem_s, light: o.light_s || (o.deep_s || o.rem_s ? 0 : o.asleep_s), awake: o.awake_s, quality: o.quality, hrv: null, hr: null, src: 'Sleep Cycle', }); }); (api.sessions || []).forEach((s) => { const date = s.ended_at.slice(0, 10); byDate.set(date, { date, total: s.total_seconds, asleep: s.sleep_seconds, deep: s.deep_seconds, rem: s.rem_seconds, light: s.light_seconds, awake: s.awake_seconds, quality: null, hrv: s.avg_hrv, hr: s.avg_hr, src: 'Eight Sleep', }); }); // Fitbit Air wins where sources overlap — it's the current device. fitbit.forEach((f) => { const date = f.ended_at.slice(0, 10); byDate.set(date, { date, total: f.total_seconds, asleep: f.sleep_seconds, deep: f.deep_seconds, rem: f.rem_seconds, light: f.light_seconds, awake: f.awake_seconds, quality: null, hrv: null, hr: null, src: 'Fitbit Air', }); }); return [...byDate.values()].sort((a, b) => (a.date < b.date ? -1 : 1)); } export function workoutVolume(w: Workout): { vol: number; sets: number } { let vol = 0; let sets = 0; (w.exercises || []).forEach((ex) => (ex.sets || []).forEach((s) => { sets++; if (s.weight_kg && s.reps) vol += s.weight_kg * s.reps; }), ); return { vol, sets }; } export function weekStart(d: string): string { const dt = new Date(d); const day = (dt.getDay() + 6) % 7; dt.setDate(dt.getDate() - day); return dt.toISOString().slice(0, 10); } export function shapeWeeks(workouts: Workout[], n: number): Week[] { const map = new Map(); workouts.forEach((w) => { const k = weekStart(w.start_time); if (!map.has(k)) map.set(k, { start: k, vol: 0, count: 0, sets: 0 }); const e = map.get(k)!; const { vol, sets } = workoutVolume(w); e.vol += vol; e.count++; e.sets += sets; }); return [...map.values()].sort((a, b) => (a.start < b.start ? -1 : 1)).slice(-n); } export function shapeBiomarkers(bm: BiomarkersResponse): MarkerGroup[] { const groups = new Map(); bm.biomarkers.forEach((b) => { if (!groups.has(b.name)) groups.set(b.name, []); groups.get(b.name)!.push(b); }); const out: MarkerGroup[] = []; groups.forEach((hist, name) => { hist.sort((a, b) => (a.collected < b.collected ? -1 : 1)); const latest = hist[hist.length - 1]; const series = hist .filter((h) => h.unit === latest.unit && h.value_num != null) .map((h) => ({ d: h.collected, v: h.value_num! })); out.push({ name, latest, series, panel: latest.panel }); }); return out.sort((a, b) => a.name.localeCompare(b.name)); }