Browse Source

feat(workout-session): implement delete workout session functionality with confirmation prompt and cascade delete in database
feat(locales): add confirmation message for deleting workout sessions in English and French
refactor(prisma): update foreign key constraints to enable cascade delete for workout sessions and related entities
chore(logging): remove unnecessary console log from session retrieval function

Mathias 1 month ago
parent
commit
1315cc5a50

+ 1 - 0
locales/en.ts

@@ -93,6 +93,7 @@ export default {
 
   // Workout Builder
   workout_builder: {
+    confirm_delete: "Are you sure you want to delete this workout session?",
     steps: {
       equipment: {
         title: "Equipment",

+ 1 - 0
locales/fr.ts

@@ -93,6 +93,7 @@ export default {
 
   // Workout Builder
   workout_builder: {
+    confirm_delete: "Êtes-vous sûr de vouloir supprimer cette séance ?",
     steps: {
       equipment: {
         title: "Équipement",

+ 11 - 0
prisma/migrations/20250615170916_add_cascade_delete_workout_sessions/migration.sql

@@ -0,0 +1,11 @@
+-- DropForeignKey
+ALTER TABLE "workout_session_exercises" DROP CONSTRAINT "workout_session_exercises_workoutSessionId_fkey";
+
+-- DropForeignKey
+ALTER TABLE "workout_sets" DROP CONSTRAINT "workout_sets_workoutSessionExerciseId_fkey";
+
+-- AddForeignKey
+ALTER TABLE "workout_session_exercises" ADD CONSTRAINT "workout_session_exercises_workoutSessionId_fkey" FOREIGN KEY ("workoutSessionId") REFERENCES "workout_sessions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "workout_sets" ADD CONSTRAINT "workout_sets_workoutSessionExerciseId_fkey" FOREIGN KEY ("workoutSessionExerciseId") REFERENCES "workout_session_exercises"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 2 - 2
prisma/schema.prisma

@@ -284,7 +284,7 @@ model WorkoutSessionExercise {
   workoutSessionId String
   exerciseId       String
   order            Int
-  workoutSession   WorkoutSession @relation(fields: [workoutSessionId], references: [id])
+  workoutSession   WorkoutSession @relation(fields: [workoutSessionId], references: [id], onDelete: Cascade)
   exercise         Exercise       @relation(fields: [exerciseId], references: [id])
   sets             WorkoutSet[]
 
@@ -301,7 +301,7 @@ model WorkoutSet {
   valuesSec                Int[]                  @default([])
   units                    WorkoutSetUnit[]       @default([])
   completed                Boolean                @default(false)
-  workoutSessionExercise   WorkoutSessionExercise @relation(fields: [workoutSessionExerciseId], references: [id])
+  workoutSessionExercise   WorkoutSessionExercise @relation(fields: [workoutSessionExerciseId], references: [id], onDelete: Cascade)
 
   @@map("workout_sets")
 }

+ 0 - 1
src/entities/user/model/get-server-session-user.ts

@@ -10,7 +10,6 @@ export class AuthError extends Error {
 
 export const serverAuth = async () => {
   const session = await auth.api.getSession({ headers: await headers() });
-  console.log("session:", session);
 
   if (session && session.user) {
     return session.user;

+ 53 - 0
src/features/workout-session/actions/delete-workout-session.action.ts

@@ -0,0 +1,53 @@
+"use server";
+
+import { z } from "zod";
+
+import { prisma } from "@/shared/lib/prisma";
+import { actionClient } from "@/shared/api/safe-actions";
+import { serverAuth } from "@/entities/user/model/get-server-session-user";
+
+const deleteWorkoutSessionSchema = z.object({
+  id: z.string(),
+});
+
+export const deleteWorkoutSessionAction = actionClient.schema(deleteWorkoutSessionSchema).action(async ({ parsedInput }) => {
+  try {
+    const user = await serverAuth();
+    const { id } = parsedInput;
+
+    if (!user) {
+      console.error("❌ User not authenticated");
+      return { serverError: "NOT_AUTHENTICATED" };
+    }
+
+    // Vérifier que la session appartient à l'utilisateur
+    const session = await prisma.workoutSession.findUnique({
+      where: { id },
+      select: { userId: true },
+    });
+
+    if (!session) {
+      console.error("❌ Session not found:", id);
+      return { serverError: "Session not found" };
+    }
+
+    if (session.userId !== user.id) {
+      console.error("❌ Unauthorized access to session:", id);
+      return { serverError: "Unauthorized" };
+    }
+
+    // Supprimer la session (cascade supprimera automatiquement les exercices et sets)
+    await prisma.workoutSession.delete({
+      where: { id },
+    });
+
+    if (process.env.NODE_ENV === "development") {
+      console.log("✅ Workout session deleted successfully:", id);
+    }
+
+    return { success: true };
+  } catch (error) {
+    console.error("❌ Error deleting workout session:", error);
+    return { serverError: "Failed to delete workout session" };
+  }
+});

+ 15 - 10
src/features/workout-session/ui/workout-session-list.tsx

@@ -2,6 +2,7 @@ import { useRouter } from "next/navigation";
 import { Play, Repeat2, Trash2 } from "lucide-react";
 
 import { useCurrentLocale, useI18n } from "locales/client";
+import { useWorkoutSessionService } from "@/shared/lib/workout-session/use-workout-session.service";
 import { useWorkoutSessions } from "@/features/workout-session/model/use-workout-sessions";
 import { useWorkoutBuilderStore } from "@/features/workout-builder/model/workout-builder.store";
 import { Link } from "@/components/ui/link";
@@ -21,24 +22,28 @@ export function WorkoutSessionList() {
   const t = useI18n();
   const router = useRouter();
   const loadFromSession = useWorkoutBuilderStore((s) => s.loadFromSession);
+  const { remove } = useWorkoutSessionService();
 
-  // const [sessions, setSessions] = useState<WorkoutSession[]>(() =>
-  //   workoutSessionLocal.getAll().sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()),
-  // );
-
-  const { data: sessions = [] } = useWorkoutSessions();
+  const { data: sessions = [], refetch } = useWorkoutSessions();
   const activeSession = sessions.find((s) => s.status === "active");
 
-  const handleDelete = (_id: string) => {
-    // TODO: delete by service
-    // workoutSessionLocal.remove(id);
+  const handleDelete = async (id: string) => {
+    const confirmed = window.confirm(t("workout_builder.confirm_delete"));
+
+    if (!confirmed) return;
+
+    try {
+      await remove(id);
+      refetch();
+    } catch (error) {
+      console.error("Error deleting session:", error);
+      alert("Error deleting session");
+    }
   };
 
   const handleRepeat = (id: string) => {
     const sessionToCopy = sessions.find((s) => s.id === id);
     if (!sessionToCopy) return;
-    // prepare data for the builder
-    console.log("sessionToCopy.exercises:", sessionToCopy.exercises);
 
     const allEquipment = Array.from(
       new Set(

+ 7 - 6
src/shared/lib/workout-session/use-workout-session.service.ts

@@ -1,6 +1,7 @@
 import { nullToUndefined } from "@/shared/lib/format";
 import { syncWorkoutSessionAction } from "@/features/workout-session/actions/sync-workout-sessions.action";
 import { getWorkoutSessionsAction } from "@/features/workout-session/actions/get-workout-sessions.action";
+import { deleteWorkoutSessionAction } from "@/features/workout-session/actions/delete-workout-session.action";
 import { useSession } from "@/features/auth/lib/auth-client";
 
 import { workoutSessionLocal } from "./workout-session.local";
@@ -94,12 +95,12 @@ export const useWorkoutSessionService = () => {
   };
 
   const remove = async (id: string) => {
-    // if (isUserLoggedIn()) {
-    //   // TODO: Créer une action deleteWorkoutSessionAction
-    //   const result = await deleteWorkoutSessionAction({ id });
-    //   if (result.serverError) throw new Error(result.serverError);
-    // }
-    // workoutSessionLocal.remove(id);
+    if (userId) {
+      const result = await deleteWorkoutSessionAction({ id });
+      if (result?.serverError) throw new Error(result.serverError);
+    }
+
+    workoutSessionLocal.remove(id);
   };
 
   return { getAll, add, update, complete, remove };