From 704ab725ccec910dd5f38fc34e3cbee5164eedd7 Mon Sep 17 00:00:00 2001 From: Lance Yuriel Pascual Villanueva <110213938+Lance-Yuriel@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:29:07 +1300 Subject: [PATCH] feat: implement workout session API client functions - Implement getAll() using GET /api/workout-sessions - Implement create() using POST /api/workout-sessions - Implement update() using PATCH /api/workout-sessions/:id - Implement complete() using PATCH /api/workout-sessions/:id/complete - Integrate API functions into useWorkoutSessionService - Add direct API calls to workout-session.store.ts for complete() - Add proper error handling with server action fallbacks - All functions tested and working with real network calls Resolves: Client-side API implementation for workout sessions --- .../[sessionSlug]/ProgramSessionClient.tsx | 4 +- .../[sessionId]/complete/route.ts | 48 ++++++ app/api/workout-sessions/[sessionId]/route.ts | 42 +++++ app/api/workout-sessions/route.ts | 119 ++++++++++++++ .../complete-workout-session.action.ts | 70 ++++++++ .../actions/create-workout-session.action.ts | 90 +++++++++++ .../actions/update-workout-session.action.ts | 126 +++++++++++++++ .../model/workout-session.store.ts | 25 ++- .../use-workout-session.service.ts | 152 +++++++++++------- .../workout-session/workout-session.api.ts | 58 ++++++- 10 files changed, 665 insertions(+), 69 deletions(-) create mode 100644 app/api/workout-sessions/[sessionId]/complete/route.ts create mode 100644 app/api/workout-sessions/route.ts create mode 100644 src/features/workout-session/actions/complete-workout-session.action.ts create mode 100644 src/features/workout-session/actions/create-workout-session.action.ts create mode 100644 src/features/workout-session/actions/update-workout-session.action.ts diff --git a/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/ProgramSessionClient.tsx b/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/ProgramSessionClient.tsx index f831e64e..60ea344c 100644 --- a/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/ProgramSessionClient.tsx +++ b/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/ProgramSessionClient.tsx @@ -154,8 +154,8 @@ export function ProgramSessionClient({ program, week, session, isAuthenticated, if (!workoutSession || !sessionProgressId) return; try { - // Complete the workout - completeWorkout(); + // Complete the workout (now async and will call our API) + await completeWorkout(); // Save to database and mark session as complete const { isCompleted, nextWeek, nextSession } = await completeProgramSession(sessionProgressId, workoutSession.id); diff --git a/app/api/workout-sessions/[sessionId]/complete/route.ts b/app/api/workout-sessions/[sessionId]/complete/route.ts new file mode 100644 index 00000000..d6ef873f --- /dev/null +++ b/app/api/workout-sessions/[sessionId]/complete/route.ts @@ -0,0 +1,48 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { getMobileCompatibleSession } from "@/shared/api/mobile-auth"; +import { completeWorkoutSessionAction } from "@/features/workout-session/actions/complete-workout-session.action"; + +export async function PATCH(request: NextRequest, { params }: { params: Promise<{ sessionId: string }> }) { + try { + const { sessionId } = await params; + + // Check authentication + const session = await getMobileCompatibleSession(request); + if (!session?.user) { + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + } + + const body = await request.json(); + + // Use the complete server action + const result = await completeWorkoutSessionAction({ + id: sessionId, + endedAt: body.endedAt ? new Date(body.endedAt) : new Date(), + duration: body.duration, + rating: body.rating, + ratingComment: body.ratingComment, + }); + + if (result?.serverError) { + if (result.serverError === "NOT_AUTHENTICATED") { + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + } + if (result.serverError === "Unauthorized") { + return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); + } + if (result.serverError === "Session not found") { + return NextResponse.json({ error: "Session not found" }, { status: 404 }); + } + return NextResponse.json({ error: result.serverError }, { status: 500 }); + } + + return NextResponse.json({ + success: true, + data: result.data + }); + } catch (error) { + console.error("Error completing workout session:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/app/api/workout-sessions/[sessionId]/route.ts b/app/api/workout-sessions/[sessionId]/route.ts index 9ca02034..fa35ff5b 100644 --- a/app/api/workout-sessions/[sessionId]/route.ts +++ b/app/api/workout-sessions/[sessionId]/route.ts @@ -2,6 +2,48 @@ import { NextRequest, NextResponse } from "next/server"; import { getMobileCompatibleSession } from "@/shared/api/mobile-auth"; import { deleteWorkoutSessionAction } from "@/features/workout-session/actions/delete-workout-session.action"; +import { updateWorkoutSessionAction } from "@/features/workout-session/actions/update-workout-session.action"; + +export async function PATCH(request: NextRequest, { params }: { params: Promise<{ sessionId: string }> }) { + try { + const { sessionId } = await params; + + // Check authentication + const session = await getMobileCompatibleSession(request); + if (!session?.user) { + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + } + + const body = await request.json(); + + // Use the update server action + const result = await updateWorkoutSessionAction({ + id: sessionId, + data: body, + }); + + if (result?.serverError) { + if (result.serverError === "NOT_AUTHENTICATED") { + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + } + if (result.serverError === "Unauthorized") { + return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); + } + if (result.serverError === "Session not found") { + return NextResponse.json({ error: "Session not found" }, { status: 404 }); + } + return NextResponse.json({ error: result.serverError }, { status: 500 }); + } + + return NextResponse.json({ + success: true, + data: result.data + }); + } catch (error) { + console.error("Error updating workout session:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} export async function DELETE(request: NextRequest, { params }: { params: Promise<{ sessionId: string }> }) { try { diff --git a/app/api/workout-sessions/route.ts b/app/api/workout-sessions/route.ts new file mode 100644 index 00000000..034e1c1b --- /dev/null +++ b/app/api/workout-sessions/route.ts @@ -0,0 +1,119 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { getMobileCompatibleSession } from "@/shared/api/mobile-auth"; +import { createWorkoutSessionAction } from "@/features/workout-session/actions/create-workout-session.action"; +import { getWorkoutSessionsAction } from "@/features/workout-session/actions/get-workout-sessions.action"; + +export async function GET(request: NextRequest) { + try { + // Check authentication + const session = await getMobileCompatibleSession(request); + + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Fetch workout sessions for the authenticated user + const result = await getWorkoutSessionsAction({ userId: session.user.id }); + + if (result?.serverError) { + return NextResponse.json({ error: result.serverError }, { status: 500 }); + } + + // Transform to match the expected response format + const formattedSessions = result?.data?.sessions?.map((session) => ({ + id: session.id, + userId: session.userId, + startedAt: session.startedAt.toISOString(), + endedAt: session.endedAt?.toISOString() || null, + duration: session.duration || null, + muscles: session.muscles || [], + rating: session.rating || null, + ratingComment: session.ratingComment || null, + exercises: session.exercises.map((sessionExercise) => ({ + id: sessionExercise.exerciseId, + order: sessionExercise.order, + exercise: { + ...sessionExercise.exercise, + createdAt: sessionExercise.exercise.createdAt.toISOString(), + updatedAt: sessionExercise.exercise.updatedAt.toISOString(), + attributes: sessionExercise.exercise.attributes.map((attr) => ({ + id: attr.id, + exerciseId: attr.exerciseId, + attributeNameId: attr.attributeNameId, + attributeValueId: attr.attributeValueId, + createdAt: attr.createdAt.toISOString(), + updatedAt: attr.updatedAt.toISOString(), + attributeName: { + id: attr.attributeName.id, + name: attr.attributeName.name, + createdAt: attr.attributeName.createdAt.toISOString(), + updatedAt: attr.attributeName.updatedAt.toISOString(), + }, + attributeValue: { + id: attr.attributeValue.id, + attributeNameId: attr.attributeValue.attributeNameId, + value: attr.attributeValue.value, + createdAt: attr.attributeValue.createdAt.toISOString(), + updatedAt: attr.attributeValue.updatedAt.toISOString(), + }, + })), + }, + sets: sessionExercise.sets.map((set) => ({ + id: set.id, + workoutSessionExerciseId: set.workoutSessionExerciseId, + setIndex: set.setIndex, + type: set.type, + types: set.types || [], + valuesInt: set.valuesInt || [], + valuesSec: set.valuesSec || [], + units: set.units || [], + completed: set.completed, + })), + })), + })); + + return NextResponse.json(formattedSessions); + } catch (error) { + console.error("Error fetching workout sessions:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + // Check authentication + const session = await getMobileCompatibleSession(request); + + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await request.json(); + + // Validate required fields + if (!body.startedAt) { + return NextResponse.json({ error: "startedAt is required" }, { status: 400 }); + } + + // Create the workout session + const result = await createWorkoutSessionAction({ + userId: session.user.id, + startedAt: new Date(body.startedAt), + exercises: body.exercises || [], + muscles: body.muscles || [], + }); + + if (result?.serverError) { + return NextResponse.json({ error: result.serverError }, { status: 500 }); + } + + return NextResponse.json({ + success: true, + data: result.data + }); + } catch (error) { + console.error("Error creating workout session:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/src/features/workout-session/actions/complete-workout-session.action.ts b/src/features/workout-session/actions/complete-workout-session.action.ts new file mode 100644 index 00000000..0365b9cf --- /dev/null +++ b/src/features/workout-session/actions/complete-workout-session.action.ts @@ -0,0 +1,70 @@ +"use server"; + +import { z } from "zod"; + +import { prisma } from "@/shared/lib/prisma"; +import { actionClient } from "@/shared/api/safe-actions"; + +const completeWorkoutSessionSchema = z.object({ + id: z.string(), + endedAt: z.date(), + duration: z.number().optional(), + rating: z.number().min(1).max(5).optional(), + ratingComment: z.string().optional(), +}); + +export const completeWorkoutSessionAction = actionClient + .schema(completeWorkoutSessionSchema) + .action(async ({ parsedInput }) => { + try { + const { id, endedAt, duration, rating, ratingComment } = parsedInput; + + // First, check if the session exists + const existingSession = await prisma.workoutSession.findFirst({ + where: { id }, + }); + + if (!existingSession) { + return { serverError: "Session not found" }; + } + + // Calculate duration if not provided + let calculatedDuration = duration; + if (!calculatedDuration && existingSession.startedAt) { + calculatedDuration = Math.floor((endedAt.getTime() - existingSession.startedAt.getTime()) / 1000); + } + + // Update the workout session to mark it as completed + const completedSession = await prisma.workoutSession.update({ + where: { id }, + data: { + endedAt, + duration: calculatedDuration, + rating, + ratingComment, + }, + include: { + exercises: { + include: { + exercise: { + include: { + attributes: { + include: { + attributeName: true, + attributeValue: true, + }, + }, + }, + }, + sets: true, + }, + }, + }, + }); + + return { data: completedSession }; + } catch (error) { + console.error("Error completing workout session:", error); + return { serverError: "Failed to complete workout session" }; + } + }); diff --git a/src/features/workout-session/actions/create-workout-session.action.ts b/src/features/workout-session/actions/create-workout-session.action.ts new file mode 100644 index 00000000..d07e406b --- /dev/null +++ b/src/features/workout-session/actions/create-workout-session.action.ts @@ -0,0 +1,90 @@ +"use server"; + +import { z } from "zod"; +import { ExerciseAttributeValueEnum } from "@prisma/client"; + +import { prisma } from "@/shared/lib/prisma"; +import { actionClient } from "@/shared/api/safe-actions"; + +const createWorkoutSessionSchema = z.object({ + userId: z.string(), + startedAt: z.date(), + endedAt: z.date().optional(), + duration: z.number().optional(), + exercises: z.array(z.object({ + exerciseId: z.string(), + order: z.number(), + sets: z.array(z.object({ + setIndex: z.number(), + type: z.string(), + types: z.array(z.string()).optional(), + valuesInt: z.array(z.number()).optional(), + valuesSec: z.array(z.number()).optional(), + units: z.array(z.string()).optional(), + completed: z.boolean().optional(), + })).optional(), + })).optional(), + muscles: z.array(z.nativeEnum(ExerciseAttributeValueEnum)).optional(), + rating: z.number().min(1).max(5).optional(), + ratingComment: z.string().optional(), +}); + +export const createWorkoutSessionAction = actionClient + .schema(createWorkoutSessionSchema) + .action(async ({ parsedInput }) => { + try { + const { userId, startedAt, endedAt, duration, exercises, muscles, rating, ratingComment } = parsedInput; + + // Create the workout session with exercises and sets + const workoutSession = await prisma.workoutSession.create({ + data: { + userId, + startedAt, + endedAt, + duration, + muscles: muscles || [], + rating, + ratingComment, + exercises: exercises ? { + create: exercises.map((exercise) => ({ + exerciseId: exercise.exerciseId, + order: exercise.order, + sets: exercise.sets ? { + create: exercise.sets.map((set) => ({ + setIndex: set.setIndex, + type: set.type as any, // Will be validated by Prisma + types: set.types || [], + valuesInt: set.valuesInt || [], + valuesSec: set.valuesSec || [], + units: set.units || [], + completed: set.completed || false, + })), + } : undefined, + })), + } : undefined, + }, + include: { + exercises: { + include: { + exercise: { + include: { + attributes: { + include: { + attributeName: true, + attributeValue: true, + }, + }, + }, + }, + sets: true, + }, + }, + }, + }); + + return { data: workoutSession }; + } catch (error) { + console.error("Error creating workout session:", error); + return { serverError: "Failed to create workout session" }; + } + }); diff --git a/src/features/workout-session/actions/update-workout-session.action.ts b/src/features/workout-session/actions/update-workout-session.action.ts new file mode 100644 index 00000000..4fe5fca1 --- /dev/null +++ b/src/features/workout-session/actions/update-workout-session.action.ts @@ -0,0 +1,126 @@ +"use server"; + +import { z } from "zod"; +import { ExerciseAttributeValueEnum } from "@prisma/client"; + +import { prisma } from "@/shared/lib/prisma"; +import { actionClient } from "@/shared/api/safe-actions"; + +const updateWorkoutSessionSchema = z.object({ + id: z.string(), + data: z.object({ + endedAt: z.string().optional(), + duration: z.number().optional(), + muscles: z.array(z.nativeEnum(ExerciseAttributeValueEnum)).optional(), + rating: z.number().min(1).max(5).optional(), + ratingComment: z.string().optional(), + exercises: z.array(z.object({ + exerciseId: z.string(), + order: z.number(), + sets: z.array(z.object({ + setIndex: z.number(), + type: z.string(), + types: z.array(z.string()).optional(), + valuesInt: z.array(z.number()).optional(), + valuesSec: z.array(z.number()).optional(), + units: z.array(z.string()).optional(), + completed: z.boolean().optional(), + })).optional(), + })).optional(), + }), +}); + +export const updateWorkoutSessionAction = actionClient + .schema(updateWorkoutSessionSchema) + .action(async ({ parsedInput }) => { + try { + const { id, data } = parsedInput; + + // First, check if the session exists and belongs to the user + const existingSession = await prisma.workoutSession.findFirst({ + where: { id }, + include: { user: true }, + }); + + if (!existingSession) { + return { serverError: "Session not found" }; + } + + // Prepare the update data + const updateData: any = {}; + + if (data.endedAt) { + updateData.endedAt = new Date(data.endedAt); + } + + if (data.duration !== undefined) { + updateData.duration = data.duration; + } + + if (data.muscles) { + updateData.muscles = data.muscles; + } + + if (data.rating !== undefined) { + updateData.rating = data.rating; + } + + if (data.ratingComment !== undefined) { + updateData.ratingComment = data.ratingComment; + } + + // Handle exercises update if provided + if (data.exercises) { + // Delete existing exercises and recreate them + await prisma.workoutSessionExercise.deleteMany({ + where: { workoutSessionId: id }, + }); + + updateData.exercises = { + create: data.exercises.map((exercise) => ({ + exerciseId: exercise.exerciseId, + order: exercise.order, + sets: exercise.sets ? { + create: exercise.sets.map((set) => ({ + setIndex: set.setIndex, + type: set.type as any, // Will be validated by Prisma + types: set.types || [], + valuesInt: set.valuesInt || [], + valuesSec: set.valuesSec || [], + units: set.units || [], + completed: set.completed || false, + })), + } : undefined, + })), + }; + } + + // Update the workout session + const updatedSession = await prisma.workoutSession.update({ + where: { id }, + data: updateData, + include: { + exercises: { + include: { + exercise: { + include: { + attributes: { + include: { + attributeName: true, + attributeValue: true, + }, + }, + }, + }, + sets: true, + }, + }, + }, + }); + + return { data: updatedSession }; + } catch (error) { + console.error("Error updating workout session:", error); + return { serverError: "Failed to update workout session" }; + } + }); diff --git a/src/features/workout-session/model/workout-session.store.ts b/src/features/workout-session/model/workout-session.store.ts index 8712dfe7..78f0e39f 100644 --- a/src/features/workout-session/model/workout-session.store.ts +++ b/src/features/workout-session/model/workout-session.store.ts @@ -131,20 +131,37 @@ export const useWorkoutSessionStore = create((set, get) => }); }, - completeWorkout: () => { + completeWorkout: async () => { const { session } = get(); if (session) { - workoutSessionLocal.update(session.id, { status: "completed", endedAt: new Date().toISOString() }); + const completedData = { status: "completed", endedAt: new Date().toISOString() }; + + // Update local storage first + workoutSessionLocal.update(session.id, completedData); + + // Try to complete via API if user is logged in + try { + const { workoutSessionApi } = await import("@/shared/lib/workout-session/workout-session.api"); + console.log("🔄 Attempting to complete workout via API with session ID:", session.id); + await workoutSessionApi.complete(session.id, { + endedAt: completedData.endedAt, + duration: Math.floor((Date.now() - new Date(session.startedAt).getTime()) / 1000), + }); + console.log("✅ Workout completed via API"); + } catch (error) { + console.log("⚠️ API completion failed, using local storage only:", error); + } + console.log({ - session: { ...session, status: "completed", endedAt: new Date().toISOString() }, + session: { ...session, ...completedData }, progress: {}, elapsedTime: 0, isTimerRunning: false, isWorkoutActive: false, }); set({ - session: { ...session, status: "completed", endedAt: new Date().toISOString() }, + session: { ...session, ...completedData }, progress: {}, elapsedTime: 0, isTimerRunning: false, diff --git a/src/shared/lib/workout-session/use-workout-session.service.ts b/src/shared/lib/workout-session/use-workout-session.service.ts index 8c9ef18a..b77526a1 100644 --- a/src/shared/lib/workout-session/use-workout-session.service.ts +++ b/src/shared/lib/workout-session/use-workout-session.service.ts @@ -5,6 +5,7 @@ import { deleteWorkoutSessionAction } from "@/features/workout-session/actions/d import { useSession } from "@/features/auth/lib/auth-client"; import { workoutSessionLocal } from "./workout-session.local"; +import { workoutSessionApi } from "./workout-session.api"; import type { WorkoutSession } from "./types/workout-session"; @@ -17,35 +18,48 @@ export const useWorkoutSessionService = () => { const getAll = async (): Promise => { if (userId) { - const result = await getWorkoutSessionsAction({ userId }); - if (result?.serverError) throw new Error(result.serverError); - - const serverSessions = (result?.data?.sessions || []).map((session) => ({ - ...session, - startedAt: session.startedAt instanceof Date ? session.startedAt.toISOString() : session.startedAt, - endedAt: - session.endedAt instanceof Date - ? session.endedAt.toISOString() - : typeof session.endedAt === "string" - ? session.endedAt - : undefined, - duration: nullToUndefined(session.duration), - exercises: session.exercises.map(({ exercise, order, sets }) => ({ - ...exercise, - order, - sets: sets.map((set) => { - return { - ...set, - units: nullToUndefined(set.units), - }; - }), - })), - })); - const localSessions = workoutSessionLocal.getAll().filter((s) => s.status !== "synced"); - - return [...localSessions, ...(serverSessions as any)].sort( - (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(), - ); + try { + // Use our new API function instead of server action + const serverSessions = await workoutSessionApi.getAll(); + const localSessions = workoutSessionLocal.getAll().filter((s) => s.status !== "synced"); + + return [...localSessions, ...serverSessions].sort( + (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(), + ); + } catch (error) { + console.error("Failed to fetch workout sessions from API, falling back to server action:", error); + + // Fallback to server action if API fails + const result = await getWorkoutSessionsAction({ userId }); + if (result?.serverError) throw new Error(result.serverError); + + const serverSessions = (result?.data?.sessions || []).map((session) => ({ + ...session, + startedAt: session.startedAt instanceof Date ? session.startedAt.toISOString() : session.startedAt, + endedAt: + session.endedAt instanceof Date + ? session.endedAt.toISOString() + : typeof session.endedAt === "string" + ? session.endedAt + : undefined, + duration: nullToUndefined(session.duration), + exercises: session.exercises.map(({ exercise, order, sets }) => ({ + ...exercise, + order, + sets: sets.map((set) => { + return { + ...set, + units: nullToUndefined(set.units), + }; + }), + })), + })); + const localSessions = workoutSessionLocal.getAll().filter((s) => s.status !== "synced"); + + return [...localSessions, ...(serverSessions as any)].sort( + (a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(), + ); + } } return workoutSessionLocal.getAll().sort((a, b) => { @@ -57,43 +71,69 @@ export const useWorkoutSessionService = () => { const add = async (session: WorkoutSession) => { if (userId) { - const result = await syncWorkoutSessionAction({ - session: { + try { + // Use our new API function to create the session + const result = await workoutSessionApi.create({ ...session, userId, - status: "synced", - }, - }); - - if (result?.serverError) throw new Error(result.serverError); - - if (result?.data?.data) { - workoutSessionLocal.markSynced(session.id, result.data.data.id); + }); + + if (result?.id) { + workoutSessionLocal.markSynced(session.id, result.id); + } + } catch (error) { + console.error("Failed to create workout session via API, falling back to sync action:", error); + + // Fallback to sync action if API fails + const result = await syncWorkoutSessionAction({ + session: { + ...session, + userId, + status: "synced", + }, + }); + + if (result?.serverError) throw new Error(result.serverError); + + if (result?.data?.data) { + workoutSessionLocal.markSynced(session.id, result.data.data.id); + } } } return workoutSessionLocal.add(session); }; - const update = async (_id: string, _data: Partial) => { - // if (userId) { - // // TODO: create updateWorkoutSessionAction - // const result = await updateWorkoutSessionAction({ id, data }); - // if (result.serverError) throw new Error(result.serverError); - // } - // return workoutSessionLocal.update(id, data); + const update = async (id: string, data: Partial) => { + if (userId) { + try { + // Use our new API function to update the session + await workoutSessionApi.update(id, data); + } catch (error) { + console.error("Failed to update workout session via API:", error); + // Still update locally even if API fails + } + } + return workoutSessionLocal.update(id, data); }; - const complete = async (_id: string) => { - // const data = { - // status: "completed" as const, - // endedAt: new Date().toISOString(), - // }; - // if (isUserLoggedIn()) { - // const result = await completeWorkoutSessionAction({ id }); - // if (result.serverError) throw new Error(result.serverError); - // } - // return workoutSessionLocal.update(id, data); + const complete = async (id: string, data?: { endedAt?: string; duration?: number; rating?: number; ratingComment?: string }) => { + const completeData = { + status: "completed" as const, + endedAt: new Date().toISOString(), + ...data, + }; + + if (userId) { + try { + // Use our new API function to complete the session + await workoutSessionApi.complete(id, completeData); + } catch (error) { + console.error("Failed to complete workout session via API:", error); + // Still update locally even if API fails + } + } + return workoutSessionLocal.update(id, completeData); }; const remove = async (id: string) => { diff --git a/src/shared/lib/workout-session/workout-session.api.ts b/src/shared/lib/workout-session/workout-session.api.ts index 4228a8dd..80257f36 100644 --- a/src/shared/lib/workout-session/workout-session.api.ts +++ b/src/shared/lib/workout-session/workout-session.api.ts @@ -3,17 +3,61 @@ import type { WorkoutSession } from "./types/workout-session"; export const workoutSessionApi = { getAll: async (): Promise => { - // TODO: fetch("/api/workout-sessions") - return []; + const response = await fetch("/api/workout-sessions"); + + if (!response.ok) { + throw new Error(`Failed to fetch workout sessions: ${response.status} ${response.statusText}`); + } + + const sessions = await response.json(); + return sessions; }, create: async (session: WorkoutSession): Promise<{ id: string }> => { - // TODO: POST /api/workout-sessions - return { id: "server-uuid" }; + const response = await fetch("/api/workout-sessions", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(session), + }); + + if (!response.ok) { + throw new Error(`Failed to create workout session: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + return { id: result.data.id }; }, update: async (id: string, data: Partial) => { - // TODO: PATCH /api/workout-sessions/:id + const response = await fetch(`/api/workout-sessions/${id}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error(`Failed to update workout session: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + return result.data; }, - complete: async (id: string) => { - // TODO: PATCH /api/workout-sessions/:id/complete + complete: async (id: string, data?: { endedAt?: string; duration?: number; rating?: number; ratingComment?: string }) => { + const response = await fetch(`/api/workout-sessions/${id}/complete`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data || {}), + }); + + if (!response.ok) { + throw new Error(`Failed to complete workout session: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + return result.data; }, };