319 lines
9.5 KiB
TypeScript
319 lines
9.5 KiB
TypeScript
import { db } from "./client";
|
|
import { generateUUID } from "../utils/uuid";
|
|
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 ───────────────────────────────────────────────
|
|
|
|
export async function saveTemplate(name: string, poolLength: number, setsData: WorkoutSet[]) {
|
|
const id = generateUUID();
|
|
await db.insert(templates).values({
|
|
id,
|
|
name,
|
|
poolLength,
|
|
jsonData: JSON.stringify(setsData),
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
return id;
|
|
}
|
|
|
|
export async function getTemplates() {
|
|
return db.select().from(templates).orderBy(desc(templates.createdAt));
|
|
}
|
|
|
|
export async function deleteTemplate(id: string) {
|
|
await db.delete(templates).where(eq(templates.id, id));
|
|
}
|
|
|
|
export async function toggleTemplateFavorite(id: string, isFavorite: boolean) {
|
|
await db.update(templates).set({ isFavorite }).where(eq(templates.id, id));
|
|
}
|
|
|
|
// ─── Workout Plans ───────────────────────────────────────────
|
|
|
|
export async function saveWorkout(
|
|
name: string | null,
|
|
poolLength: number,
|
|
setsData: WorkoutSet[],
|
|
templateId?: string
|
|
) {
|
|
const workoutId = generateUUID();
|
|
|
|
await db.insert(workouts).values({
|
|
id: workoutId,
|
|
name,
|
|
templateId: templateId ?? null,
|
|
poolLength,
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
|
|
for (let i = 0; i < setsData.length; i++) {
|
|
const s = setsData[i];
|
|
await db.insert(sets).values({
|
|
id: generateUUID(),
|
|
workoutId,
|
|
position: i + 1,
|
|
stroke: s.stroke,
|
|
distancePerRep: s.distancePerRep,
|
|
repCount: s.repCount,
|
|
restType: s.restType,
|
|
restDuration: s.restDuration,
|
|
targetTime: s.targetTime ?? null,
|
|
});
|
|
}
|
|
|
|
return 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));
|
|
return { ...workout, sets: workoutSets };
|
|
}
|
|
|
|
export async function getAllWorkouts() {
|
|
return db.select().from(workouts).orderBy(desc(workouts.createdAt));
|
|
}
|
|
|
|
// ─── Workout Sessions ────────────────────────────────────────
|
|
|
|
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();
|
|
|
|
await db.insert(workoutSessions).values({
|
|
id: sessionId,
|
|
workoutId,
|
|
name: workout?.name ?? null,
|
|
poolLength: workout?.poolLength ?? 25,
|
|
startedAt: new Date().toISOString(),
|
|
finishedAt: new Date().toISOString(),
|
|
totalDistance,
|
|
totalDuration: duration,
|
|
calories,
|
|
avgHeartRate,
|
|
source: "phone",
|
|
});
|
|
|
|
return sessionId;
|
|
}
|
|
|
|
export async function saveWatchCompletedWorkout(data: {
|
|
workoutId: string;
|
|
sessionId?: string;
|
|
duration: number;
|
|
totalDistance: number;
|
|
calories: number;
|
|
laps: Array<{
|
|
setPosition: number;
|
|
repNumber: number;
|
|
distance: number;
|
|
duration: number;
|
|
heartRate: number;
|
|
strokeCount?: number;
|
|
}>;
|
|
}) {
|
|
const avgHeartRate =
|
|
data.laps.length > 0
|
|
? Math.round(data.laps.reduce((sum, l) => sum + (l.heartRate || 0), 0) / data.laps.length)
|
|
: 0;
|
|
|
|
// 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 (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(),
|
|
totalDistance: data.totalDistance || 0,
|
|
totalDuration: data.duration,
|
|
calories: data.calories || 0,
|
|
avgHeartRate,
|
|
source: "watch",
|
|
});
|
|
} else {
|
|
await db
|
|
.update(workoutSessions)
|
|
.set({
|
|
finishedAt: new Date().toISOString(),
|
|
totalDuration: data.duration,
|
|
totalDistance: data.totalDistance || 0,
|
|
calories: data.calories || 0,
|
|
avgHeartRate,
|
|
})
|
|
.where(eq(workoutSessions.id, sessionId));
|
|
}
|
|
|
|
for (const lap of data.laps) {
|
|
const strokeCount = lap.strokeCount ?? null;
|
|
const swolf = strokeCount != null ? 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,
|
|
heartRate: lap.heartRate || null,
|
|
strokeCount,
|
|
swolf,
|
|
});
|
|
}
|
|
}
|
|
|
|
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() {
|
|
const now = new Date();
|
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
|
return db
|
|
.select({
|
|
count: sql<number>`count(*)`,
|
|
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(workoutSessions)
|
|
.where(sql`${workoutSessions.startedAt} >= ${weekAgo.toISOString()}`)
|
|
.get();
|
|
}
|
|
|
|
export async function getPersonalBests() {
|
|
return db
|
|
.select({
|
|
distance: laps.distance,
|
|
bestTime: sql<number>`min(${laps.duration})`,
|
|
bestSwolf: sql<number>`min(${laps.swolf})`,
|
|
})
|
|
.from(laps)
|
|
.groupBy(laps.distance)
|
|
.having(sql`${laps.distance} > 0`);
|
|
}
|
|
|
|
export async function getTotalStats() {
|
|
return db
|
|
.select({
|
|
totalWorkouts: sql<number>`count(*)`,
|
|
totalDistance: sql<number>`coalesce(sum(${workoutSessions.totalDistance}), 0)`,
|
|
totalDuration: sql<number>`coalesce(sum(${workoutSessions.totalDuration}), 0)`,
|
|
})
|
|
.from(workoutSessions)
|
|
.where(sql`${workoutSessions.finishedAt} IS NOT NULL`)
|
|
.get();
|
|
}
|