refactor(db): separate workout plans from completed sessions
This commit is contained in:
+147
-76
@@ -1,8 +1,9 @@
|
||||
import { db } from "./client";
|
||||
import { generateUUID } from "../utils/uuid";
|
||||
import { workouts, sets, laps, templates } from "./schema";
|
||||
import { workouts, workoutSessions, sets, laps, templates } from "./schema";
|
||||
import { eq, desc, sql } from "drizzle-orm";
|
||||
import type { WorkoutSet, LapData } from "@types/workout";
|
||||
import WatchConnectivity from "../modules/watch-connectivity";
|
||||
|
||||
// ─── Templates ───────────────────────────────────────────────
|
||||
|
||||
@@ -30,7 +31,7 @@ export async function toggleTemplateFavorite(id: string, isFavorite: boolean) {
|
||||
await db.update(templates).set({ isFavorite }).where(eq(templates.id, id));
|
||||
}
|
||||
|
||||
// ─── Workouts ────────────────────────────────────────────────
|
||||
// ─── Workout Plans ───────────────────────────────────────────
|
||||
|
||||
export async function saveWorkout(
|
||||
name: string | null,
|
||||
@@ -39,16 +40,13 @@ export async function saveWorkout(
|
||||
templateId?: string
|
||||
) {
|
||||
const workoutId = generateUUID();
|
||||
const totalDistance = setsData.reduce((acc, s) => acc + s.distancePerRep * s.repCount, 0);
|
||||
|
||||
await db.insert(workouts).values({
|
||||
id: workoutId,
|
||||
name,
|
||||
templateId: templateId ?? null,
|
||||
poolLength,
|
||||
startedAt: new Date().toISOString(),
|
||||
totalDistance,
|
||||
source: "manual",
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
for (let i = 0; i < setsData.length; i++) {
|
||||
@@ -69,74 +67,50 @@ export async function saveWorkout(
|
||||
return workoutId;
|
||||
}
|
||||
|
||||
export async function finishWorkout(
|
||||
workoutId: string,
|
||||
duration: number,
|
||||
calories: number,
|
||||
avgHeartRate: number
|
||||
) {
|
||||
await db
|
||||
.update(workouts)
|
||||
.set({
|
||||
finishedAt: new Date().toISOString(),
|
||||
totalDuration: duration,
|
||||
calories,
|
||||
avgHeartRate,
|
||||
})
|
||||
.where(eq(workouts.id, workoutId));
|
||||
}
|
||||
|
||||
export async function getWorkoutWithSets(workoutId: string) {
|
||||
const workout = await db.select().from(workouts).where(eq(workouts.id, workoutId)).get();
|
||||
if (!workout) return null;
|
||||
|
||||
const workoutSets = await db.select().from(sets).where(eq(sets.workoutId, workoutId));
|
||||
const workoutLaps = await db.select().from(laps).where(eq(laps.workoutId, workoutId));
|
||||
|
||||
return { ...workout, sets: workoutSets, laps: workoutLaps };
|
||||
return { ...workout, sets: workoutSets };
|
||||
}
|
||||
|
||||
export async function getAllWorkouts() {
|
||||
return db.select().from(workouts).orderBy(desc(workouts.startedAt));
|
||||
return db.select().from(workouts).orderBy(desc(workouts.createdAt));
|
||||
}
|
||||
|
||||
export async function deleteWorkout(id: string) {
|
||||
await db.delete(laps).where(eq(laps.workoutId, id));
|
||||
await db.delete(sets).where(eq(sets.workoutId, id));
|
||||
await db.delete(workouts).where(eq(workouts.id, id));
|
||||
}
|
||||
// ─── Workout Sessions ────────────────────────────────────────
|
||||
|
||||
// ─── Laps ────────────────────────────────────────────────────
|
||||
export async function finishWorkout(
|
||||
workoutId: string,
|
||||
duration: number,
|
||||
calories: number,
|
||||
avgHeartRate: number,
|
||||
totalDistance: number
|
||||
) {
|
||||
const sessionId = generateUUID();
|
||||
const workout = await db.select().from(workouts).where(eq(workouts.id, workoutId)).get();
|
||||
|
||||
export async function saveLap(workoutId: string, lap: LapData) {
|
||||
const swolf =
|
||||
lap.strokeCount != null ? lap.strokeCount + Math.round(lap.duration) : null;
|
||||
|
||||
await db.insert(laps).values({
|
||||
id: generateUUID(),
|
||||
await db.insert(workoutSessions).values({
|
||||
id: sessionId,
|
||||
workoutId,
|
||||
setPosition: lap.setPosition,
|
||||
repNumber: lap.repNumber,
|
||||
distance: lap.distance,
|
||||
duration: lap.duration,
|
||||
strokeCount: lap.strokeCount ?? null,
|
||||
heartRate: lap.heartRate ?? null,
|
||||
swolf,
|
||||
name: workout?.name ?? null,
|
||||
poolLength: workout?.poolLength ?? 25,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
totalDistance,
|
||||
totalDuration: duration,
|
||||
calories,
|
||||
avgHeartRate,
|
||||
source: "phone",
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveLaps(workoutId: string, lapsData: LapData[]) {
|
||||
for (const lap of lapsData) {
|
||||
await saveLap(workoutId, lap);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorkoutLaps(workoutId: string) {
|
||||
return db.select().from(laps).where(eq(laps.workoutId, workoutId));
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
export async function saveWatchCompletedWorkout(data: {
|
||||
workoutId: string;
|
||||
sessionId?: string;
|
||||
duration: number;
|
||||
totalDistance: number;
|
||||
calories: number;
|
||||
@@ -154,36 +128,48 @@ export async function saveWatchCompletedWorkout(data: {
|
||||
? Math.round(data.laps.reduce((sum, l) => sum + (l.heartRate || 0), 0) / data.laps.length)
|
||||
: 0;
|
||||
|
||||
const existing = await db
|
||||
.select({ id: workouts.id })
|
||||
// Haal plan op als het bestaat
|
||||
const plan = await db
|
||||
.select()
|
||||
.from(workouts)
|
||||
.where(eq(workouts.id, data.workoutId))
|
||||
.get();
|
||||
|
||||
// Gebruik sessionId van de watch, of genereer een nieuwe
|
||||
const sessionId = data.sessionId ?? generateUUID();
|
||||
|
||||
// Check of er al een sessie bestaat voor dit sessionId
|
||||
const existingSession = await db
|
||||
.select({ id: workoutSessions.id })
|
||||
.from(workoutSessions)
|
||||
.where(eq(workoutSessions.id, sessionId))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
await db.insert(workouts).values({
|
||||
id: data.workoutId,
|
||||
name: "Watch Workout",
|
||||
poolLength: 25,
|
||||
if (existingSession.length === 0) {
|
||||
await db.insert(workoutSessions).values({
|
||||
id: sessionId,
|
||||
workoutId: data.workoutId,
|
||||
name: plan?.name ?? "Watch Workout",
|
||||
poolLength: plan?.poolLength ?? 25,
|
||||
startedAt: new Date().toISOString(),
|
||||
finishedAt: new Date().toISOString(),
|
||||
totalDuration: data.duration,
|
||||
totalDistance: data.totalDistance || 0,
|
||||
totalDuration: data.duration,
|
||||
calories: data.calories || 0,
|
||||
avgHeartRate,
|
||||
source: "watch",
|
||||
});
|
||||
} else {
|
||||
await db
|
||||
.update(workouts)
|
||||
.update(workoutSessions)
|
||||
.set({
|
||||
finishedAt: new Date().toISOString(),
|
||||
totalDuration: data.duration,
|
||||
totalDistance: data.totalDistance || 0,
|
||||
calories: data.calories || 0,
|
||||
avgHeartRate,
|
||||
source: "watch",
|
||||
})
|
||||
.where(eq(workouts.id, data.workoutId));
|
||||
.where(eq(workoutSessions.id, sessionId));
|
||||
}
|
||||
|
||||
for (const lap of data.laps) {
|
||||
@@ -192,7 +178,7 @@ export async function saveWatchCompletedWorkout(data: {
|
||||
|
||||
await db.insert(laps).values({
|
||||
id: generateUUID(),
|
||||
workoutId: data.workoutId,
|
||||
sessionId,
|
||||
setPosition: lap.setPosition,
|
||||
repNumber: lap.repNumber,
|
||||
distance: lap.distance,
|
||||
@@ -204,6 +190,91 @@ export async function saveWatchCompletedWorkout(data: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllSessions() {
|
||||
return db
|
||||
.select()
|
||||
.from(workoutSessions)
|
||||
.orderBy(desc(workoutSessions.startedAt));
|
||||
}
|
||||
|
||||
export async function getSessionWithDetails(sessionId: string) {
|
||||
const session = await db
|
||||
.select()
|
||||
.from(workoutSessions)
|
||||
.where(eq(workoutSessions.id, sessionId))
|
||||
.get();
|
||||
if (!session) return null;
|
||||
|
||||
const sessionLaps = await db.select().from(laps).where(eq(laps.sessionId, sessionId));
|
||||
|
||||
let sessionSets: any[] = [];
|
||||
if (session.workoutId) {
|
||||
sessionSets = await db.select().from(sets).where(eq(sets.workoutId, session.workoutId));
|
||||
}
|
||||
|
||||
return { ...session, laps: sessionLaps, sets: sessionSets };
|
||||
}
|
||||
|
||||
// ─── Laps ────────────────────────────────────────────────────
|
||||
|
||||
export async function saveLap(sessionId: string, lap: LapData) {
|
||||
const swolf =
|
||||
lap.strokeCount != null ? lap.strokeCount + Math.round(lap.duration) : null;
|
||||
|
||||
await db.insert(laps).values({
|
||||
id: generateUUID(),
|
||||
sessionId,
|
||||
setPosition: lap.setPosition,
|
||||
repNumber: lap.repNumber,
|
||||
distance: lap.distance,
|
||||
duration: lap.duration,
|
||||
strokeCount: lap.strokeCount ?? null,
|
||||
heartRate: lap.heartRate ?? null,
|
||||
swolf,
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveLaps(sessionId: string, lapsData: LapData[]) {
|
||||
for (const lap of lapsData) {
|
||||
await saveLap(sessionId, lap);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSessionLaps(sessionId: string) {
|
||||
return db.select().from(laps).where(eq(laps.sessionId, sessionId));
|
||||
}
|
||||
|
||||
// ─── Delete ──────────────────────────────────────────────────
|
||||
|
||||
export async function deleteSession(sessionId: string) {
|
||||
await db.delete(laps).where(eq(laps.sessionId, sessionId));
|
||||
await db.delete(workoutSessions).where(eq(workoutSessions.id, sessionId));
|
||||
}
|
||||
|
||||
export async function deleteSessionAndSync(sessionId: string) {
|
||||
await deleteSession(sessionId);
|
||||
await WatchConnectivity.sendWorkoutDeleted(sessionId);
|
||||
}
|
||||
|
||||
export async function deleteWorkout(id: string) {
|
||||
// Verwijder alle sessies die aan dit plan gelinkt zijn
|
||||
const sessions = await db
|
||||
.select({ id: workoutSessions.id })
|
||||
.from(workoutSessions)
|
||||
.where(eq(workoutSessions.workoutId, id));
|
||||
for (const session of sessions) {
|
||||
await db.delete(laps).where(eq(laps.sessionId, session.id));
|
||||
}
|
||||
await db.delete(workoutSessions).where(eq(workoutSessions.workoutId, id));
|
||||
await db.delete(sets).where(eq(sets.workoutId, id));
|
||||
await db.delete(workouts).where(eq(workouts.id, id));
|
||||
}
|
||||
|
||||
export async function deleteWorkoutAndSync(id: string) {
|
||||
await deleteWorkout(id);
|
||||
await WatchConnectivity.sendWorkoutDeleted(id);
|
||||
}
|
||||
|
||||
// ─── Stats ───────────────────────────────────────────────────
|
||||
|
||||
export async function getWeeklyStats() {
|
||||
@@ -213,12 +284,12 @@ export async function getWeeklyStats() {
|
||||
return db
|
||||
.select({
|
||||
count: sql<number>`count(*)`,
|
||||
totalDistance: sql<number>`coalesce(sum(${workouts.totalDistance}), 0)`,
|
||||
totalDuration: sql<number>`coalesce(sum(${workouts.totalDuration}), 0)`,
|
||||
avgCalories: sql<number>`coalesce(avg(${workouts.calories}), 0)`,
|
||||
totalDistance: sql<number>`coalesce(sum(${workoutSessions.totalDistance}), 0)`,
|
||||
totalDuration: sql<number>`coalesce(sum(${workoutSessions.totalDuration}), 0)`,
|
||||
avgCalories: sql<number>`coalesce(avg(${workoutSessions.calories}), 0)`,
|
||||
})
|
||||
.from(workouts)
|
||||
.where(sql`${workouts.startedAt} >= ${weekAgo.toISOString()}`)
|
||||
.from(workoutSessions)
|
||||
.where(sql`${workoutSessions.startedAt} >= ${weekAgo.toISOString()}`)
|
||||
.get();
|
||||
}
|
||||
|
||||
@@ -238,10 +309,10 @@ export async function getTotalStats() {
|
||||
return db
|
||||
.select({
|
||||
totalWorkouts: sql<number>`count(*)`,
|
||||
totalDistance: sql<number>`coalesce(sum(${workouts.totalDistance}), 0)`,
|
||||
totalDuration: sql<number>`coalesce(sum(${workouts.totalDuration}), 0)`,
|
||||
totalDistance: sql<number>`coalesce(sum(${workoutSessions.totalDistance}), 0)`,
|
||||
totalDuration: sql<number>`coalesce(sum(${workoutSessions.totalDuration}), 0)`,
|
||||
})
|
||||
.from(workouts)
|
||||
.where(sql`${workouts.finishedAt} IS NOT NULL`)
|
||||
.from(workoutSessions)
|
||||
.where(sql`${workoutSessions.finishedAt} IS NOT NULL`)
|
||||
.get();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user