pembenaran update projek

This commit is contained in:
Baghiz Zuhdi Adzin 2026-01-30 14:48:46 +07:00
parent 301fe0a3c4
commit e0aefa4eb6
20 changed files with 1291 additions and 114 deletions

View File

@ -108,41 +108,86 @@ class PlayerController extends Controller
$data['student_id'] = auth()->user()->id; $data['student_id'] = auth()->user()->id;
$total_lesson = Lesson::where('course_id', $request->course_id)->pluck('id')->toArray(); $total_lesson = Lesson::where('course_id', $request->course_id)->pluck('id')->toArray();
$watch_history = Watch_history::where('course_id', $request->course_id)->where('student_id', auth()->user()->id)->first();
$watch_history = Watch_history::where('course_id', $request->course_id) $student_id = auth()->user()->id;
->where('student_id', auth()->user()->id)->first();
if (isset($watch_history) && $watch_history->id) { if (isset($watch_history) && $watch_history->id) {
$lessons = (array) json_decode($watch_history->completed_lesson); $completed_lessons = [];
if (! in_array($request->lesson_id, $lessons)) { if (!empty($watch_history->completed_lesson)) {
array_push($lessons, $request->lesson_id); $decoded = json_decode($watch_history->completed_lesson, true);
} if (is_array($decoded)) {
$completed_lessons = array_map('intval', $decoded);
$data['completed_lesson'] = json_encode($lessons);
$data['watching_lesson_id'] = $request->lesson_id;
$data['completed_date'] = (count($total_lesson) == count($lessons)) ? time() : null;
Watch_history::where('course_id', $request->course_id)->where('student_id', auth()->user()->id)->update($data);
} else { } else {
$lessons = [$request->lesson_id]; // Fallback jika format tidak valid
$data['completed_lesson'] = json_encode($lessons); $completed_lesson_data = trim($watch_history->completed_lesson, '[]"\' ');
$data['watching_lesson_id'] = $request->lesson_id; if (!empty($completed_lesson_data)) {
$data['completed_date'] = (count($total_lesson) == count($lessons)) ? time() : null; $completed_lessons = array_map('intval', explode(',', $completed_lesson_data));
Watch_history::insert($data); }
}
} }
if (progress_bar($request->course_id) >= 100) { $completed_lessons = array_unique($completed_lessons);
$certificate = Certificate::where('user_id', auth()->user()->id)->where('course_id', $request->course_id); sort($completed_lessons);
if ($certificate->count() == 0) { $lesson_id_int = (int)$request->lesson_id;
$certificate_data['user_id'] = auth()->user()->id; if (!in_array($lesson_id_int, $completed_lessons)) {
$certificate_data['course_id'] = $request->course_id; $completed_lessons[] = $lesson_id_int;
$certificate_data['identifier'] = $this->generateIdentifier(12); sort($completed_lessons);
$certificate_data['created_at'] = date('Y-m-d H:i:s'); }
Certificate::insert($certificate_data);
$data['completed_lesson'] = json_encode($completed_lessons);
$data['watching_lesson_id'] = $lesson_id_int;
Log::info('Updated lessons (integer array):', $completed_lessons);
Watch_history::where('course_id', $request->course_id)
->where('student_id', $student_id)
->update($data);
} else {
$lesson_id_int = (int)$request->lesson_id;
$completed_lessons = [$lesson_id_int];
$data['completed_lesson'] = json_encode($completed_lessons);
$data['watching_lesson_id'] = $lesson_id_int;
Log::info('New lessons (integer array):', $completed_lessons);
Watch_history::create($data);
}
// **LOGIKA SERTIFIKAT - dengan progress yang benar**
$completed_lessons = $completed_lessons ?? [];
$completed_in_course = array_intersect($completed_lessons, $total_lesson);
$completed_count = count($completed_in_course);
$total_count = count($total_lesson);
$progress_percentage = 0;
if ($total_count > 0) {
$progress_percentage = ($completed_count / $total_count) * 100;
}
Log::info('Progress percentage: ' . $progress_percentage);
// Jika progress 100% dan status project diterima, buat sertifikat
if ($progress_percentage >= 100) {
$certificateExists = Certificate::where('user_id', $student_id)
->where('course_id', $request->course_id)
->exists();
if (!$certificateExists) {
if (method_exists($this, 'generateIdentifier')) {
$identifier = $this->generateIdentifier(12);
} else {
$identifier = \Illuminate\Support\Str::random(12);
}
Certificate::create([
'user_id' => $student_id,
'course_id' => $request->course_id,
'identifier' => $identifier,
]);
} }
} }
return redirect()->back(); return redirect()->back();
} }

View File

