309 lines
12 KiB
PHP
309 lines
12 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Course;
|
||
|
||
use Illuminate\Http\Request;
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\Course\WatchHistory;
|
||
use App\Services\Course\CoursePlayerService;
|
||
use App\Services\Course\CourseReviewService;
|
||
use App\Services\Course\CourseService;
|
||
use App\Services\Course\CourseSectionService;
|
||
use App\Services\LiveClass\ZoomLiveService;
|
||
use App\Models\Course\Course;
|
||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||
use Illuminate\Support\Facades\Auth;
|
||
use Inertia\Inertia;
|
||
|
||
class PlayerController extends Controller
|
||
{
|
||
private function findCourseContentByIdAndType(Course $course, string $contentId, string $contentType): ?array
|
||
{
|
||
if (!in_array($contentType, ['lesson', 'quiz'], true)) {
|
||
return null;
|
||
}
|
||
|
||
foreach ($course->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;
|
||
}
|
||
}
|