feat: Implement active workout flow with status management
- Added `status`, `total_sets`, and `total_volume` fields to the Workout model. - Introduced `source_kind`, `title_snapshot`, and `image_s3_url_snapshot` fields to the WorkoutItem model. - Created endpoints for managing active workouts, including finishing and discarding workouts. - Updated workout creation to ensure only one active workout exists per user. - Implemented batch addition of workout sets and updates to workout set details. - Enhanced database schema with Alembic migrations to support new fields and constraints. - Added validation to ensure at least one field is provided for workout set updates. - Updated calorie estimation logic to reflect new workout set structure.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ApiError } from "../../api";
|
||||
import type { CatalogKind, WorkoutSetInput } from "../../types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { workoutApi } from "./api";
|
||||
|
||||
export async function invalidateWorkoutQueries(queryClient: QueryClient) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["workout", "active"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["workouts"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["calories"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["progression"] }),
|
||||
]);
|
||||
}
|
||||
|
||||
export function useActiveWorkout() {
|
||||
const { auth } = useAuth();
|
||||
return useQuery({ queryKey: ["workout", "active"], queryFn: () => workoutApi.active(auth.accessToken) });
|
||||
}
|
||||
|
||||
export function useWorkoutMutations(options: { onStartConflict?: () => void; onFinish?: () => void; onDiscard?: () => void } = {}) {
|
||||
const { auth } = useAuth();
|
||||
const token = auth.accessToken;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const refresh = () => invalidateWorkoutQueries(queryClient);
|
||||
|
||||
const startWorkout = useMutation({
|
||||
mutationFn: () => workoutApi.start(token),
|
||||
onSuccess: refresh,
|
||||
onError: async (error) => {
|
||||
if (error instanceof ApiError && error.status === 409) {
|
||||
await refresh();
|
||||
options.onStartConflict?.();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const addWorkoutItem = useMutation({
|
||||
mutationFn: ({ workoutId, sourceId, kind }: { workoutId: string; sourceId: string; kind: CatalogKind }) =>
|
||||
workoutApi.addItem(token, workoutId, {
|
||||
exercise_id: kind === "exercise" ? sourceId : null,
|
||||
equipment_id: kind === "equipment" ? sourceId : null,
|
||||
}),
|
||||
onSuccess: refresh,
|
||||
});
|
||||
|
||||
const recordWorkoutSet = useMutation({
|
||||
mutationFn: ({ itemId, payload }: { itemId: string; payload: WorkoutSetInput }) => workoutApi.addSet(token, itemId, payload),
|
||||
onSuccess: refresh,
|
||||
});
|
||||
|
||||
const recordWorkoutSetsBatch = useMutation({
|
||||
mutationFn: ({ itemId, sets }: { itemId: string; sets: WorkoutSetInput[] }) => workoutApi.addSetBatch(token, itemId, sets),
|
||||
onSuccess: refresh,
|
||||
});
|
||||
|
||||
const removeWorkoutItem = useMutation({
|
||||
mutationFn: (itemId: string) => workoutApi.removeItem(token, itemId),
|
||||
onSuccess: refresh,
|
||||
});
|
||||
|
||||
const removeWorkoutSet = useMutation({
|
||||
mutationFn: ({ itemId, setId }: { itemId: string; setId: string }) => workoutApi.removeSet(token, itemId, setId),
|
||||
onSuccess: refresh,
|
||||
});
|
||||
|
||||
const updateWorkoutSet = useMutation({
|
||||
mutationFn: ({ setId, payload }: { setId: string; payload: Partial<WorkoutSetInput> }) => workoutApi.updateSet(token, setId, payload),
|
||||
onSuccess: refresh,
|
||||
});
|
||||
|
||||
const finishWorkout = useMutation({
|
||||
mutationFn: ({ workoutId, notes }: { workoutId: string; notes?: string }) => workoutApi.finish(token, workoutId, notes),
|
||||
onSuccess: async () => {
|
||||
await refresh();
|
||||
options.onFinish?.();
|
||||
},
|
||||
});
|
||||
|
||||
const discardWorkout = useMutation({
|
||||
mutationFn: (workoutId: string) => workoutApi.discard(token, workoutId),
|
||||
onSuccess: async () => {
|
||||
await refresh();
|
||||
options.onDiscard?.();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
startWorkout,
|
||||
addWorkoutItem,
|
||||
recordWorkoutSet,
|
||||
recordWorkoutSetsBatch,
|
||||
removeWorkoutItem,
|
||||
removeWorkoutSet,
|
||||
updateWorkoutSet,
|
||||
finishWorkout,
|
||||
discardWorkout,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user