@ -8,6 +8,7 @@ use App\Models\Builder_page;
use App\Models\Category; use App\Models\Category;
use App\Models\Certificate; use App\Models\Certificate;
use App\Models\Course; use App\Models\Course;
use App\Models\Lesson;
use App\Models\Message; use App\Models\Message;
use App\Models\Message_code; use App\Models\Message_code;
use App\Models\Review; use App\Models\Review;
@ -15,6 +16,7 @@ use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
use SimpleSoftwareIO\QrCode\Facades\QrCode; use SimpleSoftwareIO\QrCode\Facades\QrCode;
use Illuminate\Support\Facades\Log;
use DB; use DB;
use App\Http\Controllers\NewsletterController; use App\Http\Controllers\NewsletterController;
@ -70,13 +72,13 @@ class HomeController extends Controller
public function update_watch_history_with_duration(Request $request) public function update_watch_history_with_duration(Request $request)
{ {
$userId = auth()->user()->id; // Get the logged-in user's ID $userId = auth()->user()->id;
$courseProgress = 0; $courseProgress = 0;
$isCompleted = 0; $isCompleted = 0;
// Retrieve and sanitize input data // Retrieve and sanitize input data
$courseId = htmlspecialchars($request->input('course_id')); $courseId = (int) htmlspecialchars($request->input('course_id'));
$lessonId = htmlspecialchars($request->input('lesson_id')); $lessonId = (int) htmlspecialchars($request->input('lesson_id'));
$currentDuration = htmlspecialchars($request->input('current_duration')); $currentDuration = htmlspecialchars($request->input('current_duration'));
// Fetch current watch history record // Fetch current watch history record
@ -90,17 +92,29 @@ class HomeController extends Controller
// Fetch course details // Fetch course details
$courseDetails = DB::table('courses')->where('id', $courseId)->first(); $courseDetails = DB::table('courses')->where('id', $courseId)->first();
$dripContentSettings = json_decode($courseDetails->drip_content_settings, true);
if (!$courseDetails) {
return response()->json([
'lesson_id' => $lessonId,
'course_progress' => 0,
'is_completed' => 0,
'error' => 'Course not found'
]);
}
$dripContentSettings = json_decode($courseDetails->drip_content_settings, true) ?? [];
if ($currentHistory) { if ($currentHistory) {
$watchedDurationArr = json_decode($currentHistory->watched_counter, true); $watchedDurationArr = json_decode($currentHistory->watched_counter, true);
if (!is_array($watchedDurationArr)) $watchedDurationArr = []; if (!is_array($watchedDurationArr)) {
$watchedDurationArr = [];
if (!in_array($currentDuration, $watchedDurationArr)) {
array_push($watchedDurationArr, $currentDuration);
} }
$watchedDurationJson = json_encode($watchedDurationArr); // Pastikan tidak ada duplikat
if (!in_array($currentDuration, $watchedDurationArr)) {
$watchedDurationArr[] = $currentDuration;
sort($watchedDurationArr);
}
DB::table('watch_durations') DB::table('watch_durations')
->where([ ->where([
@ -109,8 +123,9 @@ class HomeController extends Controller
'watched_student_id' => $userId, 'watched_student_id' => $userId,
]) ])
->update([ ->update([
'watched_counter' => $watchedDurationJson, 'watched_counter' => json_encode($watchedDurationArr),
'current_duration' => $currentDuration, 'current_duration' => $currentDuration,
'updated_at' => now(),
]); ]);
} else { } else {
$watchedDurationArr = [$currentDuration]; $watchedDurationArr = [$currentDuration];
@ -120,38 +135,65 @@ class HomeController extends Controller
'watched_student_id' => $userId, 'watched_student_id' => $userId,
'current_duration' => $currentDuration, 'current_duration' => $currentDuration,
'watched_counter' => json_encode($watchedDurationArr), 'watched_counter' => json_encode($watchedDurationArr),
'created_at' => now(),
'updated_at' => now(),
]); ]);
} }
// Jika drip content tidak aktif, return early
if ($courseDetails->enable_drip_content != 1) { if ($courseDetails->enable_drip_content != 1) {
return response()->json([ return response()->json([
'lesson_id' => $lessonId, 'lesson_id' => $lessonId,
'course_progress' => null, 'course_progress' => 0,
'is_completed' => null 'is_completed' => 0
]); ]);
} }
// Fetch lesson details for duration calculations // Fetch lesson details for duration calculations
$lessonTotalDuration = DB::table('lessons')->where('id', $lessonId)->value('duration'); $lessonTotalDuration = DB::table('lessons')->where('id', $lessonId)->value('duration');
if (!$lessonTotalDuration) {
return response()->json([
'lesson_id' => $lessonId,
'course_progress' => 0,
'is_completed' => 0,
'error' => 'Lesson duration not found'
]);
}
$lessonTotalDurationArr = explode(':', $lessonTotalDuration); $lessonTotalDurationArr = explode(':', $lessonTotalDuration);
$lessonTotalSeconds = ($lessonTotalDurationArr[0] * 3600) + ($lessonTotalDurationArr[1] * 60) + $lessonTotalDurationArr[2]; if (count($lessonTotalDurationArr) < 3) {
$currentTotalSeconds = count($watchedDurationArr) * 5; // Assuming each increment represents 5 seconds return response()->json([
'lesson_id' => $lessonId,
'course_progress' => 0,
'is_completed' => 0,
'error' => 'Invalid duration format'
]);
}
$lessonTotalSeconds = ($lessonTotalDurationArr[0] * 3600) +
($lessonTotalDurationArr[1] * 60) +
$lessonTotalDurationArr[2];
$currentTotalSeconds = count($watchedDurationArr) * 5;
// Drip content completion logic // Drip content completion logic
if (!empty($dripContentSettings) && isset($dripContentSettings['lesson_completion_role'])) {
if ($dripContentSettings['lesson_completion_role'] == 'duration') { if ($dripContentSettings['lesson_completion_role'] == 'duration') {
if ($currentTotalSeconds >= $dripContentSettings['minimum_duration']) { $minimumDuration = $dripContentSettings['minimum_duration'] ?? 0;
if ($currentTotalSeconds >= $minimumDuration) {
$isCompleted = 1; $isCompleted = 1;
} elseif (($currentTotalSeconds + 4) >= $lessonTotalSeconds) { } elseif (($currentTotalSeconds + 4) >= $lessonTotalSeconds) {
$isCompleted = 1; $isCompleted = 1;
} }
} else { } else {
$requiredDuration = ($lessonTotalSeconds / 100) * $dripContentSettings['minimum_percentage']; $minimumPercentage = $dripContentSettings['minimum_percentage'] ?? 0;
$requiredDuration = ($lessonTotalSeconds / 100) * $minimumPercentage;
if ($currentTotalSeconds >= $requiredDuration) { if ($currentTotalSeconds >= $requiredDuration) {
$isCompleted = 1; $isCompleted = 1;
} elseif (($currentTotalSeconds + 4) >= $lessonTotalSeconds) { } elseif (($currentTotalSeconds + 4) >= $lessonTotalSeconds) {
$isCompleted = 1; $isCompleted = 1;
} }
} }
}
// Update course progress if the lesson is completed // Update course progress if the lesson is completed
if ($isCompleted == 1) { if ($isCompleted == 1) {
@ -164,27 +206,92 @@ class HomeController extends Controller
if ($watchHistory) { if ($watchHistory) {
$lessonIds = json_decode($watchHistory->completed_lesson, true); $lessonIds = json_decode($watchHistory->completed_lesson, true);
$courseProgress = $watchHistory->course_progress;
if (!is_array($lessonIds)) $lessonIds = []; if (!is_array($lessonIds)) {
$lessonIds = [];
} else {
// Konversi semua nilai ke integer
$lessonIds = array_map('intval', $lessonIds);
$lessonIds = array_values(array_unique($lessonIds));
sort($lessonIds);
}
if (!in_array($lessonId, $lessonIds)) { if (!in_array($lessonId, $lessonIds)) {
array_push($lessonIds, $lessonId); // Tambahkan lesson baru
$totalLesson = DB::table('lessons')->where('course_id', $courseId)->count(); $lessonIds[] = $lessonId;
$courseProgress = (100 / $totalLesson) * count($lessonIds); $lessonIds = array_unique($lessonIds);
sort($lessonIds);
$completedDate = ($courseProgress >= 100 && !$watchHistory->completed_date) // Hitung total lesson yang valid di course
? time() $totalLesson = DB::table('lessons')->where('course_id', $courseId)->count();
: $watchHistory->completed_date;
// Hitung progress yang akurat
if ($totalLesson > 0) {
// Ambil semua lesson ID yang valid di course ini
$validLessonIds = DB::table('lessons')
->where('course_id', $courseId)
->pluck('id')
->map(function($id) {
return (int)$id;
})
->toArray();
// Hitung hanya lesson yang valid (ada di course ini)
$completedValidLessons = array_intersect($lessonIds, $validLessonIds);
$completedCount = count($completedValidLessons);
$courseProgress = ($completedCount / $totalLesson) * 100;
$courseProgress = round($courseProgress, 2);
$courseProgress = min(100, $courseProgress); // Pastikan tidak lebih dari 100%
}
// Tentukan completed_date
$updateData = [
'course_progress' => $courseProgress,
'completed_lesson' => json_encode($lessonIds, JSON_NUMERIC_CHECK), // Flag penting!
'watching_lesson_id' => $lessonId,
'updated_at' => now(),
];
// Set completed_date hanya jika progress 100% dan belum ada
if ($courseProgress >= 100 && !$watchHistory->completed_date) {
$updateData['completed_date'] = now();
} elseif ($watchHistory->completed_date) {
$updateData['completed_date'] = $watchHistory->completed_date;
}
DB::table('watch_histories') DB::table('watch_histories')
->where('id', $watchHistory->id) ->where('id', $watchHistory->id)
->update([ ->update($updateData);
'course_progress' => $courseProgress,
'completed_lesson' => json_encode($lessonIds), } else {
'completed_date' => $completedDate, // Jika lesson sudah ada, ambil progress yang ada
]); $courseProgress = $watchHistory->course_progress ?? 0;
} }
} else {
// Buat record baru di watch_histories
$lessonIds = [$lessonId];
$totalLesson = DB::table('lessons')->where('course_id', $courseId)->count();
if ($totalLesson > 0) {
$courseProgress = (1 / $totalLesson) * 100;
$courseProgress = round($courseProgress, 2);
}
$isCourseCompleted = $courseProgress >= 100;
dd(json_encode($lessonIds, JSON_NUMERIC_CHECK));
DB::table('watch_histories')->insert([
'course_id' => $courseId,
'student_id' => $userId,
'watching_lesson_id' => $lessonId,
'completed_lesson' => json_encode($lessonIds, JSON_NUMERIC_CHECK),
'course_progress' => $courseProgress,
'completed_date' => $isCourseCompleted ? now() : null,
'created_at' => now(),
'updated_at' => now(),
]);
} }
} }
@ -193,6 +300,8 @@ class HomeController extends Controller
'lesson_id' => $lessonId, 'lesson_id' => $lessonId,
'course_progress' => round($courseProgress), 'course_progress' => round($courseProgress),
'is_completed' => $isCompleted, 'is_completed' => $isCompleted,
'current_total_seconds' => $currentTotalSeconds,
'lesson_total_seconds' => $lessonTotalSeconds,
]); ]);
} }

View File

