ubahan sertifikat dan admin projek

This commit is contained in:
Baghiz Zuhdi Adzin 2026-02-03 09:00:31 +07:00
parent e0aefa4eb6
commit 8ff23608d5
18 changed files with 1918 additions and 226 deletions

View File

@ -0,0 +1,302 @@
<?php
namespace App\Http\Controllers\Admin;
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)
{
$validator = Validator::make($request->all(), [
'title' => 'required',
'section' => 'required|numeric',
'course_id' => 'required'
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
$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;
Lesson::where('id', $id)->update($data);
Session::flash('success', get_phrase('Project has been updated.'));
return redirect()->back();
}
public function store(Request $request)
{
$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['attachment'] = $request->attachment;
$data['description'] = $request->description;
$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 getSubmissions($id)
{
$participants = ProjectSubmission::join('users', 'project_submissions.user_id', '=', 'users.id')
->where('project_submissions.lesson_id', $id)
->where('project_submissions.status', 0)
->select('users.name', 'users.id')
->distinct()
->get();
return view('admin.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('admin.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,10 @@ class QuizController extends Controller
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;

View File

@ -657,11 +657,41 @@ class SettingController extends Controller
return redirect(route('admin.certificate.settings'))->with('success', get_phrase('Certificate template has been updated'));
}
function certificate_update_template_details(Request $request)
{
$request->validate(['certificate_template_details' => 'required|image']);
$row = Setting::where('type', 'certificate_template_details');
if ($row->count() > 0) {
remove_file(get_settings('certificate_template_details'));
$path = FileUploader::upload($request->certificate_template_details, 'uploads/certificate-template-details', 1000);
Setting::where('type', 'certificate_template_details')->update(['description' => $path]);
} else {
$path = FileUploader::upload($request->certificate_template_details, 'uploads/certificate-template-details', 1000);
Setting::insert(['type' => 'certificate_template_details', 'description' => $path]);
}
$certificate_builder_content = get_settings('certificate_builder_content');
if ($certificate_builder_content) {
// Use regular expression to replace the image source
$modifiedHtml = preg_replace('/(<img[^>]+src=")([^"]+)(")/', '$1' . get_image($path) . '$3', $certificate_builder_content);
Setting::where('type', 'certificate_builder_content')->update(['description' => $modifiedHtml]);
}
return redirect(route('admin.certificate.settings'))->with('success', get_phrase('Certificate template has been updated'));
}
function certificate_builder()
{
return view('admin.certificate.builder');
}
function certificate_builder_details()
{
return view('admin.certificate.builder_details');
}
function certificate_builder_update(Request $request)
{
$request->validate(['certificate_builder_content' => 'required']);
@ -677,6 +707,20 @@ class SettingController extends Controller
return route('admin.certificate.settings');
}
function certificate_builder_details_update(Request $request)
{
$request->validate(['certificate_builder_content_details' => 'required']);
$row = Setting::where('type', 'certificate_builder_content_details');
if ($row->count() > 0) {
Setting::where('type', 'certificate_builder_content_details')->update(['description' => $request->certificate_builder_content_details]);
} else {
Setting::insert(['type' => 'certificate_builder_content_details', 'description' => $request->certificate_builder_content_details]);
}
Session::flash('success', get_phrase('Certificate builder template has been updated'));
return route('admin.certificate.settings');
}
//User Review Add
public function user_review_add()
{

View File

@ -14,39 +14,32 @@ class ProjectController extends Controller
{
public function update(Request $request, $id)
{
$request->validate([
'status' => 'required|in:1,2',
'comment' => 'required_if:status,2',
$validator = Validator::make($request->all(), [
'title' => 'required',
'section' => 'required|numeric',
'course_id' => 'required'
]);
$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;
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
$submission->save();
$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;
return redirect()->back()->with('success', get_phrase('Project graded successfully'));
Lesson::where('id', $id)->update($data);
Session::flash('success', get_phrase('Project has been updated.'));
return redirect()->back();
}
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');
@ -56,8 +49,8 @@ class ProjectController extends Controller
$data['title'] = $request->title;
$data['course_id'] = $request->course_id;
$data['section_id'] = $request->section;
$data['description'] = $request->description;
$data['attachment'] = $request->attachment;
$data['description'] = $request->description;
$data['summary'] = $request->summary;
$data['lesson_type'] = 'project';
$data['status'] = 1;
@ -68,10 +61,11 @@ class ProjectController extends Controller
return redirect()->back();
}
public function getIndex($id)
public function getSubmissions($id)
{
$participants = ProjectSubmission::join('users', 'project_submissions.user_id', '=', 'users.id')
->where('project_submissions.lesson_id', $id)
->where('project_submissions.status', 0)
->select('users.name', 'users.id')
->distinct()
->get();

View File

@ -0,0 +1,849 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ get_phrase('Certificate') }}</title>
<link rel="stylesheet" type="text/css" href="{{ asset('assets/backend/vendors/bootstrap/bootstrap.min.css') }}" />
<link rel="stylesheet" href="{{ asset('assets/global/jquery-ui-themes-1.13.2/themes/base/jquery-ui.css') }}">
{{-- FlatIcons --}}
<link rel="stylesheet" type="text/css" href="{{ asset('assets/global/icons/uicons-solid-rounded/css/uicons-solid-rounded.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/global/icons/uicons-bold-rounded/css/uicons-bold-rounded.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/global/icons/uicons-bold-straight/css/uicons-bold-straight.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/global/icons/uicons-regular-rounded/css/uicons-regular-rounded.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/global/icons/uicons-thin-rounded/css/uicons-thin-rounded.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/backend/css/style.css') }}">
{{-- Font Awesome untuk ikon tambahan --}}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script type="text/javascript" src="{{ asset('assets/backend/js/jquery-3.7.1.min.js') }}"></script>
<script type="text/javascript" src="{{ asset('assets/global/jquery-ui-1.13.2/jquery-ui.min.js') }}"></script>
<script src="{{ asset('assets/backend/vendors/bootstrap/bootstrap.bundle.min.js') }}"></script>
<style type="text/css">
body {
font-family: 'Inter', 'Segoe UI', Roboto, sans-serif;
background-color: #f8f9fa;
}
.draggable {
border: 2px dashed rgba(255, 255, 255, 0.8);
cursor: move;
background-color: #15b57e33;
top: 0;
transition: all 0.3s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.draggable:hover {
border-color: #15b57e;
box-shadow: 0 6px 12px rgba(21, 181, 126, 0.2);
}
.hidden-position:not(.certificate-layout-module) {
background-color: #ffd3d3 !important;
}
.resizeable-canvas {
width: 400px;
padding: 10px;
box-shadow: 1px 3px 11px -4px #565656;
border-radius: 5px;
}
.certificate-layout-module.resizeable-canvas {
padding: 0px !important;
}
.certificate-layout-module {
background-color: #fff;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
}
.sidebar {
position: fixed;
top: 0;
right: -350px;
bottom: 0;
z-index: 200;
background-color: #ffffff;
width: 350px;
height: 100%;
transition: right 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
box-shadow: -5px 0 25px rgba(0, 0, 0, 0.1);
}
.sidebar.open {
right: 0;
}
.sidebar-header {
width: 100%;
padding: 15px;
background: rgba(0, 17, 81, 1);
color: white;
}
.sidebar-toggle {
position: fixed;
top: 20px;
right: 20px;
z-index: 150;
background: rgba(0, 17, 81, 1);
padding: 10px;
border-radius: 50%;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: transform 0.3s ease;
}
.sidebar-toggle:hover {
transform: scale(1.1);
}
.remove-item {
position: absolute;
top: -10px;
right: -10px;
background-color: #dc3545;
color: white;
border-radius: 50%;
padding: 4px;
height: 24px;
width: 24px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
z-index: 10;
}
.remove-item:hover {
background-color: #c82333;
transform: scale(1.1);
}
i:not(.fas, .fa, .fab) {
line-height: 1.5em !important;
vertical-align: -0.14em !important;
display: inline-flex !important;
}
.dotted-background {
background-image: radial-gradient(circle, #afafaf 1px, transparent 1px);
background-size: 20px 20px;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100;
background-color: #f0f2f5;
width: 100%;
height: 100%;
padding: 30px;
}
.cursor-pointer {
cursor: pointer;
}
.sidebar-body {
height: calc(100% - 60px);
overflow-y: auto;
padding: 20px;
}
.font-family-option {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 6px;
margin-bottom: 8px;
border: 1px solid #e0e0e0;
transition: all 0.2s ease;
cursor: pointer;
}
.font-family-option:hover {
background-color: #f8f9fa;
border-color: #667eea;
}
.font-family-option.active {
background-color: #e8eeff;
border-color: #667eea;
}
.font-family-preview {
font-size: 14px;
margin-left: 8px;
}
.badge {
margin: 2px;
padding: 6px 10px;
font-size: 12px;
font-weight: 500;
border-radius: 4px;
transition: all 0.2s ease;
cursor: pointer;
}
.badge:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.form-control, .form-select {
border-radius: 8px;
border: 1px solid #dee2e6;
padding: 10px 15px;
transition: all 0.3s ease;
}
.form-control:focus, .form-select:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-ol-btn-light-primary {
background-color: #e8eeff;
color: #667eea;
border: 1px solid #c2d0ff;
}
.btn-ol-btn-light-primary:hover {
background-color: #d5deff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.btn-ol-btn-primary {
background: rgba(0, 17, 81, 1);
color: white;
border: none;
}
.btn-ol-btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
}
.font-style-options {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.font-style-btn {
flex: 1;
padding: 8px;
border: 1px solid #dee2e6;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.font-style-btn:hover {
border-color: #667eea;
background-color: #f8f9fa;
}
.font-style-btn.active {
background-color: #667eea;
color: white;
border-color: #667eea;
}
.color-picker-container {
position: relative;
}
.color-preview {
width: 30px;
height: 30px;
border-radius: 4px;
border: 1px solid #dee2e6;
cursor: pointer;
}
.font-weight-slider {
width: 100%;
margin: 10px 0;
}
.text-decoration-options {
display: flex;
gap: 10px;
}
.text-decoration-btn {
flex: 1;
padding: 8px;
border: 1px solid #dee2e6;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.text-decoration-btn:hover {
border-color: #667eea;
background-color: #f8f9fa;
}
.text-decoration-btn.active {
background-color: #667eea;
color: white;
border-color: #667eea;
}
.font-option-group {
background: #f8f9fa;
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
}
.font-option-title {
font-size: 14px;
font-weight: 600;
color: #495057;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
</style>
</head>
<body>
<a onclick="$('.sidebar').addClass('open')" href="#" class="sidebar-toggle">
<i class="fas fa-sliders-h" style="color: white; font-size: 20px;"></i>
</a>
<div class="sidebar open">
<div class="sidebar-header border-bottom d-flex align-items-center">
<a class="btn text-white" href="#" onclick="$('.sidebar').removeClass('open')">
<i class="fas fa-chevron-right"></i>
</a>
<span class="ms-2 fw-bold">{{ get_phrase('Certificate Designer') }}</span>
<a class="ms-auto btn btn-sm btn-outline-light" href="{{ route('admin.certificate.settings') }}">
<i class="fas fa-arrow-left me-1"></i>{{ get_phrase('Back') }}
</a>
</div>
<div class="sidebar-body">
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h6 class="card-title d-flex align-items-center">
<i class="fas fa-code me-2"></i>{{ get_phrase('Available Variables') }}
</h6>
<div class="d-flex flex-wrap">
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{course_duration}')">{course_duration}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{certificate_id}')">{certificate_id}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{instructor_name}')">{instructor_name}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{student_name}')">{student_name}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{course_title}')">{course_title}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{number_of_lesson}')">{number_of_lesson}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{qr_code}')">{qr_code}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{course_completion_date}')">{course_completion_date}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{certificate_download_date}')">{certificate_download_date}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{course_level}')">{course_level}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{course_language}')">{course_language}</span>
<span class="badge bg-primary cursor-pointer" onclick="addVariableToText('{section_list}')">{section_list}</span>
</div>
</div>
</div>
<div class="card border-0 shadow-sm" id="custom_elem_form">
<div class="card-body">
<h6 class="card-title d-flex align-items-center">
<i class="fas fa-plus-circle me-2"></i>{{ get_phrase('Add New Element') }}
</h6>
<form action="#">
<div class="font-option-group">
<div class="font-option-title">
<i class="fas fa-font"></i>{{ get_phrase('Text Content') }}
</div>
<div class="mb-3">
<label for="certificate_element_content" class="form-label">{{ get_phrase('Enter Text with variables') }}</label>
<textarea name="certificate_element_content"
placeholder="{{ get_phrase('Example: This certifies that {student_name} has completed {course_title}') }}"
id="certificate_element_content"
rows="3"
class="form-control"></textarea>
</div>
</div>
<div class="font-option-group">
<div class="font-option-title">
<i class="fas fa-palette"></i>{{ get_phrase('Font Styling') }}
</div>
<div class="mb-3">
<label for="font_family" class="form-label">{{ get_phrase('Font Family') }}</label>
<div id="font-family-options">
<!-- Font options will be populated by JavaScript -->
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ get_phrase('Font Weight') }}</label>
<input type="range"
class="form-range font-weight-slider"
id="font_weight"
min="100"
max="900"
step="100"
value="400">
<div class="d-flex justify-content-between">
<small>Thin</small>
<small>Normal</small>
<small>Bold</small>
<small>Black</small>
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ get_phrase('Font Style') }}</label>
<div class="font-style-options">
<button type="button" class="font-style-btn" data-style="normal">
<i class="fas fa-font"></i> Normal
</button>
<button type="button" class="font-style-btn" data-style="italic">
<i class="fas fa-italic"></i> Italic
</button>
<button type="button" class="font-style-btn" data-style="oblique">
<i class="fas fa-slash"></i> Oblique
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ get_phrase('Text Decoration') }}</label>
<div class="text-decoration-options">
<button type="button" class="text-decoration-btn" data-decoration="none">
None
</button>
<button type="button" class="text-decoration-btn" data-decoration="underline">
<u>Underline</u>
</button>
<button type="button" class="text-decoration-btn" data-decoration="overline">
Overline
</button>
<button type="button" class="text-decoration-btn" data-decoration="line-through">
<s>Strike</s>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">{{ get_phrase('Text Color') }}</label>
<div class="color-picker-container">
<input type="color"
id="text_color"
value="#000000"
style="position: absolute; opacity: 0; width: 30px; height: 30px; cursor: pointer;">
<div class="color-preview" id="color_preview" style="background-color: #000000;"></div>
</div>
</div>
<div class="mb-3">
<label for="font_size" class="form-label">{{ get_phrase('Font Size') }}: <span id="font_size_value">16</span>px</label>
<input type="range"
class="form-range"
id="font_size"
min="8"
max="72"
step="1"
value="16">
</div>
</div>
<div class="font-option-group">
<div class="font-option-title">
<i class="fas fa-text-height"></i>{{ get_phrase('Text Alignment') }}
</div>
<div class="mb-3">
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary flex-fill" onclick="setTextAlignment('left')">
<i class="fas fa-align-left"></i>
</button>
<button type="button" class="btn btn-outline-secondary flex-fill" onclick="setTextAlignment('center')">
<i class="fas fa-align-center"></i>
</button>
<button type="button" class="btn btn-outline-secondary flex-fill" onclick="setTextAlignment('right')">
<i class="fas fa-align-right"></i>
</button>
<button type="button" class="btn btn-outline-secondary flex-fill" onclick="setTextAlignment('justify')">
<i class="fas fa-align-justify"></i>
</button>
</div>
</div>
</div>
<div class="mb-4">
<button type="button" class="btn ol-btn-light-primary w-100" onclick="addElemToCertificate()">
<i class="fas fa-plus me-2"></i>{{ get_phrase('Add Element') }}
</button>
</div>
<div class="mb-4">
<button type="button" class="btn ol-btn-primary w-100" onclick="saveTemplate()">
<i class="fas fa-save me-2"></i>{{ get_phrase('Save Template') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="certificate_builder_content_details" class="builder dotted-background">
{{-- Common style for page builder start --}}
<style>
/* Import Google Fonts for certificate styling */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Pinyon+Script&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Italianno&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Miss+Fajardose&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@100;300;400;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@300;400;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Source+Serif+Pro:wght@200;300;400;600;700;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Tangerine:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Alex+Brush&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Sacramento&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Parisienne&display=swap');
</style>
{{-- Common style for page builder END --}}
@if (get_settings('certificate_builder_content_details'))
@php
$htmlContent = get_settings('certificate_builder_content_details');
$newSrc = get_image(get_settings('certificate_template_details'));
$certificate_builder_content_details = preg_replace('/(<img[^>]*class=["\']certificate-template["\'][^>]*src=["\'])([^"\']*)(["\'])/i', '${1}' . $newSrc . '${3}', $htmlContent);
@endphp
{!! $certificate_builder_content_details !!}
@else
<div id="certificate-layout-module" class="certificate-layout-module resizeable-canvas draggable position-relative">
<img class="certificate-template w-100 h-100" src="{{ get_image(get_settings('certificate_template_details')) }}">
</div>
@endif
</div>
<script>
"use strict";
// Available fonts with categories
const fontOptions = [
{ name: 'Auto', value: 'auto', category: 'default' },
{ name: 'Inter', value: 'Inter, sans-serif', category: 'modern' },
{ name: 'Pinyon Script', value: 'Pinyon Script, cursive', category: 'elegant' },
{ name: 'Italianno', value: 'Italianno, cursive', category: 'script' },
{ name: 'Miss Fajardose', value: 'Miss Fajardose, cursive', category: 'script' },
{ name: 'Great Vibes', value: 'Great Vibes, cursive', category: 'script' },
{ name: 'Dancing Script', value: 'Dancing Script, cursive', category: 'script' },
{ name: 'Playfair Display', value: 'Playfair Display, serif', category: 'serif' },
{ name: 'Montserrat', value: 'Montserrat, sans-serif', category: 'modern' },
{ name: 'Roboto', value: 'Roboto, sans-serif', category: 'modern' },
{ name: 'Lato', value: 'Lato, sans-serif', category: 'modern' },
{ name: 'Open Sans', value: 'Open Sans, sans-serif', category: 'modern' },
{ name: 'Merriweather', value: 'Merriweather, serif', category: 'serif' },
{ name: 'Cinzel', value: 'Cinzel, serif', category: 'decorative' },
{ name: 'Raleway', value: 'Raleway, sans-serif', category: 'modern' },
{ name: 'Source Serif Pro', value: 'Source Serif Pro, serif', category: 'serif' },
{ name: 'Cormorant Garamond', value: 'Cormorant Garamond, serif', category: 'serif' },
{ name: 'Tangerine', value: 'Tangerine, cursive', category: 'script' },
{ name: 'Alex Brush', value: 'Alex Brush, cursive', category: 'script' },
{ name: 'Sacramento', value: 'Sacramento, cursive', category: 'script' },
{ name: 'Parisienne', value: 'Parisienne, cursive', category: 'script' }
];
// Current styling options
let currentFontStyle = 'normal';
let currentTextDecoration = 'none';
let currentTextAlign = 'left';
let currentTextColor = '#000000';
$(document).ready(function() {
initialize();
populateFontOptions();
setupEventListeners();
});
function populateFontOptions() {
const container = $('#font-family-options');
container.empty();
// Group fonts by category
const fontsByCategory = {};
fontOptions.forEach(font => {
if (!fontsByCategory[font.category]) {
fontsByCategory[font.category] = [];
}
fontsByCategory[font.category].push(font);
});
// Create options for each category
Object.keys(fontsByCategory).forEach(category => {
const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1);
const categoryDiv = $(`<div class="mb-3"><small class="text-muted">${categoryTitle} Fonts</small></div>`);
fontsByCategory[category].forEach(font => {
const isActive = font.value === 'auto' ? 'active' : '';
const option = $(`
<div class="font-family-option ${isActive}" data-value="${font.value}">
<input type="radio" name="font_family" value="${font.value}"
id="font_family_${font.value.replace(/\s+/g, '_')}"
${font.value === 'auto' ? 'checked' : ''}>
<label for="font_family_${font.value.replace(/\s+/g, '_')}" class="ms-2 flex-grow-1">${font.name}</label>
<span class="font-family-preview" style="font-family: ${font.value}">Aa</span>
</div>
`);
categoryDiv.append(option);
});
container.append(categoryDiv);
});
// Add click event to font options
$('.font-family-option').click(function() {
$('.font-family-option').removeClass('active');
$(this).addClass('active');
$(this).find('input[type="radio"]').prop('checked', true);
});
}
function setupEventListeners() {
// Font size display
$('#font_size').on('input', function() {
$('#font_size_value').text($(this).val());
});
// Font style buttons
$('.font-style-btn').click(function() {
$('.font-style-btn').removeClass('active');
$(this).addClass('active');
currentFontStyle = $(this).data('style');
});
// Text decoration buttons
$('.text-decoration-btn').click(function() {
$('.text-decoration-btn').removeClass('active');
$(this).addClass('active');
currentTextDecoration = $(this).data('decoration');
});
// Color picker
$('#text_color').on('input', function() {
$('#color_preview').css('background-color', $(this).val());
currentTextColor = $(this).val();
});
// Initialize first button as active
$('.font-style-btn[data-style="normal"]').addClass('active');
$('.text-decoration-btn[data-decoration="none"]').addClass('active');
}
function addVariableToText(variable) {
const textarea = $('#certificate_element_content');
const currentValue = textarea.val();
const cursorPos = textarea[0].selectionStart;
const newValue = currentValue.substring(0, cursorPos) +
variable +
currentValue.substring(cursorPos);
textarea.val(newValue);
textarea.focus();
}
function setTextAlignment(alignment) {
currentTextAlign = alignment;
$('.btn-outline-secondary').removeClass('active');
$(`button[onclick="setTextAlignment('${alignment}')"]`).addClass('active');
}
function saveTemplate() {
var certificate_builder_content_details = $('#certificate_builder_content_details').html();
$.ajax({
type: 'POST',
url: "{{ route('admin.certificate.builder.details.update') }}",
data: {
certificate_builder_content_details: certificate_builder_content_details
},
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
success: function(response) {
$(location).attr('href', response);
console.log(response)
},
error: function(xhr) {
console.error('Error saving template:', xhr);
alert('Error saving template. Please try again.');
}
});
}
function addElemToCertificate() {
var font_family = $("input[type='radio'][name='font_family']:checked").val();
var font_size = $("#font_size").val();
var font_weight = $("#font_weight").val();
var certificate_element_content = $('#certificate_element_content').val();
// Generate unique ID for the element
var elementId = 'elem_' + Date.now() + Math.floor(Math.random() * 1000);
var certificateElem = `
<div id="${elementId}"
class="draggable resizeable-canvas"
style="
padding: 10px !important;
position: absolute;
font-size: ${font_size}px;
font-weight: ${font_weight};
font-style: ${currentFontStyle};
text-decoration: ${currentTextDecoration};
text-align: ${currentTextAlign};
color: ${currentTextColor};
top: 50px;
left: 50px;
min-width: 100px;
min-height: 40px;
font-family: ${font_family};
border-radius: 8px;
z-index: 5;
">
${certificate_element_content}
<div class="remove-item" onclick="$(this).parent().remove(); positionTracking(this.parentNode)">
<i class="fas fa-times"></i>
</div>
</div>`;
if (certificate_element_content.trim() !== '') {
$('#certificate-layout-module').append(certificateElem);
// Reset form
$('#certificate_element_content').val('');
$("#font_size").val(16);
$("#font_size_value").text('16');
$("#font_weight").val(400);
currentTextColor = '#000000';
$('#color_preview').css('background-color', '#000000');
$('#text_color').val('#000000');
// Reset to default font
$('.font-family-option').removeClass('active');
$('.font-family-option[data-value="auto"]').addClass('active').find('input').prop('checked', true);
initialize();
} else {
alert('Please enter some text content.');
}
}
function initialize() {
$(".draggable").draggable({
containment: "#certificate-layout-module",
cursor: "move",
scroll: false
});
$(".resizeable-canvas").resizable({
resize: function(event, ui) {
$(".draggable").draggable("disable");
positionTracking(this);
},
stop: function(event, ui) {
$(".draggable").draggable("enable");
positionTracking(this);
},
handles: "all",
minHeight: 30,
minWidth: 50
});
$(".draggable").on("dragstop", function(e, pos) {
positionTracking(this);
});
}
function positionTracking(e) {
var layoutCanvasOffset = $('#certificate-layout-module').offset();
var layoutCanvasRight = $('#certificate-layout-module').width() + layoutCanvasOffset.left;
var layoutCanvasBottom = $('#certificate-layout-module').height() + layoutCanvasOffset.top;
if ($(e).attr('id') != 'certificate-layout-module') {
var itemOffset = $(e).offset();
var itemRight = $(e).width() + itemOffset.left;
var itemBottom = $(e).height() + itemOffset.top;
if (
layoutCanvasOffset.left < itemOffset.left &&
layoutCanvasOffset.top < itemOffset.top &&
layoutCanvasRight > itemRight &&
layoutCanvasBottom > itemBottom
) {
$(e).removeClass('hidden-position');
$(e).css('border-color', 'rgba(21, 181, 126, 0.5)');
} else {
$(e).addClass('hidden-position');
$(e).css('border-color', '#ff6b6b');
}
} else {
var draggableItems = document.getElementsByClassName("draggable");
for (var i = 0; i < draggableItems.length; i++) {
var item = $(draggableItems.item(i));
if (item.attr('id') == 'certificate-layout-module') continue;
var itemOffset = item.offset();
var itemRight = item.width() + itemOffset.left;
var itemBottom = item.height() + itemOffset.top;
if (
layoutCanvasOffset.left < itemOffset.left &&
layoutCanvasOffset.top < itemOffset.top &&
layoutCanvasRight > itemRight &&
layoutCanvasBottom > itemBottom
) {
item.removeClass('hidden-position');
item.css('border-color', 'rgba(21, 181, 126, 0.5)');
} else {
item.addClass('hidden-position');
item.css('border-color', '#ff6b6b');
}
}
}
}
</script>
</body>
</html>

View File

@ -32,7 +32,7 @@
<div class="row">
<div class="col-md-6">
<div class="ol-card p-4">
<p class="title text-14px mb-3">{{ get_phrase('Certificate template') }}</p>
<p class="title text-14px mb-3">{{ get_phrase('Certificate Template Main') }}</p>
<div class="ol-card-body certificate_builder_view" id="certificate_builder_view">
@php
$htmlContent = get_settings('certificate_builder_content');
@ -47,7 +47,7 @@
</div>
<div class="col-md-6">
<div class="ol-card p-4">
<p class="title text-14px mb-3">{{ get_phrase('Certificate template') }}</p>
<p class="title text-14px mb-3">{{ get_phrase('Certificate Template Main') }}</p>
<div class="ol-card-body">
<form action="{{ route('admin.certificate.update.template') }}" method="post" enctype="multipart/form-data">
@csrf
@ -55,7 +55,7 @@
<img class="my-2" height="200px" src="{{ get_image(get_settings('certificate_template')) }}" alt="">
</div>
<div class="form-group mb-3">
<label class="form-label ol-form-label" for="certificate_template">{{ get_phrase('Upload your certificate template') }}</label>
<label class="form-label ol-form-label" for="certificate_template">{{ get_phrase('Upload your certificate template main') }}</label>
<input type="file" class="form-control" name="certificate_template" id="certificate_template"rows="4">
</div>
<div class="form-group">
@ -66,6 +66,44 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="ol-card p-4">
<p class="title text-14px mb-3">{{ get_phrase('Certificate Template Details') }}</p>
<div class="ol-card-body certificate_builder_view" id="certificate_builder_view">
@php
$htmlContent = get_settings('certificate_builder_content_details');
// Use regex to update the src attribute of the <img> tag with the class 'certificate-template'.
$newSrc = get_image(get_settings('certificate_template_details'));
$certificate_builder_content = preg_replace('/(<img[^>]*class=["\']certificate-template["\'][^>]*src=["\'])([^"\']*)(["\'])/i', '${1}' . $newSrc . '${3}', $htmlContent);
@endphp
{!! $certificate_builder_content !!}
<a class="btn ol-btn-primary mt-3" href="{{ route('admin.certificate.builder_details') }}">{{ get_phrase('Build your certificate') }}</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="ol-card p-4">
<p class="title text-14px mb-3">{{ get_phrase('Certificate Template Details') }}</p>
<div class="ol-card-body">
<form action="{{ route('admin.certificate.update.template.details') }}" method="post" enctype="multipart/form-data">
@csrf
<div class="form-group text-start mb-3">
<img class="my-2" height="200px" src="{{ get_image(get_settings('certificate_template_details')) }}" alt="">
</div>
<div class="form-group mb-3">
<label class="form-label ol-form-label" for="certificate_template_details">{{ get_phrase('Upload your certificate template details') }}</label>
<input type="file" class="form-control" name="certificate_template_details" id="certificate_template_details"rows="4">
</div>
<div class="form-group">
<button class="btn ol-btn-primary" type="submit">{{ get_phrase('Upload') }}</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
@push('js')

View File

@ -10,6 +10,8 @@
<a href="#" onclick="ajaxModal('{{ route('modal', ['admin.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', ['admin.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', ['admin.project.create', 'id' => $course_details->id]) }}', '{{ get_phrase('Add new project') }}')" class="btn ol-btn-light ol-btn-sm">{{ get_phrase('Add project') }}</a>
@endif
</div>
@ -57,7 +59,7 @@
<div class="buttons">
@if ($lesson->lesson_type == 'quiz')
<a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Result') }}" onclick="ajaxModal('{{ route('modal', ['admin.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 href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Questions') }}" onclick="ajaxModal('{{ route('modal', ['admin.questions.index', 'id' => $lesson->id]) }}', '{{ get_phrase('Questions') }}', 'modal-lg')" class="edit-delete">
@ -67,9 +69,20 @@
<a href="#" data-bs-toggle="tooltip" title="{{ get_phrase('Edit quiz') }}" onclick="ajaxModal('{{ route('modal', ['admin.quiz.edit', 'id' => $lesson->id]) }}', '{{ get_phrase('Edit quiz') }}')" class="edit-delete">
<span class="fi-rr-pencil"></span>
</a>
@elseif ($lesson->lesson_type == 'project')
<a href="javascript:void(0);"
data-bs-toggle="tooltip"
title="{{ get_phrase('Submissions') }}"
onclick="ajaxModal('{{ route('admin.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', ['admin.project.edit', 'id' => $lesson->id]) }}', '{{ get_phrase('Edit project') }}')" class="edit-delete">
<span class="fi-rr-pencil"></span>
</a>
@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', ['admin.course.lesson_edit', 'id' => $lesson->id]) }}', '{{ get_phrase('Edit lesson') }}')" class="edit-delete">
<span class="fi-rr-pencil"></span>
</a>

View File

@ -0,0 +1,50 @@
<form action="{{ route('admin.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" required>
<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="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"></textarea>
</div>
<div class="fpb7">
<button type="submit" class="btn ol-btn-primary">{{ get_phrase('Add Project') }}</button>
</div>
</form>
@include('admin.init')

View File

@ -0,0 +1,63 @@
@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('admin.course.project.update', $id) }}" method="post">@csrf
{{-- FIX: Pass the course_id back to the controller --}}
<input type="hidden" name="course_id" value="{{ $project->course_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" 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" required>
<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>
{{-- Ensure your Controller handles $request->attachment --}}
<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('admin.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('admin.project.grading.preview') }}");
console.log("- CSRF Token:", $('meta[name="csrf-token"]').attr('content'));
// 3. AJAX Call
$.ajax({
type: "GET",
url: "{{ route('admin.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('admin.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

@ -8,21 +8,10 @@
@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;
list-style-type: disc;
list-style-position: inside ;
margin-left: 10px ;
}
</style>

View File

@ -98,7 +98,7 @@
</span>
</div>
<p class="d-none">{{ $lesson->lesson_type }}</p>
@if (in_array($type, ['google_drive', 'video-url', 'quiz']))
@if (in_array($type, ['google_drive', 'video-url', 'quiz', 'project']))
<a href="{{ route('course.player', ['slug' => $course_details->slug, 'id' => $lesson->id]) }}"
class="video-title">
{{ $lesson->title }}

View File

@ -26,7 +26,7 @@
display: none !important;
}
.certificate-layout-module {
.certificate-layout-module {
left: unset !important;
top: unset !important;
margin-left: auto !important;
@ -38,6 +38,11 @@
height: 100%;
}
.certificate-page {
width: 1123px;
height: 794px;
}
.download-wrapper {
position: fixed;
bottom: 20px;
@ -123,201 +128,220 @@
}
</style>
@php
$bulan_indonesia = [
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April',
5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus',
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember'
];
{{-- Downloadable canvas --}}
$completion_date = $certificate->created_at;
$timestamp = strtotime($completion_date);
$hari = date('j', $timestamp);
$bulan_angka = (int)date('n', $timestamp);
$tahun = date('Y', $timestamp);
$course_completion_date = $hari . ' ' . $bulan_indonesia[$bulan_angka] . ' ' . $tahun;
$course_duration = $certificate->course->total_duration();
$student_name = $certificate->user->name;
$course_title = $certificate->course->title;
$number_of_lesson = $certificate->course->lessons->count();
$qr_code = $qrcode;
$certificate_id = $certificate->identifier;
$certificate_download_date = date('d M Y');
$course_level = ucfirst($certificate->course->level);
$course_language = ucfirst($certificate->course->language);
$instructor_name = '';
foreach ($certificate->course->instructors() as $instructor) {
$instructor_name .= '<p>' . $instructor->name . '</p>';
}
// --- PERBAIKAN DI SINI (Hapus tanda = yang nyasar) ---
$sections = $certificate->course->sections()->orderBy('sort', 'asc')->get();
// --- PERBAIKAN NAMA VARIABEL ---
// Ubah $section_list menjadi $section_list_html agar sesuai dengan pemanggilan di bawah
$section_list_html = '<ol style="list-style-type: decimal; padding-left: 20px;">';
foreach ($sections as $section) {
$section_list_html .= '<li style="margin-bottom: 5px;">' . $section->title . '</li>';
}
$section_list_html .= '</ol>';
$certificate_builder_content = get_settings('certificate_builder_content');
// Replacements Array (Agar lebih rapi dan bisa dipakai ulang)
$replacements = [
'{course_duration}' => $course_duration,
'{instructor_name}' => $instructor_name,
'{student_name}' => $student_name,
'{course_title}' => $course_title,
'{number_of_lesson}' => $number_of_lesson,
'{qr_code}' => $qr_code,
'{course_completion_date}' => $course_completion_date,
'{certificate_id}' => $certificate_id,
'{certificate_download_date}' => $certificate_download_date,
'{course_level}' => $course_level,
'{course_language}' => $course_language,
];
// Proses Halaman 1
foreach ($replacements as $key => $value) {
$certificate_builder_content = str_replace($key, $value, $certificate_builder_content);
}
// Update Image Src Halaman 1
$newSrc = get_image(get_settings('certificate_template'));
$certificate_builder_content = preg_replace('/(<img[^>]*class=["\']certificate-template["\'][^>]*src=["\'])([^"\']*)(["\'])/i', '${1}' . $newSrc . '${3}', $certificate_builder_content);
// --- PEMROSESAN HALAMAN TAMBAHAN (DETAILS) ---
$certificate_builder_content_details = get_settings('certificate_builder_content_details');
// Proses Halaman 2
foreach ($replacements as $key => $value) {
$certificate_builder_content_details = str_replace($key, $value, $certificate_builder_content_details);
}
// Masukkan Section List (Pastikan variabelnya $section_list_html)
$certificate_builder_content_details = str_replace('{section_list}', $section_list_html, $certificate_builder_content_details);
// Update Image Src Halaman 2
$newSrcDetails = get_image(get_settings('certificate_template_details'));
$certificate_builder_content_details = preg_replace('/(<img[^>]*class=["\']certificate-template-details["\'][^>]*src=["\'])([^"\']*)(["\'])/i', '${1}' . $newSrcDetails . '${3}', $certificate_builder_content_details);
@endphp
{{-- Capture Certificate Wrapper --}}
<div class="captureCertificate" id="captureCertificate">
@php
$bulan_indonesia = [
1 => 'Januari',
2 => 'Februari',
3 => 'Maret',
4 => 'April',
5 => 'Mei',
6 => 'Juni',
7 => 'Juli',
8 => 'Agustus',
9 => 'September',
10 => 'Oktober',
11 => 'November',
12 => 'Desember'
];
// Jika date_formatter() mengembalikan tanggal string
$completion_date = $certificate->created_at; // Ambil timestamp asli
// Konversi ke format Indonesia
$timestamp = strtotime($completion_date); // atau $completion_date jika sudah timestamp
$hari = date('j', $timestamp); // tanggal tanpa nol
$bulan_angka = (int)date('n', $timestamp); // bulan angka
$tahun = date('Y', $timestamp);
$course_completion_date = $hari . ' ' . $bulan_indonesia[$bulan_angka] . ' ' . $tahun;
$course_duration = $certificate->course->total_duration();
$student_name = $certificate->user->name;
$course_title = $certificate->course->title;
$number_of_lesson = $certificate->course->lessons->count();
$qr_code = $qrcode;
$certificate_id = $certificate->identifier;
$certificate_download_date = date('d M Y');
$course_level = ucfirst($certificate->course->level);
$course_language = ucfirst($certificate->course->language);
$instructor_name = '';
foreach ($certificate->course->instructors() as $instructor) {
$instructor_name .= '<p>' . $instructor->name . '</p>';
}
$certificate_builder_content = get_settings('certificate_builder_content');
$certificate_builder_content = str_replace('{course_duration}', $course_duration, $certificate_builder_content);
$certificate_builder_content = str_replace('{instructor_name}', $instructor_name, $certificate_builder_content);
$certificate_builder_content = str_replace('{student_name}', $student_name, $certificate_builder_content);
$certificate_builder_content = str_replace('{course_title}', $course_title, $certificate_builder_content);
$certificate_builder_content = str_replace('{number_of_lesson}', $number_of_lesson, $certificate_builder_content);
$certificate_builder_content = str_replace('{qr_code}', $qr_code, $certificate_builder_content);
$certificate_builder_content = str_replace('{course_completion_date}', $course_completion_date, $certificate_builder_content);
$certificate_builder_content = str_replace('{certificate_id}', $certificate_id, $certificate_builder_content);
$certificate_builder_content = str_replace('{certificate_download_date}', $certificate_download_date, $certificate_builder_content);
$certificate_builder_content = str_replace('{course_level}', $course_level, $certificate_builder_content);
$certificate_builder_content = str_replace('{course_language}', $course_language, $certificate_builder_content);
// Use regex to update the src attribute of the <img> tag with the class 'certificate-template'.
$newSrc = get_image(get_settings('certificate_template'));
$certificate_builder_content = preg_replace('/(<img[^>]*class=["\']certificate-template["\'][^>]*src=["\'])([^"\']*)(["\'])/i', '${1}' . $newSrc . '${3}', $certificate_builder_content);
@endphp
{!! $certificate_builder_content !!}
</div>
{{-- Downloadable canvas end--}}
{{-- Preview certificate --}}
<div class="absolute-view">
<div class="certificate_builder_view" id="certificate_builder_view">
{{-- Halaman 1 --}}
<div class="certificate-page page-1">
{!! $certificate_builder_content !!}
</div>
</div>
{{-- Preview certificate end--}}
{{-- Halaman 2 (Baru) --}}
{{-- Style margin-top hanya visual di web, tidak mempengaruhi PDF split --}}
<div class="certificate-page page-2" style="margin-top: 50px;">
{!! $certificate_builder_content_details !!}
</div>
</div>
{{-- Capture Certificate End --}}
{{-- Preview Certificate --}}
<div class="absolute-view">
<div class="certificate_builder_view" id="certificate_builder_view">
{{-- Tampilkan kedua halaman di preview juga --}}
<div class="certificate-page">
{!! $certificate_builder_content !!}
</div>
<div class="certificate-page" style="margin-top: 20px;">
{!! $certificate_builder_content_details !!}
</div>
</div>
</div>
<div class="download-wrapper">
<a class="download-btn" href="#"
<!-- <a class="download-btn secondary" href="#"
onclick="setTimeout(() => renderCanvasToImage(), 500); return false;">
{{ get_phrase('Download Image') }}
</a>
</a> -->
<a class="download-btn secondary" href="#"
<a class="download-btn" href="#"
onclick="renderCanvasToPDF(); return false;">
{{ get_phrase('Download PDF') }}
{{ get_phrase('Download Sertificate') }}
</a>
</div>
<script>
"use strict";
"use strict";
$(function() {
var certificate_builder_view_width = $('.certificate_builder_view').width();
var certificate_layout_module = $('.certificate_builder_view .certificate-layout-module').width();
var zoomScaleValue = ((certificate_builder_view_width/certificate_layout_module)*100) - 8;
$('.certificate_builder_view .certificate-layout-module').css('zoom', zoomScaleValue+'%');
});
$(function() {
// Skrip zoom diperbaiki agar berlaku untuk semua halaman sertifikat
var certificate_builder_view_width = $('.certificate_builder_view').width();
function renderCanvasToImage() {
// Ambil modul layout pertama untuk perhitungan rasio
var certificate_layout_module = $('.certificate_builder_view .certificate-layout-module').first().width();
var certificate_width = $('#captureCertificate > div').width();
html2canvas(document.querySelector("#captureCertificate > div"), {
allowTaint: true,
useCORS: true,
width: certificate_width,
scale: 2
}).then(canvas => {
document.querySelector("#captureCertificate").appendChild(canvas);
$("canvas").hide();
setTimeout(function() {
var canvas = document.querySelector("canvas");
downloadCanvas(canvas, "certificate.png");
}, 2000);
});
}
function downloadCanvas(canvas, filename) {
// Convert canvas to data URL
var dataUrl = canvas.toDataURL("image/png");
// Create a temporary link element
var link = document.createElement("a");
link.href = dataUrl;
link.download = filename;
// Append the link to the body
document.body.appendChild(link);
// Create a new MouseEvent and dispatch it
var event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});
link.dispatchEvent(event);
// Remove the link from the body
document.body.removeChild(link);
}
$(function() {
var certificate_builder_view_width = $('.certificate_builder_view').width();
var certificate_layout_module = $('.certificate_builder_view .certificate-layout-module').width();
var zoomScaleValue = ((certificate_builder_view_width / certificate_layout_module) * 100) - 8;
$('.certificate_builder_view .certificate-layout-module').css('zoom', zoomScaleValue + '%');
});
function renderCanvasToPDF() {
const target = document.querySelector("#captureCertificate > div");
html2canvas(target, {
allowTaint: true,
useCORS: true,
scale: 2
}).then(canvas => {
const imgData = canvas.toDataURL("image/png");
// A4 Landscape
const pdf = new window.jspdf.jsPDF({
orientation: 'landscape',
unit: 'mm',
format: 'a4'
if(certificate_layout_module > 0) {
var zoomScaleValue = ((certificate_builder_view_width / certificate_layout_module) * 100) - 8;
// Terapkan zoom ke SEMUA layout module
$('.certificate_builder_view .certificate-layout-module').css('zoom', zoomScaleValue + '%');
}
});
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
// Fungsi Gambar (Download PNG) - Hanya mengambil halaman pertama atau gabungan (opsional)
// Disarankan default mengambil halaman utama saja untuk PNG, atau biarkan custom
function renderCanvasToImage() {
// Mengambil elemen halaman pertama saja untuk PNG thumbnail
var elementToCapture = document.querySelector("#captureCertificate .page-1");
var certificate_width = $(elementToCapture).width();
// Hitung rasio agar tidak terpotong
const imgWidth = pageWidth;
const imgHeight = canvas.height * imgWidth / canvas.width;
html2canvas(elementToCapture, {
allowTaint: true,
useCORS: true,
width: certificate_width,
scale: 3
}).then(canvas => {
// Logic download existing
var link = document.createElement('a');
link.download = 'certificate.png';
link.href = canvas.toDataURL("image/png");
link.click();
});
}
const positionY = (pageHeight - imgHeight) / 2;
async function renderCanvasToPDF() {
const pages = document.querySelectorAll("#captureCertificate .certificate-page");
if (pages.length === 0) return;
pdf.addImage(
imgData,
'PNG',
0,
positionY,
imgWidth,
imgHeight,
undefined,
'FAST'
);
const pdf = new window.jspdf.jsPDF({
orientation: 'landscape',
unit: 'mm',
format: 'a4'
});
pdf.save("certificate.pdf");
});
}
const pageWidth = pdf.internal.pageSize.getWidth(); // 297 mm
const pageHeight = pdf.internal.pageSize.getHeight(); // 210 mm
</script>
for (let i = 0; i < pages.length; i++) {
const pageElement = pages[i];
const canvas = await html2canvas(pageElement, {
allowTaint: true,
useCORS: true,
scale: 2, // cukup tajam, jangan kebesaran
backgroundColor: '#ffffff'
});
const imgData = canvas.toDataURL("image/png");
if (i > 0) pdf.addPage();
// 🔥 FULL PAGE TANPA WHITESPACE
pdf.addImage(
imgData,
'PNG',
0,
0,
pageWidth,
pageHeight,
undefined,
'FAST'
);
}
const courseTitle = {!! json_encode($course_title) !!};
const fileName = `Grownesesa Certificate - ${courseTitle}.pdf`;
pdf.save(fileName);
}
</script>
</body>
</html>

View File

@ -15,7 +15,7 @@
{{ 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">
<select class="form-control ol-form-control ol-select2" data-toggle="select2" name="section" required>
<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>
@ -37,9 +37,9 @@
</div>
<div class="fpb-7 mb-6">
<label for="attachment"
<label for="summary"
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>
<textarea name="summary" rows="5" class="form-control ol-form-control text_editor"></textarea>
</div>
<div class="fpb7">

View File

@ -7,6 +7,10 @@
@endphp
<form action="{{ route('instructor.course.project.update', $id) }}" method="post">@csrf
{{-- FIX: Pass the course_id back to the controller --}}
<input type="hidden" name="course_id" value="{{ $project->course_id }}">
<div class="fpb7 mb-3">
<label class="form-label ol-form-label" for="title">
{{ get_phrase('Title') }}
@ -22,7 +26,7 @@
{{ 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">
<select class="form-control ol-form-control ol-select2" data-toggle="select2" name="section" required>
<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>
@ -41,6 +45,7 @@
<div class="fpb-7 mb-3">
<label for="attachment"
class="form-label ol-form-label col-form-label">{{ get_phrase('Assessment Criteria') }}</label>
{{-- Ensure your Controller handles $request->attachment --}}
<textarea name="attachment" rows="5" class="form-control ol-form-control text_editor">{!! $project->attachment !!}</textarea>
</div>

View File

@ -10,6 +10,7 @@ use App\Http\Controllers\Admin\MessageController;
use App\Http\Controllers\Admin\OfflinePaymentController;
use App\Http\Controllers\Admin\OpenAiController;
use App\Http\Controllers\Admin\PageBuilderController;
use App\Http\Controllers\Admin\ProjectController;
use App\Http\Controllers\Admin\QuestionController;
use App\Http\Controllers\Admin\QuizController;
use App\Http\Controllers\Admin\TeamTrainingController;
@ -256,8 +257,11 @@ Route::name('admin.')->prefix('admin')->middleware('admin')->group(function () {
// Certificate settings
Route::get('certificate_settings', 'certificate')->name('certificate.settings');
Route::post('certificate/update/template', 'certificate_update_template')->name('certificate.update.template');
Route::post('certificate/update/template/details', 'certificate_update_template_details')->name('certificate.update.template.details');
Route::get('certificate/builder', 'certificate_builder')->name('certificate.builder');
Route::post('certificate/builder/update', 'certificate_builder_update')->name('certificate.certificate.builder.update');
Route::get('certificate/builder_details', 'certificate_builder_details')->name('certificate.builder_details');
Route::post('certificate/builder/update/details', 'certificate_builder_details_update')->name('certificate.builder.details.update');
// Admin User Review Add
Route::get('user/review', 'user_review_add')->name('review.create');
@ -356,6 +360,16 @@ Route::name('admin.')->prefix('admin')->middleware('admin')->group(function () {
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}', 'getSubmissions')->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
Route::controller(QuestionController::class)->group(function () {
Route::post('course/question/store', 'store')->name('course.question.store');

View File

@ -82,7 +82,7 @@ Route::name('instructor.')->prefix('instructor')->middleware(['instructor'])->gr
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/grading/{id}', 'getSubmissions')->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');
});