sections as $section) { if ($contentType === 'lesson') { foreach ($section->section_lessons as $lesson) { if ((string) $lesson->id === (string) $contentId) { return ['type' => 'lesson', 'id' => $lesson->id]; } } } else { foreach ($section->section_quizzes as $quiz) { if ((string) $quiz->id === (string) $contentId) { return ['type' => 'quiz', 'id' => $quiz->id]; } } } } return null; } private function findCourseContentById(Course $course, string $contentId): ?array { foreach ($course->sections as $section) { foreach ($section->section_lessons as $lesson) { if ((string) $lesson->id === (string) $contentId) { return ['type' => 'lesson', 'id' => $lesson->id]; } } foreach ($section->section_quizzes as $quiz) { if ((string) $quiz->id === (string) $contentId) { return ['type' => 'quiz', 'id' => $quiz->id]; } } } return null; } private function findFirstCourseContent(Course $course): ?array { foreach ($course->sections as $section) { $lesson = $section->section_lessons->first(); if ($lesson) { return ['type' => 'lesson', 'id' => $lesson->id]; } $quiz = $section->section_quizzes->first(); if ($quiz) { return ['type' => 'quiz', 'id' => $quiz->id]; } } return null; } public function __construct( protected CourseService $courseService, protected CoursePlayerService $coursePlay, protected CourseSectionService $sectionService, protected CourseReviewService $reviewService, protected ZoomLiveService $zoomLiveService, // protected AssignmentSubmissionService $submissionService, ) {} public function index(Request $request) { $courses = $this->courseService->getCourses($request->all(), null, true); return Inertia::render('courses/index', compact('courses')); } public function intWatchHistory(Request $request) { $user = Auth::user(); $watchHistory = $this->sectionService->initWatchHistory($request->course_id, 'lesson', $user->id); if (! $watchHistory) { return back()->with('error', 'Dieser Kurs hat noch keine Lektionen oder Quizze.'); } return redirect()->route('course.player', [ 'type' => $watchHistory->current_watching_type, 'watch_history' => $watchHistory->id, 'lesson_id' => $watchHistory->current_watching_id, ]); } public function course_player(Request $request, string $type, WatchHistory $watch_history, string $lesson_id) { try { $user = Auth::user(); $watching_id = $lesson_id ?: $watch_history->current_watching_id; $watching_type = in_array($type, ['lesson', 'quiz'], true) ? $type : ($watch_history->current_watching_type ?? 'lesson'); $course = $this->courseService->getUserCourseById($watch_history->course_id, $user); if (! $course) { return redirect() ->route('category.courses', ['category' => 'all']) ->with('error', 'Der Kurs konnte nicht gefunden werden.'); } // Fix wrong `type`/`lesson_id` combinations by resolving the content inside the course. $resolved = $this->findCourseContentByIdAndType($course, (string) $watching_id, $watching_type); if (! $resolved) { $resolved = $this->findCourseContentById($course, (string) $watching_id); } if (! $resolved) { $resolved = $this->findCourseContentByIdAndType($course, (string) $watch_history->current_watching_id, (string) $watch_history->current_watching_type); } if (! $resolved) { $resolved = $this->findCourseContentById($course, (string) $watch_history->current_watching_id); } if (! $resolved) { $resolved = $this->findFirstCourseContent($course); } if (! $resolved) { return redirect() ->route('course.details', ['slug' => $course->slug, 'id' => $course->id]) ->with('error', 'Dieser Kurs hat noch keine Lektionen oder Quizze.'); } $watching_id = (string) $resolved['id']; $watching_type = $resolved['type']; // Canonicalize URL if it doesn’t match the resolved content. if ($type !== $watching_type || (string) $lesson_id !== (string) $watching_id) { return redirect()->route('course.player', [ 'type' => $watching_type, 'watch_history' => $watch_history->id, 'lesson_id' => $watching_id, ]); } $watching = $this->coursePlay->getWatchingLesson($watching_id, $watching_type, (string) $course->id); $reviews = $this->reviewService->getReviews(['course_id' => $course->id, ...$request->all()], true); $userReview = $this->reviewService->userReview($course->id, $user->id); $totalReviews = $this->reviewService->totalReviews($course->id); $zoomConfig = $this->zoomLiveService->zoomConfig; $totalContent = 0; foreach ($course->sections as $courseSection) { $totalContent += count($courseSection->section_lessons) + count($courseSection->section_quizzes); } $watchHistory = $this->coursePlay->watchHistory($course, $watching_id, $watching_type, $user->id); $section = $course->sections->firstWhere('id', $watchHistory->current_section_id); // $submissions = null; // if ($assignment) { // $submissions = $this->submissionService->getSubmissions($assignment, $request->all()); // } return Inertia::render('course-player/index', [ 'type' => $watching_type, 'course' => $course, 'section' => $section, 'reviews' => $reviews, 'watching' => $watching, 'totalContent' => $totalContent, 'watchHistory' => $watchHistory, 'userReview' => $userReview, 'totalReviews' => $totalReviews, 'zoomConfig' => $zoomConfig, ]); } catch (ModelNotFoundException $th) { $user = Auth::user(); $course = $this->courseService->getUserCourseById($watch_history->course_id, $user); if ($course) { $watchHistoryForUser = WatchHistory::where('course_id', $course->id) ->where('user_id', $user->id) ->first(); $watchHistoryId = $watchHistoryForUser?->id ?? $watch_history->id; $requestedId = $lesson_id; $requestedType = $watching_type ?? $type; // If the ID exists in the course but the type is wrong, redirect with the correct type. $resolved = $this->findCourseContentById($course, $requestedId); if ($resolved && $resolved['type'] !== $requestedType) { return redirect()->route('course.player', [ 'type' => $resolved['type'], 'watch_history' => $watchHistoryId, 'lesson_id' => $resolved['id'], ]); } // If the watch history points to a valid item, jump there. $resolved = $this->findCourseContentById($course, (string) $watch_history->current_watching_id); if ($resolved && ((string) $resolved['id'] !== (string) $requestedId || $resolved['type'] !== $requestedType)) { return redirect()->route('course.player', [ 'type' => $resolved['type'], 'watch_history' => $watchHistoryId, 'lesson_id' => $resolved['id'], ]); } // Last resort: send the user to the first available content in the course. $first = $this->findFirstCourseContent($course); if ($first && ((string) $first['id'] !== (string) $requestedId || $first['type'] !== $requestedType)) { return redirect()->route('course.player', [ 'type' => $first['type'], 'watch_history' => $watchHistoryId, 'lesson_id' => $first['id'], ]); } return redirect() ->route('course.details', ['slug' => $course->slug, 'id' => $course->id]) ->with('error', 'Dieser Kurs hat noch keine Lektionen oder Quizze.'); } return redirect()->route('category.courses', ['category' => 'all'])->with('error', 'Der angeforderte Inhalt konnte nicht gefunden werden.'); } catch (\Throwable $th) { return redirect()->route('category.courses', ['category' => 'all'])->with('error', $th->getMessage()); } } public function finish_course(WatchHistory $watch_history) { $completedItems = json_decode($watch_history->completed_watching, true) ?: []; $lastItem = [ 'id' => $watch_history->current_watching_id, 'type' => $watch_history->current_watching_type, ]; // Check if lastItem already exists in completedItems $itemExists = false; foreach ($completedItems as $item) { if ((string)$item['id'] === (string)$lastItem['id'] && $item['type'] === $lastItem['type']) { $itemExists = true; break; } } // Add lastItem to completedItems if it doesn't exist if (!$itemExists) { $completedItems[] = $lastItem; } // Clean up duplicates and ensure consistent data types $completedItems = $this->cleanupCompletedItems($completedItems); $watch_history->completed_watching = json_encode($completedItems); $watch_history->completion_date = now(); $watch_history->save(); return back()->with('success', 'Kurs erfolgreich abgeschlossen.'); } /** * Clean up completed items to remove duplicates and ensure consistent data types */ private function cleanupCompletedItems(array $completedItems): array { $cleaned = []; $seen = []; foreach ($completedItems as $item) { // Ensure consistent data types (string for ID) $normalizedItem = [ 'id' => (string)$item['id'], 'type' => $item['type'] ]; // Create unique key for duplicate checking $key = $normalizedItem['id'] . '|' . $normalizedItem['type']; // Only add if not already seen if (!isset($seen[$key])) { $seen[$key] = true; $cleaned[] = $normalizedItem; } } return $cleaned; } }