@ -0,0 +1,309 @@
<?php
namespace App\Http\Controllers\instructor;
use App\Http\Controllers\Controller;
use App\Models\Lesson;
use App\Models\ProjectSubmission;
use App\Models\Watch_history;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
class ProjectController extends Controller
{
public function update(Request $request, $id)
{
$request->validate([
'status' => 'required|in:1,2',
'comment' => 'required_if:status,2',
]);
$submission = ProjectSubmission::findOrFail($id);
$submission->status = $request->status;
$submission->reviewed_by = auth()->id();
$submission->reviewed_at = now();
if ($request->status == 1) {
$submission->comment = null;
} else {
$submission->comment = $request->comment;
}
$submission->save();
return redirect()->back()->with('success', get_phrase('Project graded successfully'));
}
public function store(Request $request)
{
$title = Lesson::join('sections', 'lessons.section_id', 'sections.id')
->join('courses', 'sections.course_id', 'courses.id')
->where('courses.user_id', auth()->user()->id)
->where('lessons.title', $request->title)
->first();
if ($title) {
Session::flash('error', get_phrase('Title has been taken.'));
return redirect()->back();
}
$maxSort = Lesson::where('section_id', $request->section)
->max('sort');
$data['user_id'] = auth()->user()->id;
$data['sort'] = $maxSort + 1;
$data['title'] = $request->title;
$data['course_id'] = $request->course_id;
$data['section_id'] = $request->section;
$data['description'] = $request->description;
$data['attachment'] = $request->attachment;
$data['summary'] = $request->summary;
$data['lesson_type'] = 'project';
$data['status'] = 1;
Lesson::insert($data);
Session::flash('success', get_phrase('Project has been created.'));
return redirect()->back();
}
public function getIndex($id)
{
$participants = ProjectSubmission::join('users', 'project_submissions.user_id', '=', 'users.id')
->where('project_submissions.lesson_id', $id)
->select('users.name', 'users.id')
->distinct()
->get();
return view('instructor.project.grading.index', ['participants' => $participants, 'lesson_id' => $id]);
}
public function getPreview(Request $request)
{
// Validasi input
$request->validate([
'lesson_id' => 'required|integer',
'user_id' => 'required|integer'
]);
// Cari submission terbaru
$submission = ProjectSubmission::where('lesson_id', $request->lesson_id)
->where('user_id', $request->user_id)
->latest()
->first();
// dd($submission); // <-- INI HARUS DIHAPUS ATAU DI-KOMEN
// Tentukan label komentar
$commentLabel = (!empty($submission) && !empty($submission->comment))
? "Revisi Sebelumnya"
: "Masukkan Revisi Anda";
// Return view preview
return view('instructor.project.grading.preview', compact('submission', 'commentLabel'));
}
public function getParticipantSubmission(Request $request)
{
$submissions = ProjectSubmission::where('lesson_id', $request->quizId)
->where('user_id', $request->participant)
->orderBy('created_at', 'desc')
->get();
$html = '<option value="">' . get_phrase('Select a submission') . '</option>';
if($submissions->count() > 0){
foreach ($submissions as $submission) {
$date = date('d M Y, H:i', strtotime($submission->created_at));
$statusLabel = '';
if($submission->status == 1) $statusLabel = ' (Lulus)';
elseif($submission->status == 2) $statusLabel = ' (Revisi)';
else $statusLabel = ' (Pending)';
$html .= '<option value="' . $submission->id . '">' . $date . $statusLabel . '</option>';
}
} else {
$html .= '<option value="" disabled>' . get_phrase('No submission found') . '</option>';
}
return response()->json($html);
}
public function updateSubmission(Request $request, $id)
{
$request->validate([
'status' => 'required|in:1,2',
'comment' => 'required_if:status,2', // Wajib isi jika status 2 (Revisi)
]);
// Menggunakan Model findOrFail
$submission = ProjectSubmission::findOrFail($id);
$submission->status = $request->status;
$submission->reviewed_by = auth()->id();
$submission->reviewed_at = now();
if ($request->status == 1) {
// Jika Diterima (1), Comment jadi NULL (sesuai request)
$submission->comment = null;
} else {
// Jika Perlu Perbaikan (2), Simpan Comment
$submission->comment = $request->comment;
}
$submission->save();
// **LOGIKA WATCH HISTORY - DIPERBAIKI berdasarkan referensi**
$lesson = Lesson::findOrFail($submission->lesson_id);
$student_id = $submission->user_id;
$course_id = $lesson->course_id;
$lesson_id = $submission->lesson_id;
// Ambil semua lesson ID untuk course ini
$total_lesson = Lesson::where('course_id', $course_id)
->pluck('id')
->map(function($id) {
return (int)$id;
})
->toArray();
// Cari watch_history untuk student ini
$watch_history = Watch_history::where('course_id', $course_id)
->where('student_id', $student_id)
->first();
// **LOGIKA UTAMA berdasarkan referensi set_watch_history**
if ($watch_history) {
// Parse data lama dengan handling yang robust
$completed_lessons = [];
if (!empty($watch_history->completed_lesson)) {
$decoded = json_decode($watch_history->completed_lesson, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
// Konversi semua nilai ke integer
$completed_lessons = array_map('intval', $decoded);
} else {
// Fallback jika format tidak valid
$completed_lesson_data = trim($watch_history->completed_lesson, '[]"\' ');
if (!empty($completed_lesson_data)) {
$completed_lessons = array_map('intval', explode(',', $completed_lesson_data));
}
}
}
// Hapus duplikat dan sortir
$completed_lessons = array_unique($completed_lessons);
sort($completed_lessons);
// **HANYA tambahkan lesson jika project DITERIMA (status 1)**
if ($request->status == 1 && !in_array($lesson_id, $completed_lessons)) {
$completed_lessons[] = $lesson_id;
sort($completed_lessons);
}
// Hitung completion dengan benar (seperti referensi)
$completed_in_course = array_intersect($completed_lessons, $total_lesson);
$is_completed = count($total_lesson) > 0 &&
count($completed_in_course) >= count($total_lesson);
$data = [
'course_id' => $course_id,
'student_id' => $student_id,
'watching_lesson_id' => $lesson_id,
'completed_lesson' => json_encode($completed_lessons, JSON_NUMERIC_CHECK), // Gunakan JSON_NUMERIC_CHECK
'completed_date' => $is_completed ? date('Y-m-d H:i:s') : null, // Format MySQL datetime
];
// **FIX: Update dengan student_id yang benar (bukan auth user)**
Watch_history::where('course_id', $course_id)
->where('student_id', $student_id)
->update($data);
// Log untuk debugging
\Log::info('Project submission watch history updated:', [
'project_submission_id' => $id,
'student_id' => $student_id,
'course_id' => $course_id,
'lesson_id' => $lesson_id,
'completed_lessons' => $completed_lessons,
'is_completed' => $is_completed,
'total_lessons_in_course' => count($total_lesson)
]);
} else {
// **Jika tidak ada watch_history, buat baru**
// Hanya buat jika project DITERIMA (status 1)
if ($request->status == 1) {
$completed_lessons = [$lesson_id];
// Hitung completion untuk data baru
$is_completed = count($total_lesson) == 1 && in_array($lesson_id, $total_lesson);
$data = [
'course_id' => $course_id,
'student_id' => $student_id,
'watching_lesson_id' => $lesson_id,
'completed_lesson' => json_encode($completed_lessons, JSON_NUMERIC_CHECK),
'completed_date' => $is_completed ? date('Y-m-d H:i:s') : null,
'created_at' => now(),
'updated_at' => now(),
];
Watch_history::create($data);
\Log::info('New watch history created from project submission:', [
'project_submission_id' => $id,
'student_id' => $student_id,
'course_id' => $course_id,
'lesson_id' => $lesson_id
]);
}
}
// **LOGIKA SERTIFIKAT - dengan progress yang benar**
$completed_lessons = $completed_lessons ?? [];
$completed_in_course = array_intersect($completed_lessons, $total_lesson);
$completed_count = count($completed_in_course);
$total_count = count($total_lesson);
$progress_percentage = 0;
if ($total_count > 0) {
$progress_percentage = ($completed_count / $total_count) * 100;
}
// Jika progress 100% dan status project diterima, buat sertifikat
if ($progress_percentage >= 100 && $request->status == 1) {
$certificateExists = Certificate::where('user_id', $student_id)
->where('course_id', $course_id)
->exists();
if (!$certificateExists) {
// Cek jika method generateIdentifier ada
if (method_exists($this, 'generateIdentifier')) {
$identifier = $this->generateIdentifier(12);
} else {
// Fallback ke random string
$identifier = \Illuminate\Support\Str::random(12);
}
Certificate::create([
'user_id' => $student_id,
'course_id' => $course_id,
'identifier' => $identifier,
]);
\Log::info('Certificate created after project submission:', [
'student_id' => $student_id,
'course_id' => $course_id,
'project_submission_id' => $id
]);
}
}
return redirect()->back()->with('success', get_phrase('Project graded successfully'));
}
}

View File

@ -56,6 +56,11 @@ class QuizController extends Controller
return redirect()->back(); return redirect()->back();
} }
$maxSort = Lesson::where('section_id', $request->section)
->max('sort');
$data['user_id'] = auth()->user()->id;
$data['sort'] = $maxSort + 1;
$data['title'] = $request->title; $data['title'] = $request->title;
$data['course_id'] = $request->course_id; $data['course_id'] = $request->course_id;
$data['section_id'] = $request->section; $data['section_id'] = $request->section;

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\student;
use App\Http\Controllers\Controller;
use App\Models\Lesson;
use App\Models\ProjectSubmission;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Validator;
use Carbon\Carbon;
class ProjectController extends Controller
{
public function submit(Request $request)
{
$request->validate([
'lesson_id' => 'required|exists:lessons,id',
'submission_link' => 'required|url',
]);
$userId = auth()->user()->id;
$lessonId = $request->lesson_id;
$submission = ProjectSubmission::where('user_id', $userId)
->where('lesson_id', $lessonId)
->first();
if ($submission) {
$submission->drive_link = $request->submission_link;
$submission->status = 0;
$submission->submitted_at = Carbon::now();
$submission->save();
$message = get_phrase('Project successfully resubmitted.');
} else {
$submission = new ProjectSubmission();
$submission->user_id = $userId;
$submission->lesson_id = $lessonId;
$submission->submitted_at = Carbon::now();
$submission->drive_link = $request->submission_link;
$submission->status = 0;
$submission->save();
$message = get_phrase('Project successfully submitted.');
}
session()->flash('success', $message);
return redirect()->back();
}
}

View File

@ -137,21 +137,24 @@ class QuizController extends Controller
{ {
// Validasi Course // Validasi Course
$course = Course::where('id', $course_id)->first(); $course = Course::where('id', $course_id)->first();
if (!$course) return; if (!$course) return false;
// Validasi Enrollment // Validasi Enrollment
$enrollment = Enrollment::where('course_id', $course->id) $enrollment = Enrollment::where('course_id', $course->id)
->where('user_id', $student_id) ->where('user_id', $student_id)
->exists(); ->exists();
// Cek Role (Jika admin/instruktur boleh bypass enrollment check) // Cek Role
$currentUserRole = auth()->check() ? auth()->user()->role : 'student'; $currentUserRole = auth()->check() ? auth()->user()->role : 'student';
if (!$enrollment && ($currentUserRole != 'admin' && !is_course_instructor($course->id))) { if (!$enrollment && ($currentUserRole != 'admin' && !is_course_instructor($course->id))) {
return; return false;
} }
// Ambil semua lesson ID untuk hitung progress manual // Ambil semua lesson ID
$total_lesson_ids = Lesson::where('course_id', $course_id)->pluck('id')->toArray(); $total_lesson_ids = Lesson::where('course_id', $course_id)
->orderBy('id', 'asc')
->pluck('id')
->toArray();
// Ambil data history user // Ambil data history user
$watch_history = Watch_history::where('course_id', $course_id) $watch_history = Watch_history::where('course_id', $course_id)
@ -160,43 +163,72 @@ class QuizController extends Controller
$completed_lessons = []; $completed_lessons = [];
// Parse data lama // Parse data lama dengan cara yang lebih aman
if ($watch_history && $watch_history->completed_lesson) { if ($watch_history && !empty($watch_history->completed_lesson)) {
$decoded = json_decode($watch_history->completed_lesson, true); $completed_lesson_data = $watch_history->completed_lesson;
if (is_array($decoded)) {
// Coba decode JSON
$decoded = json_decode($completed_lesson_data, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
$completed_lessons = $decoded; $completed_lessons = $decoded;
} else {
// Jika bukan JSON valid, coba parsing manual
$completed_lesson_data = trim($completed_lesson_data, '[]"\' ');
if (!empty($completed_lesson_data)) {
$completed_lessons = array_map('intval', explode(',', $completed_lesson_data));
$completed_lessons = array_unique($completed_lessons); // pastikan unik
}
} }
} }
// Tambahkan lesson_id baru jika belum ada // HANYA tambahkan lesson saat ini jika belum ada
if (!in_array($lesson_id, $completed_lessons)) { $lesson_id_int = (int)$lesson_id;
$completed_lessons[] = $lesson_id; if (!in_array($lesson_id_int, $completed_lessons)) {
$completed_lessons[] = $lesson_id_int;
} }
// Tentukan apakah course selesai // Sort dan pastikan unique
$is_completed = count($total_lesson_ids) <= count($completed_lessons); sort($completed_lessons);
$completed_lessons = array_values(array_unique($completed_lessons));
// Course selesai jika semua lesson sudah completed
$all_lessons_completed = true;
foreach ($total_lesson_ids as $total_lesson_id) {
if (!in_array($total_lesson_id, $completed_lessons)) {
$all_lessons_completed = false;
break;
}
}
$is_completed = $all_lessons_completed;
$data = [ $data = [
'course_id' => $course_id, 'course_id' => $course_id,
'student_id' => $student_id, 'student_id' => $student_id,
'watching_lesson_id' => $lesson_id, 'watching_lesson_id' => $lesson_id,
'completed_lesson' => json_encode($completed_lessons), 'completed_lesson' => json_encode($completed_lessons),
'completed_date' => $is_completed ? time() : null, 'completed_date' => $is_completed ? now() : null,
]; ];
// Debug final data
// dd($data);
// Simpan ke DB // Simpan ke DB
if ($watch_history) { if ($watch_history) {
Watch_history::where('id', $watch_history->id)->update($data); // Gunakan ini untuk memastikan hanya update yang diperlukan
$watch_history->update($data);
} else { } else {
// Gunakan create agar timestamp created_at terisi otomatis
Watch_history::create($data); Watch_history::create($data);
} }
// --- LOGIKA SERTIFIKAT --- // **LOGIKA SERTIFIKAT - dengan progress yang benar**
// Hitung progress secara manual agar lebih akurat $completed_count = count(array_intersect($completed_lessons, $total_lesson_ids));
$total_count = count($total_lesson_ids);
$progress_percentage = 0; $progress_percentage = 0;
if (count($total_lesson_ids) > 0) { if ($total_count > 0) {
$progress_percentage = (count($completed_lessons) / count($total_lesson_ids)) * 100; $progress_percentage = ($completed_count / $total_count) * 100;
} }
if ($progress_percentage >= 100) { if ($progress_percentage >= 100) {

View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProjectSubmission extends Model
{
use HasFactory;
protected $fillable = [
'lesson_id',
'user_id',
'drive_link',
'status',
'comment',
'reviewed_at',
'resubmitted_at',
'reviewed_by',
'created_at',
'updated_at'
];
}

View File

@ -47,7 +47,7 @@
<section class="video-playlist-section"> <section class="video-playlist-section">
<div class="my-container"> <div class="my-container">
<div class="row"> <div class="row">
<div class="col-lg-8" id="player_content"> <div class="col-lg-8" id="player_content"> -->
@if(in_array($lesson_details->id, get_locked_lesson_ids($course_details->id, auth()->user()->id)) && $course_details->enable_drip_content) @if(in_array($lesson_details->id, get_locked_lesson_ids($course_details->id, auth()->user()->id)) && $course_details->enable_drip_content)
@php @php
$drip_content_settings = json_decode($course_details->drip_content_settings); $drip_content_settings = json_decode($course_details->drip_content_settings);

View File

@ -40,8 +40,8 @@
@include('course_player.player_config') @include('course_player.player_config')
@else @else
<div class="alert alert-danger text-center py-5"> <div class="alert alert-danger text-center py-5">
<h5>Invalid Google Drive Link</h5> <h5>{{ get_phrase('Invalid Google Drive Link') }}</h5>
<p>Could not extract video ID from the provided URL.</p> <p>{{ get_phrase('Could not extract video ID from the provided URL.') }}</p>
</div> </div>
@endif @endif
@elseif($lesson_details->lesson_type == 'image') @elseif($lesson_details->lesson_type == 'image')
@ -62,7 +62,7 @@
<div id="{{ $fullscreen_id }}" class="w-100 border rounded overflow-hidden bg-light"> <div id="{{ $fullscreen_id }}" class="w-100 border rounded overflow-hidden bg-light">
@if ($lesson_details->attachment_type == 'pdf') @if ($lesson_details->attachment_type == 'pdf')
<a href="{{ $direct_pdf }}" target="_blank" class="btn btn-sm position-absolute top-0 end-0 mt-2 me-2 shadow" style="z-index: 1050; color: white; background: #001151;" title="Buka PDF di Tab Baru (tekan F11 untuk full screen)"> <a href="{{ $direct_pdf }}" target="_blank" class="btn btn-sm position-absolute top-0 end-0 mt-2 me-2 shadow" style="z-index: 1050; color: white; background: #001151;" title="Buka PDF di Tab Baru (tekan F11 untuk full screen)">
<i class="bi bi-box-arrow-up-right"></i> Buka Full PDF <i class="bi bi-box-arrow-up-right"></i> {{ get_phrase('Open Full PDF') }}
</a> </a>
<iframe src="{{ $pdf_src }}#toolbar=1&navpanes=0&scrollbar=1&view=FitH" <iframe src="{{ $pdf_src }}#toolbar=1&navpanes=0&scrollbar=1&view=FitH"
class="w-100 border-0" class="w-100 border-0"
@ -82,10 +82,10 @@
<div class="text-center mb-3"> <div class="text-center mb-3">
<p class="mt-3 mb-1 fw-bold"> <p class="mt-3 mb-1 fw-bold">
Dokumen tidak dapat ditampilkan secara langsung {{ get_phrase('The document cannot be displayed directly.') }}
</p> </p>
<p class="text-muted small"> <p class="text-muted small">
Silakan unduh file untuk melihat isi dokumen {{ get_phrase('Please download the file to view the contents of the document.') }}
</p> </p>
</div> </div>
@ -94,7 +94,7 @@
target="_blank" target="_blank"
download> download>
<i class="fa fa-download me-1"></i> <i class="fa fa-download me-1"></i>
Download Dokumen {{ get_phrase('Download Document') }}
</a> </a>
</div> </div>
@endif @endif
@ -104,6 +104,10 @@
<div class="course-video-area border-primary pb-5"> <div class="course-video-area border-primary pb-5">
@include('course_player.quiz.index') @include('course_player.quiz.index')
</div> </div>
@elseif($lesson_details->lesson_type == 'project')
<div class="course-video-area border-primary pb-5">
@include('course_player.project.index')
</div>
@else @else
<iframe class="embed-responsive-item" width="100%" src="{{ $lesson_details->lesson_src }}" allowfullscreen></iframe> <iframe class="embed-responsive-item" width="100%" src="{{ $lesson_details->lesson_src }}" allowfullscreen></iframe>
@endif @endif

View File

@ -0,0 +1,163 @@
@php
$project = App\Models\Lesson::where('id', request()->route()->parameter('id'))->firstOrFail();
$submission = DB::table('project_submissions')
->where('lesson_id', $project->id)
->where('user_id', auth()->id())
->first();
@endphp
<style>
/* Mengatur agar list dan blockquote tidak memakan banyak ruang */
.description blockquote{
margin: 5px 0 !important;
padding: 0 10px !important;
}
.description ul, ul {
list-style-type: disc !important;
list-style-position: inside !important;
margin-left: 10px !important;
display: block !important;
}
.description li {
display: list-item !important;
}
</style>
<div class="row px-4">
<div class="col-12">
<h4 class="quiz-title text-center mt-4 fw-bold">
<i class="fas fa-project-diagram me-2"></i> {{ $project->title }}
</h4>
<hr>
<div class="row mb-5">
<div class="col-12 mb-2">
<div class="card shadow-sm border-0">
<div class="card-body">
<h5 class="card-title text-primary mb-3">
<i class="fas fa-file-alt me-2"></i>{{ get_phrase('Project Details') }}
</h5>
<div class="description text-secondary">
{!! $project->description !!}
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0 bg-light">
<div class="card-body">
<h5 class="card-title text-danger mb-3">
<i class="fas fa-clipboard-check me-2"></i>{{ get_phrase('Assessment criteria') }}
</h5>
<div class="description small text-muted">
{!! $project->attachment !!}
</div>
</div>
</div>
</div>
</div>
<div class="submission-area p-4 border rounded mb-5">
<h5 class="mb-3">{{ get_phrase('Submit Assignment') }}</h5>
{{-- KONDISI 1: JIKA BELUM PERNAH SUBMIT --}}
@if(!$submission)
<div class="alert alert-info">
<i class="fas fa-info-circle me-1"></i> {{ get_phrase('Please enter your project link below to be assessed.') }}
</div>
<form action="{{ route('student.project.submit') }}" method="POST">
@csrf
<input type="hidden" name="lesson_id" value="{{ $project->id }}">
<div class="mb-3">
<label for="project_link" class="form-label fw-bold">{{ get_phrase('Project Link (Google Drive/Github/Website)') }}</label>
<input type="url" class="form-control" id="project_link" name="submission_link" placeholder="https://..." required>
<div class="form-text">{{ get_phrase('Make sure the link is accessible to the instructor (Public/Anyone with link).') }}</div>
</div>
<button type="submit" class="eBtn gradient border-0">
<i class="fas fa-paper-plane me-1"></i> {{ get_phrase('Submit Project') }}
</button>
</form>
{{-- KONDISI 2: JIKA SUDAH PERNAH SUBMIT --}}
@else
{{-- STATUS 0: MENUNGGU DIPERIKSA --}}
@if($submission->status == 0)
<div class="text-center p-5 border rounded bg-light">
<i class="fas fa-clock fa-3x text-warning mb-3"></i>
<h5 class="fw-bold">{{ get_phrase('Project is Being Reviewed') }}</h5>
<p class="text-muted">
{{ get_phrase('Thank you! We have received your project and it is currently in the instructor\'s review queue.') }}
</p>
<div class="mt-3">
<span class="text-secondary small">{{ get_phrase('Link that you submitted :') }} </span><br>
<a href="{{ $submission->drive_link }}" target="_blank" class="eBtn gradient border-0 btn-sm mt-1">
<i class="fas fa-external-link-alt me-1"></i> {{ get_phrase('View Your Project') }}
</a>
</div>
</div>
{{-- STATUS 2: PERLU REVISI --}}
@elseif($submission->status == 2)
<div class="alert alert-danger border-danger">
<h5 class="alert-heading fw-bold">
<i class="fas fa-exclamation-circle me-2"></i>{{ get_phrase('Project Needs Revision') }}
</h5>
<p>{{ get_phrase('The instructor has reviewed your project and provided feedback for improvement.') }}</p>
<hr>
{{-- Menampilkan Komentar Instruktur --}}
<div class="mb-3">
<label class="fw-bold text-danger">{{ get_phrase('Instructor\'s Notes / Feedback:') }}</label>
<div class="bg-white p-3 rounded border border-danger text-dark mt-1">
{!! $submission->comment !!}
</div>
</div>
{{-- Form Resubmit --}}
<div class="mt-4">
<h6 class="fw-bold">{{ get_phrase('Submit Revision:') }}</h6>
<form action="{{ route('student.project.submit') }}" method="POST">
@csrf
<input type="hidden" name="lesson_id" value="{{ $project->id }}">
<div class="input-group">
{{-- PERBAIKAN: name diubah jadi submission_link --}}
<input type="url" class="form-control" name="submission_link" placeholder="Masukkan link projek yang sudah diperbaiki..." required>
<button class="btn btn-danger" type="submit">
<i class="fas fa-redo me-1"></i> {{ get_phrase('Submit Revisions') }}
</button>
</div>
<div class="form-text text-danger">{{ get_phrase('Make sure the new link includes the requested fix.') }} </div>
</form>
</div>
</div>
{{-- STATUS 1: DITERIMA / LULUS --}}
@elseif($submission->status == 1)
<div class="text-center p-5 border rounded bg-white border-success">
<i class="fas fa-check-circle fa-3x text-success mb-3"></i>
<h5 class="fw-bold text-success">{{ get_phrase('Congratulations! Project Accepted') }}</h5>
<p class="text-muted">
{{ get_phrase('Good work! Your project has met the assessment criteria.') }}
</p>
@if($submission->comment)
<div class="alert alert-success d-inline-block text-start mt-2">
<strong>{{ get_phrase('Instructor\'s Comment:') }}</strong> <br> {{ $submission->comment }}
</div>
@endif
</div>
@endif
@endif
</div>
</div>
</div>

View File

@ -9,7 +9,10 @@
<a href="#" onclick="ajaxModal('{{ route('modal', ['instructor.quiz.create', 'id' => $course_details->id]) }}', '{{ get_phrase('Add new quiz') }}')" class="btn ol-btn-light ol-btn-sm">{{ get_phrase('Add quiz') }}</a> <a href="#" onclick="ajaxModal('{{ route('modal', ['instructor.quiz.create', 'id' => $course_details->id]) }}', '{{ get_phrase('Add new quiz') }}')" class="btn ol-btn-light ol-btn-sm">{{ get_phrase('Add quiz') }}</a>
<a href="#" onclick="ajaxModal('{{ route('modal', ['instructor.project.create', 'id' => $course_details->id]) }}', '{{ get_phrase('Add new project') }}')" class="btn ol-btn-light ol-btn-sm">{{ get_phrase('Add project') }}</a>
<a href="#" onclick="ajaxModal('{{ route('modal', ['instructor.course.section_sort', 'id' => $course_details->id]) }}', '{{ get_phrase('Sort sections') }}')" class="btn ol-btn-light ol-btn-sm">{{ get_phrase('Sort Section') }}</a> <a href="#" onclick="ajaxModal('{{ route('modal', ['instructor.course.section_sort', 'id' => $course_details->id]) }}', '{{ get_phrase('Sort sections') }}')" class="btn ol-btn-light ol-btn-sm">{{ get_phrase('Sort Section') }}</a>
@endif @endif
</div> </div>
@ -58,7 +61,7 @@
<div class="buttons"> <div class="buttons">
@if ($lesson->lesson_type == 'quiz') @if ($lesson->lesson_type == 'quiz')
<a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Result') }}" onclick="ajaxModal('{{ route('modal', ['instructor.quiz_result.index', 'id' => $lesson->id]) }}', '{{ get_phrase('Result') }}', 'modal-xl')" class="edit-delete"> <a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Result') }}" onclick="ajaxModal('{{ route('modal', ['instructor.quiz_result.index', 'id' => $lesson->id]) }}', '{{ get_phrase('Result') }}', 'modal-xl')" class="edit-delete">
<span class="fi fi-rr-clipboard-list-check"></span> <span class="fi fi-rr-paperclip-vertical"></span>
</a> </a>
<a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Questions') }}" onclick="ajaxModal('{{ route('modal', ['instructor.questions.index', 'id' => $lesson->id]) }}', '{{ get_phrase('Questions') }}', 'modal-lg')" class="edit-delete"> <a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Questions') }}" onclick="ajaxModal('{{ route('modal', ['instructor.questions.index', 'id' => $lesson->id]) }}', '{{ get_phrase('Questions') }}', 'modal-lg')" class="edit-delete">
@ -68,9 +71,20 @@
<a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Edit quiz') }}" onclick="ajaxModal('{{ route('modal', ['instructor.quiz.edit', 'id' => $lesson->id]) }}', '{{ get_phrase('Edit quiz') }}')" class="edit-delete"> <a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Edit quiz') }}" onclick="ajaxModal('{{ route('modal', ['instructor.quiz.edit', 'id' => $lesson->id]) }}', '{{ get_phrase('Edit quiz') }}')" class="edit-delete">
<span class="fi-rr-pencil"></span> <span class="fi-rr-pencil"></span>
</a> </a>
@elseif ($lesson->lesson_type == 'project')
<a href="javascript:void(0);"
data-bs-toggle="tooltip"
title="{{ get_phrase('Submissions') }}"
onclick="ajaxModal('{{ route('instructor.project.grading.index', $lesson->id) }}', '{{ get_phrase('Submissions') }}', 'modal-xl')"
class="edit-delete">
<span class="fi fi-rr-clipboard-list"></span>
</a>
<a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Edit project') }}" onclick="ajaxModal('{{ route('modal', ['instructor.project.edit', 'id' => $lesson->id]) }}', '{{ get_phrase('Edit project') }}')" class="edit-delete">
<span class="fi-rr-pencil"></span>
</a>
@endif @endif
@if ($lesson->lesson_type != 'quiz') @if ($lesson->lesson_type != 'quiz' && $lesson->lesson_type != 'project')
<a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Edit lesson') }}" onclick="ajaxModal('{{ route('modal', ['instructor.course.lesson_edit', 'id' => $lesson->id]) }}', '{{ get_phrase('Edit lesson') }}')" class="edit-delete"> <a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Edit lesson') }}" onclick="ajaxModal('{{ route('modal', ['instructor.course.lesson_edit', 'id' => $lesson->id]) }}', '{{ get_phrase('Edit lesson') }}')" class="edit-delete">
<span class="fi-rr-pencil"></span> <span class="fi-rr-pencil"></span>
</a> </a>

View File

@ -14,8 +14,6 @@
{{ get_phrase('Google drive video') }} {{ get_phrase('Google drive video') }}
@elseif($lesson_type == 'document') @elseif($lesson_type == 'document')
{{ get_phrase('Document file') }} {{ get_phrase('Document file') }}
@elseif ($lesson_type == 'scorm')
{{ get_phrase('Scorm content') }}
@else @else
{{ ucfirst($lesson_type) }} {{ ucfirst($lesson_type) }}
@endif @endif
@ -47,14 +45,10 @@
@include('instructor.course.youtube_type_lesson_add') @include('instructor.course.youtube_type_lesson_add')
@elseif ($lesson_type == 'academy_cloud') @elseif ($lesson_type == 'academy_cloud')
@include('instructor.course.academy_cloud_type_lesson_add') @include('instructor.course.academy_cloud_type_lesson_add')
@elseif ($lesson_type == 'vimeo')
@include('instructor.course.vimeo_type_lesson_add')
@elseif ($lesson_type == 'html5') @elseif ($lesson_type == 'html5')
@include('instructor.course.html5_type_lesson_add') @include('instructor.course.html5_type_lesson_add')
@elseif ($lesson_type == 'video') @elseif ($lesson_type == 'video')
@include('instructor.course.video_type_lesson_add') @include('instructor.course.video_type_lesson_add')
@elseif ($lesson_type == 'amazon-s3')
@include('amazon_s3_type_lesson_add.php')
@elseif ($lesson_type == 'google_drive_video') @elseif ($lesson_type == 'google_drive_video')
@include('instructor.course.google_drive_type_lesson_add') @include('instructor.course.google_drive_type_lesson_add')
@elseif ($lesson_type == 'document') @elseif ($lesson_type == 'document')
@ -65,8 +59,6 @@
@include('instructor.course.image_file_type_lesson_add') @include('instructor.course.image_file_type_lesson_add')
@elseif ($lesson_type == 'iframe') @elseif ($lesson_type == 'iframe')
@include('instructor.course.iframe_type_lesson_add') @include('instructor.course.iframe_type_lesson_add')
@elseif ($lesson_type == 'scorm')
@include('instructor.course.scorm_type_lesson_add')
@endif @endif
<div class="form-group mb-3"> <div class="form-group mb-3">

View File

@ -0,0 +1,50 @@
<form action="{{ route('instructor.course.project.store') }}" method="post">@csrf
<input type="hidden" name="course_id" value="{{ $id }}">
<div class="fpb7 mb-3">
<label class="form-label ol-form-label" for="title">
{{ get_phrase('Title') }}
<span class="text-danger ms-1">*</span>
</label>
<input class="form-control ol-form-control" type="text" id="title" name="title" required>
</div>
<div class="row mb-3">
<div class="col-sm-12 fpb-7">
<label class="form-label ol-form-label">
{{ get_phrase('Section') }}
<span class="text-danger ms-1">*</span>
</label>
<select class="form-control ol-form-control ol-select2" data-toggle="select2" name="section">
<option value="">{{ get_phrase('Select an option') }}</option>
@foreach (App\Models\Section::where('course_id', $id)->get() as $section)
<option value="{{ $section->id }}">{{ $section->title }}</option>
@endforeach
</select>
</div>
</div>
<div class="fpb-7 mb-3">
<label for="description"
class="form-label ol-form-label col-form-label">{{ get_phrase('Project Assignment') }}</label>
<textarea name="description" rows="5" class="form-control ol-form-control text_editor"></textarea>
</div>
<div class="fpb-7 mb-3">
<label for="attachment"
class="form-label ol-form-label col-form-label">{{ get_phrase('Assessment Criteria') }}</label>
<textarea name="attachment" rows="5" class="form-control ol-form-control text_editor"></textarea>
</div>
<div class="fpb-7 mb-6">
<label for="attachment"
class="form-label ol-form-label col-form-label">{{ get_phrase('Summary') }}</label>
<textarea name="attachment" rows="5" class="form-control ol-form-control text_editor"></textarea>
</div>
<div class="fpb7">
<button type="submit" class="btn ol-btn-primary">{{ get_phrase('Add Project') }}</button>
</div>
</form>
@include('instructor.init')

View File

@ -0,0 +1,58 @@
@php
$project = App\Models\Lesson::join('sections', 'lessons.section_id', 'sections.id')
->join('courses', 'sections.course_id', 'courses.id')
->select('lessons.*', 'courses.id as course_id')
->where('lessons.id', $id)
->first();
@endphp
<form action="{{ route('instructor.course.project.update', $id) }}" method="post">@csrf
<div class="fpb7 mb-3">
<label class="form-label ol-form-label" for="title">
{{ get_phrase('Title') }}
<span class="text-danger ms-1">*</span>
</label>
<input class="form-control ol-form-control" type="text" id="title" name="title" value="{{ $project->title }}"
required>
</div>
<div class="row mb-3">
<div class="col-sm-12 fpb-7">
<label class="form-label ol-form-label">
{{ get_phrase('Section') }}
<span class="text-danger ms-1">*</span>
</label>
<select class="form-control ol-form-control ol-select2" data-toggle="select2" name="section">
<option value="">{{ get_phrase('Select an option') }}</option>
@foreach (App\Models\Section::where('course_id', $project->course_id)->get() as $section)
<option value="{{ $section->id }}" @if ($section->id == $project->section_id) selected @endif>
{{ $section->title }}</option>
@endforeach
</select>
</div>
</div>
<div class="fpb-7 mb-3">
<label for="description"
class="form-label ol-form-label col-form-label">{{ get_phrase('Project Assignment') }}</label>
<textarea name="description" rows="5" class="form-control ol-form-control text_editor">{!! $project->description !!}</textarea>
</div>
<div class="fpb-7 mb-3">
<label for="attachment"
class="form-label ol-form-label col-form-label">{{ get_phrase('Assessment Criteria') }}</label>
<textarea name="attachment" rows="5" class="form-control ol-form-control text_editor">{!! $project->attachment !!}</textarea>
</div>
<div class="fpb-7 mb-6">
<label for="summary"
class="form-label ol-form-label col-form-label">{{ get_phrase('Summary') }}</label>
<textarea name="summary" rows="5" class="form-control ol-form-control text_editor">{!! $project->summary !!}</textarea>
</div>
<div class="fpb7">
<button type="submit" class="btn ol-btn-primary">{{ get_phrase('Update Project') }}</button>
</div>
</form>
@include('instructor.init')

View File

@ -0,0 +1,121 @@
{{-- Container Utama --}}
<div class="row g-0 h-100">
{{-- KOLOM KIRI: LIST SISWA --}}
<div class="col-md-4 border-end" style="min-height: 400px; max-height: 70vh; overflow-y: auto;">
<div class="p-3 border-bottom bg-light sticky-top">
<h6 class="mb-0 fw-bold"><i class="fas fa-users me-2"></i> {{ get_phrase('Student List') }}</h6>
</div>
<div class="list-group list-group-flush" id="studentList">
@if($participants->isEmpty())
<div class="text-center p-5 text-muted small">
{{ get_phrase('No participants found.') }}
</div>
@else
@foreach ($participants as $participant)
<a href="javascript:void(0);"
class="list-group-item list-group-item-action py-3 student-item"
{{-- PERBAIKAN DISINI: Tambahkan parameter ke-3 yaitu $lesson_id --}}
onclick="loadStudentSubmission(this, '{{ $participant->id }}', '{{ $lesson_id }}')">
<div class="d-flex align-items-center">
<div class="me-3 d-flex align-items-center justify-content-center rounded-circle bg-primary text-white fw-bold"
style="width: 44px; height: 36px; font-size: 14px;">
{{ strtoupper(substr($participant->name, 0, 1)) }}
</div>
<div class="w-100">
<h6 class="mb-0 text-dark" style="font-size: 14px;">{{ $participant->name }}</h6>
<small class="text-muted" style="font-size: 12px;">{{ get_phrase('Click to grade') }}</small>
</div>
<i class="fas fa-chevron-right text-muted small"></i>
</div>
</a>
@endforeach
@endif
</div>
</div>
{{-- KOLOM KANAN: AREA PREVIEW & GRADING --}}
<div class="col-md-8 bg-white">
{{-- ID ini penting: target AJAX --}}
<div id="grading-area" class="h-100 position-relative">
{{-- State Awal (Belum Klik) --}}
<div class="d-flex flex-column align-items-center justify-content-center h-100 text-center p-5">
<div class="mb-3 p-4 rounded-circle bg-light">
<i class="fi fi-rr-box-open fs-1 text-secondary opacity-50"></i>
</div>
<h6 class="text-muted">{{ get_phrase('Select a student to start grading') }}</h6>
</div>
</div>
</div>
</div>
<script>
"use strict";
function loadStudentSubmission(element, userId, lessonId) {
// Cegah event default
event.preventDefault();
// Validasi parameter
if (!userId || !lessonId) {
console.error("Missing parameters. User ID:", userId, "Lesson ID:", lessonId);
alert('Missing parameters');
return;
}
// 1. Visual Active State
$('.student-item').removeClass('active bg-light');
$(element).addClass('active bg-light');
// 2. Loading State
$('#grading-area').html(`
<div class="d-flex flex-column align-items-center justify-content-center h-100">
<div class="spinner-border text-primary mb-2" role="status"></div>
<small class="text-muted">Loading data...</small>
</div>
`);
// Debugging
console.log("AJAX Call Parameters:");
console.log("- Lesson ID:", lessonId);
console.log("- User ID:", userId);
console.log("- URL:", "{{ route('instructor.project.grading.preview') }}");
console.log("- CSRF Token:", $('meta[name="csrf-token"]').attr('content'));
// 3. AJAX Call
$.ajax({
type: "GET",
url: "{{ route('instructor.project.grading.preview') }}",
data: {
lesson_id: lessonId,
user_id: userId
},
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
success: function(response) {
console.log("Success Response:", response.substring(0, 200)); // Log 200 karakter pertama
$('#grading-area').html(response);
},
error: function(xhr, status, error) {
console.error("AJAX Error:", error);
console.error("Status:", status);
console.error("Response:", xhr.responseText);
$('#grading-area').html(`
<div class="alert alert-danger m-3">
<h6>Failed to load data</h6>
<small>Error: ${error}</small>
<br>
<small>Check console for details</small>
</div>
`);
}
});
}
</script>

View File

@ -0,0 +1,182 @@
{{-- HAPUS <div class="row"> jika ada, gunakan div biasa --}}
<div class="h-100">
{{-- KONDISI 1: Belum Submit --}}
@if(!$submission)
<div class="d-flex flex-column align-items-center justify-content-center h-100 text-center p-5">
<div class="mb-3 p-3 bg-light rounded-circle">
<i class="fas fa-times text-danger fa-2x"></i>
</div>
<h5>{{ get_phrase('Not Submitted') }}</h5>
<p class="text-muted">{{ get_phrase('This student has not submitted the project yet.') }}</p>
</div>
{{-- KONDISI 2: Sudah Submit --}}
@else
<div class="p-4">
{{-- Header Status --}}
<div class="d-flex justify-content-between align-items-center mb-4 pb-3 border-bottom">
<div>
<h5 class="fw-bold mb-1">{{ get_phrase('Project Submission') }}</h5>
<small class="text-muted">
<i class="far fa-clock me-1"></i>
{{ date('d M Y, H:i', strtotime($submission->created_at)) }}
</small>
</div>
<div>
@if($submission->status == 1)
<span class="badge bg-success px-3 py-2">{{ get_phrase('Accepted') }}</span>
@elseif($submission->status == 2)
<span class="badge bg-danger px-3 py-2">{{ get_phrase('Revision Needed') }}</span>
@else
<span class="badge bg-warning text-dark px-3 py-2">{{ get_phrase('Pending Review') }}</span>
@endif
</div>
</div>
{{-- Link Project --}}
<div class="mb-4">
<label class="fw-bold mb-2">{{ get_phrase('Student Project Link') }}</label>
<div class="input-group">
<input type="text" class="form-control bg-light" value="{{ $submission->drive_link }}" readonly>
<a href="{{ $submission->drive_link }}" target="_blank" class="btn btn-primary">
<i class="fas fa-external-link-alt"></i> {{ get_phrase('Open') }}
</a>
</div>
</div>
{{-- Form Grading --}}
<div class="card border bg-light shadow-none">
<div class="card-body">
<form action="{{ route('instructor.project.grading.update', $submission->id) }}" method="post" id="gradingForm">
@csrf
<h6 class="fw-bold mb-3"><i class="fas fa-check-double me-2"></i>{{ get_phrase('Grading') }}</h6>
<div class="mb-3">
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="status" id="accept_{{ $submission->id }}" value="1"
{{ $submission->status == 1 ? 'checked' : '' }} onchange="toggleComment(this)">
<label class="btn btn-outline-success" for="accept_{{ $submission->id }}">{{ get_phrase('Passed') }}</label>
<input type="radio" class="btn-check" name="status" id="revise_{{ $submission->id }}" value="2"
{{ $submission->status == 2 ? 'checked' : '' }} onchange="toggleComment(this)">
<label class="btn btn-outline-danger" for="revise_{{ $submission->id }}">{{ get_phrase('Revision Needed') }}</label>
</div>
</div>
{{-- Area Komentar dengan WYSIWYG Editor --}}
<div id="comment_box_area" style="display: {{ $submission->status == 2 ? 'block' : 'none' }};">
<div class="mb-3">
<label class="fw-bold text-danger mb-1">
{{ empty($submission->comment) ? get_phrase('Add Revision Notes') : get_phrase('Previous Revision Notes') }} *
</label>
{{-- Textarea untuk Summernote --}}
<textarea name="comment" id="comment_editor" class="form-control">{{ $submission->comment }}</textarea>
</div>
</div>
<button type="submit" class="btn btn-primary w-100">{{ get_phrase('Save Grading') }}</button>
</form>
</div>
</div>
</div>
@endif
</div>
{{-- Tambahkan CSS Summernote --}}
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css" rel="stylesheet">
{{-- Tambahkan Script Summernote --}}
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
<script>
let commentEditor = null;
function toggleComment(radio) {
let box = document.getElementById('comment_box_area');
if (radio.value == '2') {
$(box).slideDown();
// Inisialisasi editor jika belum diinisialisasi
initSummernote();
// Fokus ke editor
setTimeout(() => {
if (commentEditor && commentEditor.summernote) {
commentEditor.summernote('focus');
}
}, 300);
} else {
$(box).slideUp();
}
}
// Fungsi untuk menginisialisasi Summernote
function initSummernote() {
if (!commentEditor) {
$('#comment_editor').summernote({
height: 200,
toolbar: [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['fontname', ['fontname']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture']],
['view', ['fullscreen', 'codeview', 'help']]
],
placeholder: '{{ get_phrase("Explain what needs to be fixed...") }}'
});
commentEditor = $('#comment_editor');
}
}
// Fungsi untuk validasi form sebelum submit
$('#gradingForm').on('submit', function(e) {
const status = $('input[name="status"]:checked').val();
if (status == '2') {
// Jika status "Revision Needed", pastikan ada konten di editor
let commentContent = '';
if (commentEditor && commentEditor.summernote) {
commentContent = commentEditor.summernote('code').trim();
} else {
commentContent = $('#comment_editor').val().trim();
}
// Hapus tag HTML untuk cek konten sebenarnya
const plainText = commentContent.replace(/<[^>]*>/g, '').trim();
if (plainText === '') {
e.preventDefault();
alert('{{ get_phrase("Please add revision notes before submitting.") }}');
if (commentEditor && commentEditor.summernote) {
commentEditor.summernote('focus');
}
return false;
}
}
});
// Inisialisasi editor otomatis jika sudah ada komentar sebelumnya dan status adalah "Revision Needed"
@if($submission && $submission->status == 2 && !empty($submission->comment))
$(document).ready(function() {
// Tunggu sebentar agar DOM siap
setTimeout(() => {
initSummernote();
}, 100);
});
@endif
// Inisialisasi editor jika comment_box_area langsung terlihat
$(document).ready(function() {
@if($submission && $submission->status == 2)
// Tunggu sedikit agar DOM selesai render
setTimeout(() => {
initSummernote();
}, 200);
@endif
});
</script>

View File

@ -32,7 +32,7 @@
<div class="row"> <div class="row">
<div class="col-4"> <div class="col-4">
<input class="form-control ol-form-control" type="number" min="0" max="23" name="hour" <input class="form-control ol-form-control" type="number" min="0" max="23" name="hour"
placeholder="00 hour"> placeholder="00 hour" required>
</div> </div>
<div class="col-4"> <div class="col-4">
<input class="form-control ol-form-control" type="number" min="0" max="59" name="minute" <input class="form-control ol-form-control" type="number" min="0" max="59" name="minute"

View File

@ -27,7 +27,7 @@ use App\Http\Controllers\NewsletterController;
use App\Http\Controllers\ReportController; use App\Http\Controllers\ReportController;
use App\Http\Controllers\SeoController; use App\Http\Controllers\SeoController;
use App\Http\Controllers\SettingController; use App\Http\Controllers\SettingController;
use App\Http\Controllers\Updater; // use App\Http\Controllers\Updater;
use App\Http\Controllers\UsersController; use App\Http\Controllers\UsersController;
use App\Http\Controllers\Admin\TutorBookingController; use App\Http\Controllers\Admin\TutorBookingController;
use App\Http\Controllers\Admin\KnowledgeBaseController; use App\Http\Controllers\Admin\KnowledgeBaseController;
@ -77,12 +77,12 @@ Route::name('admin.')->prefix('admin')->middleware('admin')->group(function () {
//invoice //invoice
Route::get('invoice/{id?}', [InvoiceController::class, 'invoice'])->name('admin.invoice'); Route::get('invoice/{id?}', [InvoiceController::class, 'invoice'])->name('admin.invoice');
Route::controller(Updater::class)->middleware('auth', 'verified')->group(function () { // Route::controller(Updater::class)->middleware('auth', 'verified')->group(function () {
Route::post('admin/addon/create', 'update')->name('addon.create'); // Route::post('admin/addon/create', 'update')->name('addon.create');
Route::post('admin/addon/update', 'update')->name('addon.update'); // Route::post('admin/addon/update', 'update')->name('addon.update');
Route::post('admin/product/update', 'update')->name('product.update'); // Route::post('admin/product/update', 'update')->name('product.update');
}); // });
//curriculum //curriculum
Route::controller(CurriculumController::class)->group(function () { Route::controller(CurriculumController::class)->group(function () {

View File

@ -14,6 +14,7 @@ use App\Http\Controllers\instructor\PayoutController;
use App\Http\Controllers\instructor\PayoutSettingsController; use App\Http\Controllers\instructor\PayoutSettingsController;
use App\Http\Controllers\instructor\QuestionController; use App\Http\Controllers\instructor\QuestionController;
use App\Http\Controllers\instructor\QuizController; use App\Http\Controllers\instructor\QuizController;
use App\Http\Controllers\instructor\ProjectController;
use App\Http\Controllers\instructor\SalesReportController; use App\Http\Controllers\instructor\SalesReportController;
use App\Http\Controllers\instructor\SectionController; use App\Http\Controllers\instructor\SectionController;
use App\Http\Controllers\instructor\TeamTrainingController; use App\Http\Controllers\instructor\TeamTrainingController;
@ -76,6 +77,16 @@ Route::name('instructor.')->prefix('instructor')->middleware(['instructor'])->gr
Route::get('quiz/result/preview', 'result_preview')->name('quiz.result.preview'); Route::get('quiz/result/preview', 'result_preview')->name('quiz.result.preview');
}); });
// course project
Route::controller(ProjectController::class)->group(function () {
Route::post('course/project/store', 'store')->name('course.project.store');
Route::post('course/project/update/{id}', 'update')->name('course.project.update');
Route::get('/project/grading/preview', 'getPreview')->name('project.grading.preview');
Route::get('/project/grading/{id}', 'getIndex')->name('project.grading.index');
Route::get('/project/participant/submission', 'getParticipantSubmission')->name('project.participant.submission');
Route::post('/project/grading/update/{id}', 'updateSubmission')->name('project.grading.update');
});
// question route // question route
Route::controller(QuestionController::class)->group(function () { Route::controller(QuestionController::class)->group(function () {
Route::post('course/question/store', 'store')->name('course.question.store'); Route::post('course/question/store', 'store')->name('course.question.store');

View File

@ -14,6 +14,7 @@ use App\Http\Controllers\student\MyCoursesController;
use App\Http\Controllers\student\MyProfileController; use App\Http\Controllers\student\MyProfileController;
use App\Http\Controllers\student\MyTeamPackageController; use App\Http\Controllers\student\MyTeamPackageController;
use App\Http\Controllers\student\OfflinePaymentController; use App\Http\Controllers\student\OfflinePaymentController;
use App\Http\Controllers\student\ProjectController;
use App\Http\Controllers\student\VAPaymentController; use App\Http\Controllers\student\VAPaymentController;
use App\Http\Controllers\student\PurchaseController; use App\Http\Controllers\student\PurchaseController;
use App\Http\Controllers\student\QuizController; use App\Http\Controllers\student\QuizController;
@ -49,6 +50,12 @@ Route::middleware(['auth'])->group(function () {
Route::get('load/quiz/questions/', 'load_questions')->name('load.quiz.questions'); Route::get('load/quiz/questions/', 'load_questions')->name('load.quiz.questions');
}); });
// project routes
Route::controller(ProjectController::class)->group(function () {
Route::post('project/submit', 'submit')->name('student.project.submit');
});
// purchase routes // purchase routes
Route::controller(PurchaseController::class)->group(function () { Route::controller(PurchaseController::class)->group(function () {
Route::get('purchase/course/{course_id}', 'purchase_course')->name('purchase.course'); Route::get('purchase/course/{course_id}', 'purchase_course')->name('purchase.course');