Compare commits
No commits in common. "e3611f38162b6afd7d12244a5bae3e4a0b642bc2" and "5f069fc337ffd84dd1ed49de9edf08a8582e81df" have entirely different histories.
e3611f3816
...
5f069fc337
|
|
@ -58,7 +58,7 @@ public function signup(Request $request)
|
|||
return redirect('/');
|
||||
}
|
||||
|
||||
public function logout(Request $request): RedirectResponse
|
||||
public function logoutt(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\NodeJS;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class NodeJSController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function dashboardStudent() {
|
||||
$topicCount = DB::table('projects')->count();
|
||||
return view('dashboard_student', compact('topicCountt'));
|
||||
}
|
||||
|
||||
public function dashboardTeacher() {
|
||||
$topicCount = DB::table('projects')->count();
|
||||
return view('dashboard_teacher', compact('topicCount'));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -119,42 +119,54 @@ public function download(Request $request, $project_id)
|
|||
if ($zipMedia) {
|
||||
return response()->json($zipMedia->getUrl(), 200);
|
||||
} else {
|
||||
// Perbaiki collection name sesuai database
|
||||
$tests = $project->getMedia('project_tests');
|
||||
|
||||
// Tambahkan pengecekan apakah ada file tests
|
||||
if (count($tests) === 0) {
|
||||
return response()->json(["message" => "no tests available"], 404);
|
||||
}
|
||||
$tests_api = $project->getMedia('project_tests_api');
|
||||
$tests_web = $project->getMedia('project_tests_web');
|
||||
$tests_images = $project->getMedia('project_tests_images');
|
||||
|
||||
$tempDir = storage_path('app/public/assets/projects/' . $project->title . '/zips');
|
||||
if (!is_dir($tempDir)) mkdir($tempDir);
|
||||
if (!is_dir($tempDir . '/tests')) mkdir($tempDir . '/tests');
|
||||
if (!is_dir($tempDir . '/tests/api')) mkdir($tempDir . '/tests/api');
|
||||
if (!is_dir($tempDir . '/tests/web')) mkdir($tempDir . '/tests/web');
|
||||
if (!is_dir($tempDir . '/tests/web/images')) mkdir($tempDir . '/tests/web/images');
|
||||
|
||||
// Copy semua file tests ke tempDir
|
||||
foreach ($tests as $test) {
|
||||
foreach ($tests_api as $test) {
|
||||
$path = $test->getPath();
|
||||
$filename = $test->file_name;
|
||||
copy($path, $tempDir . '/' . $filename);
|
||||
copy($path, $tempDir . '/tests/api/' . $filename);
|
||||
}
|
||||
foreach ($tests_web as $test) {
|
||||
$path = $test->getPath();
|
||||
$filename = $test->file_name;
|
||||
copy($path, $tempDir . '/tests/web/' . $filename);
|
||||
}
|
||||
foreach ($tests_images as $test) {
|
||||
$path = $test->getPath();
|
||||
$filename = $test->file_name;
|
||||
copy($path, $tempDir . '/tests/web/images/' . $filename);
|
||||
}
|
||||
|
||||
$zipPath = $tempDir . '/tests.zip';
|
||||
$zip = new ZipArchive;
|
||||
if ($zip->open($zipPath, ZipArchive::CREATE) === TRUE) {
|
||||
$files = Storage::files('public/assets/projects/' . $project->title . '/zips');
|
||||
foreach ($files as $file) {
|
||||
// Skip jika file adalah zip itu sendiri
|
||||
if (basename(storage_path('app/' . $file)) !== 'tests.zip') {
|
||||
$zip->addFile(storage_path('app/' . $file), basename($file));
|
||||
$zip->addEmptyDir('api');
|
||||
$zip->addEmptyDir('web');
|
||||
$zip->addEmptyDir('web/images');
|
||||
$api_files = Storage::files('public/assets/projects/' . $project->title . '/zips/tests/api');
|
||||
foreach ($api_files as $file) {
|
||||
$zip->addFile(storage_path('app/' . $file), 'api/' . basename($file));
|
||||
}
|
||||
$api_files = Storage::files('public/assets/projects/' . $project->title . '/zips/tests/web');
|
||||
foreach ($api_files as $file) {
|
||||
$zip->addFile(storage_path('app/' . $file), 'web/' . basename($file));
|
||||
}
|
||||
$image_files = Storage::files('public/assets/projects/' . $project->title . '/zips/tests/web/images');
|
||||
foreach ($image_files as $file) {
|
||||
$zip->addFile(storage_path('app/' . $file), 'web/images/' . basename($file));
|
||||
}
|
||||
$zip->close();
|
||||
|
||||
// Hapus file individual setelah di-zip
|
||||
foreach ($files as $file) {
|
||||
if (basename(storage_path('app/' . $file)) !== 'tests.zip') {
|
||||
unlink(storage_path('app/' . $file));
|
||||
}
|
||||
}
|
||||
$zip->close();
|
||||
Process::fromShellCommandline("rm -rf {$tempDir}/tests")->run();
|
||||
} else {
|
||||
throw new Exception('Failed to create zip archive');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ public function upload(Request $request, $project_id)
|
|||
|
||||
public function submit(Request $request)
|
||||
{
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'project_id' => 'required|exists:projects,id',
|
||||
|
|
@ -160,55 +161,34 @@ public function submit(Request $request)
|
|||
$submission = new Submission();
|
||||
$submission->user_id = $request->user()->id;
|
||||
$submission->project_id = $request->project_id;
|
||||
|
||||
if ($request->has('folder_path')) {
|
||||
$submission->type = Submission::$FILE;
|
||||
$submission->path = $request->folder_path;
|
||||
|
||||
$temporary_file = TemporaryFile::where('folder_path', $request->folder_path)->first();
|
||||
|
||||
// Debug: Cek apakah temporary file ada
|
||||
if (!$temporary_file) {
|
||||
\Log::error('TemporaryFile not found for folder_path: ' . $request->folder_path);
|
||||
return response()->json(['message' => 'Temporary file not found'], 400);
|
||||
}
|
||||
|
||||
$file_path = storage_path('app/' . $request->folder_path . '/' . $temporary_file->file_name);
|
||||
|
||||
// Debug: Cek apakah file fisik ada
|
||||
if (!file_exists($file_path)) {
|
||||
\Log::error('Physical file not found: ' . $file_path);
|
||||
return response()->json(['message' => 'Physical file not found'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$submission->addMedia($file_path)->toMediaCollection('submissions', 'nodejs_public_submissions_files');
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Media collection error: ' . $e->getMessage());
|
||||
return response()->json(['message' => 'Failed to add media: ' . $e->getMessage()], 500);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if ($temporary_file) {
|
||||
$path = storage_path('app/' . $request->folder_path . '/' . $temporary_file->file_name);
|
||||
$submission->addMedia($path)->toMediaCollection('submissions', 'public_submissions_files');
|
||||
if ($this->is_dir_empty(storage_path('app/' . $request->folder_path))) {
|
||||
rmdir(storage_path('app/' . $request->folder_path));
|
||||
}
|
||||
$temporary_file->delete();
|
||||
}
|
||||
} else {
|
||||
$submission->type = Submission::$URL;
|
||||
$submission->path = $request->github_url;
|
||||
}
|
||||
|
||||
$submission->status = Submission::$PENDING;
|
||||
$submission->start = now();
|
||||
$submission->save();
|
||||
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Submission created successfully',
|
||||
'submission' => $submission,
|
||||
], 201);
|
||||
|
||||
} catch (\Throwable $th) {
|
||||
\Log::error('Submission error: ' . $th->getMessage() . ' | Line: ' . $th->getLine() . ' | File: ' . $th->getFile());
|
||||
return response()->json([
|
||||
'message' => 'Submission failed',
|
||||
'error' => $th->getMessage(),
|
||||
|
|
@ -682,18 +662,9 @@ public function restart(Request $request)
|
|||
if ($request->submission_id == null) return response()->json([
|
||||
'message' => 'Submission ID is required',
|
||||
], 404);
|
||||
|
||||
$user = Auth::user();
|
||||
$submission = Submission::where('id', $request->submission_id)->where('user_id', $user->id)->first();
|
||||
|
||||
if ($submission) {
|
||||
// Check if attempts count is 3 or more
|
||||
if ($submission->attempts >= 3) {
|
||||
return response()->json([
|
||||
'message' => 'Maximum attempts reached. Cannot restart submission.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$submission->createHistory("Submission has been restarted");
|
||||
|
||||
if ($submission->port != null) {
|
||||
|
|
@ -706,7 +677,6 @@ public function restart(Request $request)
|
|||
['rm', '-rf', $this->getTempDir($submission)],
|
||||
];
|
||||
}
|
||||
|
||||
// Delete temp directory
|
||||
foreach ($commands as $command) {
|
||||
if (!$this->is_dir_empty($this->getTempDir($submission))) {
|
||||
|
|
@ -726,7 +696,6 @@ public function restart(Request $request)
|
|||
'message' => 'Submission has been restarted successfully',
|
||||
], 200);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Submission not found',
|
||||
], 404);
|
||||
|
|
@ -737,18 +706,9 @@ public function changeSourceCode($submission_id)
|
|||
{
|
||||
$user = Auth::user();
|
||||
$submission = Submission::where('id', $submission_id)->where('user_id', $user->id)->first();
|
||||
|
||||
if ($submission) {
|
||||
// Check if attempts count is 3 or more
|
||||
if ($submission->attempts >= 3) {
|
||||
return response()->json([
|
||||
'message' => 'Maximum attempts reached. Cannot change source code.',
|
||||
], 403);
|
||||
return view('submissions.change_source_code', compact('submission'));
|
||||
}
|
||||
|
||||
return view('nodejs.submissions.change_source_code', compact('submission'));
|
||||
}
|
||||
|
||||
return redirect()->route('submissions');
|
||||
}
|
||||
|
||||
|
|
@ -773,6 +733,12 @@ public function update(Request $request)
|
|||
});
|
||||
}
|
||||
|
||||
// delete temp directory if is not empty
|
||||
$tempDir = $this->getTempDir($submission);
|
||||
if (!$this->is_dir_empty($tempDir)) {
|
||||
Process::fromShellCommandline('rm -rf ' . $tempDir)->run();
|
||||
}
|
||||
|
||||
if ($request->has('folder_path')) {
|
||||
$submission->type = Submission::$FILE;
|
||||
$submission->path = $request->folder_path;
|
||||
|
|
@ -781,7 +747,7 @@ public function update(Request $request)
|
|||
|
||||
if ($temporary_file) {
|
||||
$path = storage_path('app/' . $request->folder_path . '/' . $temporary_file->file_name);
|
||||
$submission->addMedia($path)->toMediaCollection('submissions', 'nodejs_public_submissions_files');
|
||||
$submission->addMedia($path)->toMediaCollection('submissions', 'public_submissions_files');
|
||||
if ($this->is_dir_empty(storage_path('app/' . $request->folder_path))) {
|
||||
rmdir(storage_path('app/' . $request->folder_path));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
<?php
|
||||
namespace App\Http\Controllers\NodeJS\Teacher;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// Statistik yang sudah ada
|
||||
$totalProyek = DB::table('projects')->count();
|
||||
|
||||
$totalSiswa = DB::table('users')
|
||||
->where('role', 'student')
|
||||
->count();
|
||||
|
||||
// Statistik baru
|
||||
|
||||
// 1. Hitung submission yang selesai (status 'completed' atau yang sesuai)
|
||||
$submissionSelesai = DB::table('submissions')
|
||||
->where('status', 'completed')
|
||||
->count();
|
||||
|
||||
// 2. Hitung siswa yang telah mengirimkan minimal satu proyek
|
||||
$siswaSubmisi = DB::table('submissions')
|
||||
->join('users', 'users.id', '=', 'submissions.user_id')
|
||||
->where('users.role', 'student')
|
||||
->distinct('submissions.user_id')
|
||||
->count('submissions.user_id');
|
||||
|
||||
// 3. Hitung total submission
|
||||
$totalSubmisi = DB::table('submissions')->count();
|
||||
|
||||
// 4. Hitung submission yang gagal (status 'failed')
|
||||
$submissionGagal = DB::table('submissions')
|
||||
->where('status', 'failed')
|
||||
->count();
|
||||
|
||||
// 5. Hitung tingkat keberhasilan (submission selesai vs total submission)
|
||||
$tingkatKeberhasilan = $totalSubmisi > 0
|
||||
? round(($submissionSelesai / $totalSubmisi) * 100)
|
||||
: 0;
|
||||
|
||||
// 6. Hitung tingkat kegagalan (submission gagal vs total submission)
|
||||
$tingkatKegagalan = $totalSubmisi > 0
|
||||
? round(($submissionGagal / $totalSubmisi) * 100)
|
||||
: 0;
|
||||
|
||||
// 7. Hitung proyek dengan minimal satu submission
|
||||
$proyekDenganSubmisi = DB::table('submissions')
|
||||
->distinct('project_id')
|
||||
->count('project_id');
|
||||
|
||||
// 8. Hitung proyek tanpa submission
|
||||
$proyekTanpaSubmisi = $totalProyek - $proyekDenganSubmisi;
|
||||
|
||||
// 9. Hitung persentase siswa yang telah membuat submission
|
||||
$tingkatPartisipasiSiswa = $totalSiswa > 0
|
||||
? round(($siswaSubmisi / $totalSiswa) * 100)
|
||||
: 0;
|
||||
|
||||
// 10. Hitung submission yang sedang diproses
|
||||
$submisiDalamProses = DB::table('submissions')
|
||||
->where('status', 'processing')
|
||||
->count();
|
||||
|
||||
// 11. Hitung submission yang tertunda
|
||||
$submisiTertunda = DB::table('submissions')
|
||||
->where('status', 'pending')
|
||||
->count();
|
||||
|
||||
// 12. Hitung rata-rata percobaan per submission
|
||||
$ratarataPecobaan = DB::table('submissions')
|
||||
->avg('attempts');
|
||||
$ratarataPecobaan = round($ratarataPecobaan, 1); // Bulatkan ke 1 angka desimal
|
||||
|
||||
return view('nodejs.dashboard-teacher.index', compact(
|
||||
'totalProyek',
|
||||
'totalSiswa',
|
||||
'submissionSelesai',
|
||||
'totalSubmisi',
|
||||
'tingkatPartisipasiSiswa',
|
||||
'submisiDalamProses',
|
||||
'submisiTertunda',
|
||||
'proyekDenganSubmisi',
|
||||
'proyekTanpaSubmisi',
|
||||
'siswaSubmisi',
|
||||
'ratarataPecobaan',
|
||||
'tingkatKeberhasilan',
|
||||
'submissionGagal',
|
||||
'tingkatKegagalan'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\NodeJS\Teacher;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RankController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the ranking page
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('nodejs.rank-teacher.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ranking data for a specific project
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getRankingByProject(Request $request)
|
||||
{
|
||||
$projectId = $request->input('project_id');
|
||||
|
||||
if (!$projectId) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Project ID is required'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Get all submissions for this project
|
||||
$submissions = DB::table('submissions')
|
||||
->join('users', 'submissions.user_id', '=', 'users.id')
|
||||
->where('submissions.project_id', $projectId)
|
||||
->select(
|
||||
'submissions.id',
|
||||
'submissions.user_id',
|
||||
'users.name as user_name',
|
||||
'submissions.attempts',
|
||||
'submissions.results',
|
||||
'submissions.updated_at'
|
||||
)
|
||||
->get();
|
||||
|
||||
// Process the submissions to calculate scores
|
||||
$rankings = [];
|
||||
$processedUsers = [];
|
||||
|
||||
foreach ($submissions as $submission) {
|
||||
$userId = $submission->user_id;
|
||||
|
||||
// Parse the results JSON
|
||||
$resultsData = json_decode($submission->results, true);
|
||||
|
||||
if (!$resultsData) {
|
||||
continue; // Skip invalid results
|
||||
}
|
||||
|
||||
// Calculate total tests and passed tests
|
||||
$totalTests = 0;
|
||||
$passedTests = 0;
|
||||
|
||||
foreach ($resultsData as $testSuite) {
|
||||
if (isset($testSuite['testResults'])) {
|
||||
foreach ($testSuite['testResults'] as $result) {
|
||||
if (isset($result['totalTests']) && isset($result['passedTests'])) {
|
||||
$totalTests += $result['totalTests'];
|
||||
$passedTests += $result['passedTests'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate percentage score and round to nearest integer
|
||||
$score = $totalTests > 0 ? ($passedTests / $totalTests) * 100 : 0;
|
||||
$score = round($score, 0); // Round to nearest integer
|
||||
|
||||
// If we already processed this user, only keep the higher score
|
||||
// If scores are equal, keep the one with fewer attempts
|
||||
if (isset($processedUsers[$userId])) {
|
||||
$shouldReplace = false;
|
||||
|
||||
if ($score > $processedUsers[$userId]['score']) {
|
||||
$shouldReplace = true;
|
||||
} elseif ($score == $processedUsers[$userId]['score'] && $submission->attempts < $processedUsers[$userId]['attempts']) {
|
||||
$shouldReplace = true;
|
||||
}
|
||||
|
||||
if ($shouldReplace) {
|
||||
$processedUsers[$userId] = [
|
||||
'user_id' => $userId,
|
||||
'user_name' => $submission->user_name,
|
||||
'attempts' => $submission->attempts,
|
||||
'score' => $score,
|
||||
'total_tests' => $totalTests,
|
||||
'passed_tests' => $passedTests,
|
||||
'submission_date' => $submission->updated_at,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$processedUsers[$userId] = [
|
||||
'user_id' => $userId,
|
||||
'user_name' => $submission->user_name,
|
||||
'attempts' => $submission->attempts,
|
||||
'score' => $score,
|
||||
'total_tests' => $totalTests,
|
||||
'passed_tests' => $passedTests,
|
||||
'submission_date' => $submission->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to array and sort by score (descending), then by attempts (ascending), then by submission date (ascending)
|
||||
$rankings = array_values($processedUsers);
|
||||
usort($rankings, function($a, $b) {
|
||||
// First priority: score (higher is better)
|
||||
if ($a['score'] != $b['score']) {
|
||||
return $b['score'] - $a['score'];
|
||||
}
|
||||
|
||||
// Second priority: attempts (fewer is better)
|
||||
if ($a['attempts'] != $b['attempts']) {
|
||||
return $a['attempts'] - $b['attempts'];
|
||||
}
|
||||
|
||||
// Third priority: submission date (earlier is better)
|
||||
return strtotime($a['submission_date']) - strtotime($b['submission_date']);
|
||||
});
|
||||
|
||||
// Add rank information
|
||||
$rank = 1;
|
||||
foreach ($rankings as $key => $value) {
|
||||
$rankings[$key]['rank'] = $rank++;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'rankings' => $rankings
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all projects for the ranking dropdown
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getProjects()
|
||||
{
|
||||
$projects = DB::table('projects')
|
||||
->select('id', 'title') // Alias 'title' as 'name' to match frontend expectations
|
||||
->orderBy('title')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => [
|
||||
'projects' => $projects
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export ranking data to CSV
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
*/
|
||||
public function exportRanking(Request $request)
|
||||
{
|
||||
$projectId = $request->input('project_id');
|
||||
|
||||
if (!$projectId) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Project ID is required'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Get project name
|
||||
$project = DB::table('projects')
|
||||
->where('id', $projectId)
|
||||
->first();
|
||||
|
||||
if (!$project) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Project not found'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Get rankings data by calling the existing method
|
||||
$rankingResponse = $this->getRankingByProject($request);
|
||||
$content = json_decode($rankingResponse->getContent(), true);
|
||||
|
||||
if (!isset($content['data']['rankings'])) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'No ranking data available'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$rankings = $content['data']['rankings'];
|
||||
$filename = 'ranking_' . \Illuminate\Support\Str::slug($project->title) . '_' . date('Y-m-d') . '.csv';
|
||||
|
||||
// Create CSV headers
|
||||
$headers = [
|
||||
"Content-type" => "text/csv",
|
||||
"Content-Disposition" => "attachment; filename=$filename",
|
||||
"Pragma" => "no-cache",
|
||||
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
|
||||
"Expires" => "0"
|
||||
];
|
||||
|
||||
// Create the CSV file
|
||||
$callback = function() use ($rankings) {
|
||||
$file = fopen('php://output', 'w');
|
||||
|
||||
// Add CSV header row
|
||||
fputcsv($file, ['Rank', 'Name', 'Attempts', 'Score (%)', 'Passed Tests', 'Total Tests', 'Submission Date']);
|
||||
|
||||
// Add data rows
|
||||
foreach ($rankings as $ranking) {
|
||||
fputcsv($file, [
|
||||
$ranking['rank'],
|
||||
$ranking['user_name'],
|
||||
$ranking['attempts'],
|
||||
$ranking['score'],
|
||||
$ranking['passed_tests'],
|
||||
$ranking['total_tests'],
|
||||
$ranking['submission_date']
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($file);
|
||||
};
|
||||
|
||||
return response()->stream($callback, 200, $headers);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,7 @@ class TeacherMiddleware
|
|||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
|
||||
if ( Auth::user()->role !== "teacher" ) {
|
||||
|
||||
if ( Auth::user()->teacher !== "teacher" ) {
|
||||
|
||||
abort(403, "Unauthorized action.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@
|
|||
use App\Models\NodeJS\ExecutionStep;
|
||||
use App\Models\NodeJS\Submission;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
|
|
|
|||
1069
database/seeders/NodeJS_Seeder.php
Normal file
41
package-lock.json
generated
|
|
@ -4,11 +4,6 @@
|
|||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.14.9",
|
||||
"select2": "^4.1.0-rc.0",
|
||||
"tailwindcss": "^4.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.1.2",
|
||||
"laravel-vite-plugin": "^0.7.5",
|
||||
|
|
@ -367,30 +362,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
||||
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/alpinejs": {
|
||||
"version": "3.14.9",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.9.tgz",
|
||||
"integrity": "sha512-gqSOhTEyryU9FhviNqiHBHzgjkvtukq9tevew29fTj+ofZtfsYriw4zPirHHOAy9bw8QoL3WGhyk7QqCh5AYlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "~3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
|
@ -637,12 +608,6 @@
|
|||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/select2": {
|
||||
"version": "4.1.0-rc.0",
|
||||
"resolved": "https://registry.npmjs.org/select2/-/select2-4.1.0-rc.0.tgz",
|
||||
"integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
|
|
@ -652,12 +617,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz",
|
||||
"integrity": "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz",
|
||||
|
|
|
|||
|
|
@ -9,10 +9,5 @@
|
|||
"axios": "^1.1.2",
|
||||
"laravel-vite-plugin": "^0.7.5",
|
||||
"vite": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.14.9",
|
||||
"select2": "^4.1.0-rc.0",
|
||||
"tailwindcss": "^4.1.7"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -29,39 +29,25 @@ describe("Testing application configuration", () => {
|
|||
it("Should have the necessary development packages", (done) => {
|
||||
expect(
|
||||
packages.devDependencies,
|
||||
`The package "cross-env" was not found in the devDependencies object. Install the package by running this command "npm i cross-env --save-dev"`,
|
||||
options
|
||||
).toHaveProperty("cross-env");
|
||||
expect(
|
||||
packages.devDependencies,
|
||||
`The package "jest" was not found in the devDependencies object. Install the package by running this command "npm i jest --save-dev"`,
|
||||
options
|
||||
).toHaveProperty("jest");
|
||||
expect(
|
||||
packages.devDependencies,
|
||||
`The package "nodemon" was not found in the devDependencies object. Install the package by running this command "npm i nodemon --save-dev"`,
|
||||
options
|
||||
).toHaveProperty("nodemon");
|
||||
expect(
|
||||
packages.devDependencies,
|
||||
`The package "supertest" was not found in the devDependencies object. Install the package by running this command "npm i supertest --save-dev"`,
|
||||
options
|
||||
).toHaveProperty("supertest");
|
||||
expect(
|
||||
packages.devDependencies,
|
||||
`The package "jest-image-snapshot" was not found in the devDependencies object. Install the package by running this command "npm i jest-image-snapshot --save-dev"`,
|
||||
options
|
||||
).toHaveProperty("jest-image-snapshot");
|
||||
expect(
|
||||
packages.devDependencies,
|
||||
`The package "jest-expect-message" was not found in the devDependencies object. Install the package by running this command "npm i jest-expect-message --save-dev"`,
|
||||
options
|
||||
).toHaveProperty("jest-expect-message");
|
||||
expect(
|
||||
packages.devDependencies,
|
||||
`The package "puppeteer" was not found in the devDependencies object. Install the package by running this command "npm i puppeteer --save-dev"`,
|
||||
options
|
||||
).toHaveProperty("puppeteer");
|
||||
// expect(
|
||||
// packages.devDependencies,
|
||||
// ).toHaveProperty("jest-image-snapshot");
|
||||
// expect(
|
||||
// packages.devDependencies,
|
||||
// ).toHaveProperty("jest-expect-message");
|
||||
// expect(
|
||||
// packages.devDependencies,
|
||||
// ).toHaveProperty("puppeteer");
|
||||
done();
|
||||
});
|
||||
// Testing the package.json file for the necessary production packages
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
|
@ -1,166 +0,0 @@
|
|||
const request = require('supertest');
|
||||
const mongoose = require('mongoose');
|
||||
const {app} = require('../app');
|
||||
const packages = require('../package.json');
|
||||
const connectDB = require('../src/config/database');
|
||||
|
||||
|
||||
|
||||
describe("Pengujian konfigurasi aplikasi", () => {
|
||||
it("Harus memiliki paket-paket development yang diperlukan", (done) => {
|
||||
try {
|
||||
expect(packages.devDependencies).toHaveProperty("jest");
|
||||
} catch (error) {
|
||||
throw new Error('Paket "jest" tidak ditemukan di devDependencies. Jalankan "npm i jest --save-dev".');
|
||||
}
|
||||
|
||||
try {
|
||||
expect(packages.devDependencies).toHaveProperty("nodemon");
|
||||
} catch (error) {
|
||||
throw new Error('Paket "nodemon" tidak ditemukan di devDependencies. Jalankan "npm i nodemon --save-dev".');
|
||||
}
|
||||
|
||||
try {
|
||||
expect(packages.devDependencies).toHaveProperty("supertest");
|
||||
} catch (error) {
|
||||
throw new Error('Paket "supertest" tidak ditemukan di devDependencies. Jalankan "npm i supertest --save-dev".');
|
||||
}
|
||||
|
||||
try {
|
||||
expect(packages.devDependencies).toHaveProperty("cross-env");
|
||||
} catch (error) {
|
||||
throw new Error('Paket "cross-env" tidak ditemukan di devDependencies. Jalankan "npm i cross-env --save-dev".');
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
it("Harus memiliki paket-paket production yang diperlukan", (done) => {
|
||||
try {
|
||||
expect(packages.dependencies).toHaveProperty("dotenv");
|
||||
} catch (error) {
|
||||
throw new Error('Paket "dotenv" tidak ditemukan di dependencies. Jalankan "npm i dotenv --save".');
|
||||
}
|
||||
|
||||
try {
|
||||
expect(packages.dependencies).toHaveProperty("express");
|
||||
} catch (error) {
|
||||
throw new Error('Paket "express" tidak ditemukan di dependencies. Jalankan "npm i express --save".');
|
||||
}
|
||||
|
||||
try {
|
||||
expect(packages.dependencies).toHaveProperty("mongoose");
|
||||
} catch (error) {
|
||||
throw new Error('Paket "mongoose" tidak ditemukan di dependencies. Jalankan "npm i mongoose --save".');
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
it("Harus memiliki nama yang benar", (done) => {
|
||||
try {
|
||||
expect(packages.name).toBe("restaurant-reservation");
|
||||
} catch (error) {
|
||||
throw new Error(`Nama aplikasi harus "restaurant-reservation", tetapi ditemukan "${packages.name}".`);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
it("Harus memiliki variabel lingkungan yang benar", (done) => {
|
||||
try {
|
||||
expect(process.env).toHaveProperty("MONGODB_URL");
|
||||
} catch (error) {
|
||||
throw new Error('Variabel lingkungan "MONGODB_URL" tidak ditemukan. Periksa file .env');
|
||||
}
|
||||
|
||||
try {
|
||||
expect(process.env).toHaveProperty("PORT");
|
||||
} catch (error) {
|
||||
throw new Error('Variabel lingkungan "PORT" tidak ditemukan. Periksa file .env');
|
||||
}
|
||||
|
||||
try {
|
||||
expect(process.env).toHaveProperty("MONGODB_URL_TEST");
|
||||
} catch (error) {
|
||||
throw new Error('Variabel lingkungan "MONGODB_URL_TEST" tidak ditemukan. Periksa file .env');
|
||||
}
|
||||
|
||||
try {
|
||||
expect(process.env).toHaveProperty("NODE_ENV");
|
||||
} catch (error) {
|
||||
throw new Error('Variabel lingkungan "NODE_ENV" tidak ditemukan. Periksa file .env');
|
||||
}
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Pengujian Middleware Aplikasi", () => {
|
||||
it("Harus memiliki middleware yang diperlukan", (done) => {
|
||||
let application_stack = [];
|
||||
app._router.stack.forEach((element) => {
|
||||
application_stack.push(element.name);
|
||||
});
|
||||
|
||||
// Test for JSON middleware
|
||||
expect(application_stack).toContain("jsonParser");
|
||||
if (!application_stack.includes("jsonParser")) {
|
||||
throw new Error("Aplikasi tidak menggunakan format JSON. Periksa file app.js");
|
||||
}
|
||||
|
||||
// Test for Express middleware
|
||||
expect(application_stack).toContain("expressInit");
|
||||
if (!application_stack.includes("expressInit")) {
|
||||
throw new Error("Aplikasi tidak menggunakan express framework. Periksa file app.js");
|
||||
}
|
||||
|
||||
// Test for URL-encoded middleware
|
||||
expect(application_stack).toContain("urlencodedParser");
|
||||
if (!application_stack.includes("urlencodedParser")) {
|
||||
throw new Error("Aplikasi tidak menggunakan format urlencoded. Periksa file app.js");
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pengujian Koneksi Database', () => {
|
||||
it('Harus berhasil terhubung ke database MongoDB', async () => {
|
||||
try {
|
||||
const state = mongoose.connection.readyState;
|
||||
expect(state).toBe(1);
|
||||
} catch (error) {
|
||||
throw new Error(`Gagal terhubung ke database: ${error.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pengujian API Utama', () => {
|
||||
it('Harus mengembalikan pesan yang sesuai', async () => {
|
||||
try {
|
||||
const res = await request(app).get('/test');
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toHaveProperty('status', 'success');
|
||||
expect(res.body).toHaveProperty('message', 'Welcome to Restaurant Reservation API');
|
||||
expect(res.body).toHaveProperty('version', '1.0.0');
|
||||
} catch (error) {
|
||||
throw new Error(`Terjadi kesalahan pada pengujian GET /: ${error.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function disconnectDB() {
|
||||
await mongoose.connection.close();
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
try {
|
||||
await connectDB();
|
||||
dbConnected = true;
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Gagal terhubung ke database:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
afterAll(async () => {
|
||||
await disconnectDB();
|
||||
});
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../app");
|
||||
const Menu = require("../src/models/menuModel");
|
||||
const connectDB = require("../src/config/database");
|
||||
|
||||
describe("Pengujian Integrasi - API Menu", () => {
|
||||
beforeAll(async () => {
|
||||
await connectDB();
|
||||
await Menu.deleteMany({}); // Pastikan database dalam keadaan kosong sebelum pengujian
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await Menu.deleteMany({}); // Hapus semua data sebelum setiap pengujian
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.close(); // Tutup koneksi database setelah semua pengujian selesai
|
||||
});
|
||||
|
||||
it("harus berhasil membuat item menu baru melalui API", async () => {
|
||||
const newItem = {
|
||||
name: "Pizza",
|
||||
description: "Pizza keju yang lezat",
|
||||
price: 12.99,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
};
|
||||
|
||||
const res = await request(app).post("/createMenu").send(newItem);
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.name).toBe(newItem.name);
|
||||
expect(res.body.price).toBe(newItem.price);
|
||||
});
|
||||
|
||||
it("harus mengambil semua item menu melalui API", async () => {
|
||||
await Menu.create([
|
||||
{ name: "Burger", price: 9.99, category: "main", isAvailable: true },
|
||||
{ name: "Salad", price: 5.99, category: "appetizer", isAvailable: true },
|
||||
]);
|
||||
|
||||
const res = await request(app).get("/menu");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.length).toBe(2);
|
||||
});
|
||||
|
||||
it("harus mengambil item menu berdasarkan kategori melalui API", async () => {
|
||||
await Menu.create([
|
||||
{ name: "Steak", price: 19.99, category: "main", isAvailable: true },
|
||||
{ name: "Soda", price: 2.99, category: "beverage", isAvailable: true },
|
||||
]);
|
||||
|
||||
const res = await request(app).get("/menu/main");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.length).toBe(1);
|
||||
expect(res.body[0].name).toBe("Steak");
|
||||
});
|
||||
|
||||
it("harus mengembalikan 404 jika kategori tidak ditemukan", async () => {
|
||||
const res = await request(app).get("/menu/dessert");
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toBe("Menu with category 'dessert' not found");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
const Menu = require("../src/models/menuModel");
|
||||
const menuController = require("../src/controllers/menuController");
|
||||
|
||||
// Mock model Menu
|
||||
jest.mock("../src/models/menuModel");
|
||||
|
||||
describe("Pengujian Unit - Controller Menu", () => {
|
||||
let req, res;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
req = {
|
||||
body: {
|
||||
name: "Item Uji",
|
||||
description: "Deskripsi Uji",
|
||||
price: 9.99,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
},
|
||||
params: {
|
||||
category: "main",
|
||||
},
|
||||
};
|
||||
|
||||
res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// ✅ Pengujian Pembuatan Item Menu
|
||||
describe("createMenuItem", () => {
|
||||
it("harus berhasil membuat item menu", async () => {
|
||||
const savedItem = { ...req.body, _id: "123" };
|
||||
Menu.mockImplementation(() => ({
|
||||
save: jest.fn().mockResolvedValue(savedItem),
|
||||
}));
|
||||
|
||||
await menuController.createMenuItem(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(201);
|
||||
expect(res.json).toHaveBeenCalledWith(savedItem);
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ Pengujian Mendapatkan Semua Item Menu
|
||||
describe("getAllMenuItems", () => {
|
||||
it("harus mengembalikan semua item menu dengan sukses", async () => {
|
||||
const items = [
|
||||
{ name: "Item 1", price: 9.99 },
|
||||
{ name: "Item 2", price: 14.99 },
|
||||
];
|
||||
Menu.find.mockResolvedValue(items);
|
||||
|
||||
await menuController.getAllMenuItems(req, res);
|
||||
|
||||
expect(Menu.find).toHaveBeenCalledWith({});
|
||||
expect(res.json).toHaveBeenCalledWith(items);
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ Pengujian Mendapatkan Menu Berdasarkan Kategori
|
||||
describe("getMenuByCategory", () => {
|
||||
it("harus mengembalikan item untuk kategori yang valid", async () => {
|
||||
const items = [
|
||||
{ name: "Main Course 1", category: "main" },
|
||||
{ name: "Main Course 2", category: "main" },
|
||||
];
|
||||
Menu.find.mockResolvedValue(items);
|
||||
|
||||
await menuController.getMenuByCategory(req, res);
|
||||
|
||||
expect(Menu.find).toHaveBeenCalledWith({ category: "main" });
|
||||
expect(res.json).toHaveBeenCalledWith(items);
|
||||
});
|
||||
|
||||
it("harus menangani kategori yang tidak ditemukan", async () => {
|
||||
Menu.find.mockResolvedValue([]);
|
||||
|
||||
await menuController.getMenuByCategory(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: "Menu with category 'main' not found",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../app");
|
||||
const Meja = require("../src/models/mejaModel");
|
||||
const connectDB = require("../src/config/database");
|
||||
|
||||
describe("Pengujian Integrasi - API Meja", () => {
|
||||
beforeAll(async () => {
|
||||
await connectDB();
|
||||
await Meja.deleteMany({}); // Pastikan database dalam keadaan kosong sebelum pengujian
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await Meja.deleteMany({}); // Hapus semua data sebelum setiap pengujian
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.close(); // Tutup koneksi database setelah semua pengujian selesai
|
||||
});
|
||||
|
||||
describe("POST add/meja", () => {
|
||||
test("Harus berhasil membuat data meja baru", async () => {
|
||||
const dataMeja = {
|
||||
tableNumber: 1,
|
||||
capacity: 4
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/add/meja")
|
||||
.send(dataMeja);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty("tableNumber", 1);
|
||||
expect(response.body.data).toHaveProperty("capacity", 4);
|
||||
expect(response.body.data).toHaveProperty("status", "available");
|
||||
});
|
||||
|
||||
test("Harus gagal ketika data tableNumber tidak disediakan", async () => {
|
||||
const dataMeja = {
|
||||
capacity: 4
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/add/meja")
|
||||
.send(dataMeja);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.error).toBeDefined();
|
||||
});
|
||||
|
||||
test("Harus gagal ketika nomor meja sudah ada", async () => {
|
||||
// Buat meja pertama
|
||||
await Meja.create({
|
||||
tableNumber: 5,
|
||||
capacity: 4
|
||||
});
|
||||
|
||||
// Coba buat meja dengan nomor yang sama
|
||||
const response = await request(app)
|
||||
.post("/add/meja")
|
||||
.send({
|
||||
tableNumber: 5,
|
||||
capacity: 2
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /meja", () => {
|
||||
test("Harus mendapatkan daftar meja kosong", async () => {
|
||||
const response = await request(app).get("/meja");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toEqual([]);
|
||||
});
|
||||
|
||||
test("Harus mendapatkan semua data meja", async () => {
|
||||
// Tambahkan beberapa meja untuk pengujian
|
||||
await Meja.create([
|
||||
{ tableNumber: 1, capacity: 2 },
|
||||
{ tableNumber: 2, capacity: 4 },
|
||||
{ tableNumber: 3, capacity: 6 }
|
||||
]);
|
||||
|
||||
const response = await request(app).get("/meja");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.length).toBe(3);
|
||||
expect(response.body.data[0]).toHaveProperty("tableNumber", 1);
|
||||
expect(response.body.data[1]).toHaveProperty("tableNumber", 2);
|
||||
expect(response.body.data[2]).toHaveProperty("tableNumber", 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /meja/:tableNumber/reserve", () => {
|
||||
test("Harus berhasil memesan meja yang tersedia", async () => {
|
||||
// Buat data meja terlebih dahulu
|
||||
await Meja.create({
|
||||
tableNumber: 10,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put("/meja/10/reserve")
|
||||
.send({ customerName: "John Doe" }); // Tambahkan customerName
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty("tableNumber", 10);
|
||||
expect(response.body.data).toHaveProperty("status", "reserved");
|
||||
expect(response.body.data).toHaveProperty("customerName", "John Doe"); // Verifikasi customerName
|
||||
});
|
||||
|
||||
test("Harus gagal memesan meja yang tidak ada", async () => {
|
||||
const response = await request(app)
|
||||
.put("/meja/99/reserve")
|
||||
.send({ customerName: "John Doe" }); // Tambahkan customerName
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.error).toBe("Meja tidak tersedia");
|
||||
});
|
||||
|
||||
test("Harus gagal memesan meja yang sudah dipesan", async () => {
|
||||
// Buat meja yang sudah dipesan
|
||||
await Meja.create({
|
||||
tableNumber: 11,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put("/meja/11/reserve")
|
||||
.send({ customerName: "John Doe" }); // Tambahkan customerName
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.error).toBe("Meja tidak tersedia");
|
||||
});
|
||||
|
||||
test("Harus gagal memesan meja jika customerName tidak disediakan", async () => {
|
||||
// Buat data meja terlebih dahulu
|
||||
await Meja.create({
|
||||
tableNumber: 14,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put("/meja/14/reserve")
|
||||
.send({}); // Tidak menyertakan customerName
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.error).toBe("Nama pelanggan harus diisi");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /meja/:tableNumber/cancel", () => {
|
||||
test("Harus berhasil membatalkan reservasi meja", async () => {
|
||||
// Buat meja yang sudah dipesan
|
||||
await Meja.create({
|
||||
tableNumber: 12,
|
||||
capacity: 6,
|
||||
status: "reserved",
|
||||
customerName: "John Doe", // Tambahkan customerName
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put("/meja/12/cancel")
|
||||
.send({});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data).toHaveProperty("tableNumber", 12);
|
||||
expect(response.body.data).toHaveProperty("status", "available");
|
||||
expect(response.body.message).toBe("Reservation for table 12 has been cancelled");
|
||||
});
|
||||
|
||||
test("Harus gagal membatalkan reservasi meja yang tidak ada", async () => {
|
||||
const response = await request(app)
|
||||
.put("/meja/99/cancel")
|
||||
.send({});
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.error).toBe("Table not found or not currently reserved");
|
||||
});
|
||||
|
||||
test("Harus gagal membatalkan reservasi meja dengan status available", async () => {
|
||||
// Buat meja dengan status available
|
||||
await Meja.create({
|
||||
tableNumber: 13,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put("/meja/13/cancel")
|
||||
.send({});
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.success).toBe(false);
|
||||
expect(response.body.error).toBe("Table not found or not currently reserved");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Skenario Alur Pengelolaan Meja", () => {
|
||||
test("Harus menjalankan alur lengkap: membuat, memesan, dan membatalkan reservasi meja", async () => {
|
||||
// 1. Membuat meja baru
|
||||
const createResponse = await request(app)
|
||||
.post("/add/meja")
|
||||
.send({ tableNumber: 20, capacity: 4 });
|
||||
|
||||
expect(createResponse.status).toBe(201);
|
||||
expect(createResponse.body.data).toHaveProperty("tableNumber", 20);
|
||||
expect(createResponse.body.data).toHaveProperty("status", "available");
|
||||
|
||||
// 2. Memesan meja
|
||||
const reserveResponse = await request(app)
|
||||
.put("/meja/20/reserve")
|
||||
.send({ customerName: "John Doe" }); // Tambahkan customerName
|
||||
|
||||
expect(reserveResponse.status).toBe(200);
|
||||
expect(reserveResponse.body.data).toHaveProperty("status", "reserved");
|
||||
expect(reserveResponse.body.data).toHaveProperty("customerName", "John Doe"); // Verifikasi customerName
|
||||
|
||||
// 3. Membatalkan reservasi
|
||||
const cancelResponse = await request(app)
|
||||
.put("/meja/20/cancel")
|
||||
.send({});
|
||||
|
||||
expect(cancelResponse.status).toBe(200);
|
||||
expect(cancelResponse.body.data).toHaveProperty("status", "available");
|
||||
|
||||
// 4. Verifikasi status akhir
|
||||
const getResponse = await request(app).get("/meja");
|
||||
|
||||
expect(getResponse.status).toBe(200);
|
||||
expect(getResponse.body.data.length).toBe(1);
|
||||
expect(getResponse.body.data[0]).toHaveProperty("tableNumber", 20);
|
||||
expect(getResponse.body.data[0]).toHaveProperty("status", "available");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,307 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
const Meja = require('../src/models/mejaModel');
|
||||
const mejaController = require('../src/controllers/mejaController');
|
||||
|
||||
// Mock Express req dan res objects
|
||||
const mockRequest = (body = {}, params = {}) => ({
|
||||
body,
|
||||
params
|
||||
});
|
||||
|
||||
const mockResponse = () => {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
// Mock the Meja model methods
|
||||
jest.mock('../src/models/mejaModel');
|
||||
|
||||
describe('Meja Controller', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createMeja', () => {
|
||||
test('harus membuat meja baru dan mengembalikan status 201', async () => {
|
||||
// Arrange
|
||||
const mockMeja = {
|
||||
tableNumber: 1,
|
||||
capacity: 4,
|
||||
status: 'available'
|
||||
};
|
||||
|
||||
Meja.create.mockResolvedValue(mockMeja);
|
||||
|
||||
const req = mockRequest({ tableNumber: 1, capacity: 4 });
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.createMeja(req, res);
|
||||
|
||||
// Assert
|
||||
expect(Meja.create).toHaveBeenCalledWith({
|
||||
tableNumber: 1,
|
||||
capacity: 4
|
||||
});
|
||||
expect(res.status).toHaveBeenCalledWith(201);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockMeja
|
||||
});
|
||||
});
|
||||
|
||||
test('harus menangani error dan mengembalikan status 400', async () => {
|
||||
// Arrange
|
||||
const errorMessage = 'Validation error';
|
||||
Meja.create.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const req = mockRequest({ tableNumber: 1, capacity: 4 });
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.createMeja(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllMeja', () => {
|
||||
test('harus mengembalikan semua meja dengan status 200', async () => {
|
||||
// Arrange
|
||||
const mockMejaList = [
|
||||
{ tableNumber: 1, capacity: 4, status: 'available' },
|
||||
{ tableNumber: 2, capacity: 2, status: 'reserved' }
|
||||
];
|
||||
|
||||
// Setup chaining untuk find().sort()
|
||||
const mockSort = jest.fn().mockResolvedValue(mockMejaList);
|
||||
const mockFind = jest.fn().mockReturnValue({ sort: mockSort });
|
||||
Meja.find = mockFind;
|
||||
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.getAllMeja(req, res);
|
||||
|
||||
// Assert
|
||||
expect(Meja.find).toHaveBeenCalled();
|
||||
expect(mockSort).toHaveBeenCalledWith({ tableNumber: 1 });
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockMejaList
|
||||
});
|
||||
});
|
||||
|
||||
test('harus menangani error dan mengembalikan status 400', async () => {
|
||||
// Arrange
|
||||
const errorMessage = 'Database error';
|
||||
|
||||
// Setup chaining untuk find().sort() yang mengembalikan error
|
||||
const mockSort = jest.fn().mockRejectedValue(new Error(errorMessage));
|
||||
const mockFind = jest.fn().mockReturnValue({ sort: mockSort });
|
||||
Meja.find = mockFind;
|
||||
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.getAllMeja(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reserveMeja', () => {
|
||||
test('harus memesan meja yang tersedia dan mengembalikan status 200', async () => {
|
||||
// Arrange
|
||||
const tableNumber = '5';
|
||||
const customerName = 'John Doe';
|
||||
const mockMeja = {
|
||||
tableNumber: 5,
|
||||
capacity: 4,
|
||||
status: 'reserved',
|
||||
customerName: 'John Doe'
|
||||
};
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(mockMeja);
|
||||
|
||||
const req = mockRequest({ customerName }, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.reserveMeja(req, res);
|
||||
|
||||
// Assert
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber, status: 'available' },
|
||||
{ status: 'reserved', customerName },
|
||||
{ new: true }
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockMeja
|
||||
});
|
||||
});
|
||||
|
||||
test('harus mengembalikan 404 ketika meja tidak tersedia', async () => {
|
||||
// Arrange
|
||||
const tableNumber = '5';
|
||||
const customerName = 'John Doe';
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(null);
|
||||
|
||||
const req = mockRequest({ customerName }, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.reserveMeja(req, res);
|
||||
|
||||
// Assert
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber, status: 'available' },
|
||||
{ status: 'reserved', customerName },
|
||||
{ new: true }
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Meja tidak tersedia'
|
||||
});
|
||||
});
|
||||
|
||||
test('harus mengembalikan 400 ketika customerName tidak disediakan', async () => {
|
||||
// Arrange
|
||||
const tableNumber = '5';
|
||||
|
||||
const req = mockRequest({ tableNumber }, {});
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.reserveMeja(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Nama pelanggan harus diisi'
|
||||
});
|
||||
});
|
||||
|
||||
test('harus menangani error dan mengembalikan status 400', async () => {
|
||||
// Arrange
|
||||
const tableNumber = '5';
|
||||
const customerName = 'John Doe';
|
||||
const errorMessage = 'Database error';
|
||||
|
||||
Meja.findOneAndUpdate.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const req = mockRequest({ customerName }, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.reserveMeja(req, res);
|
||||
|
||||
// Assert
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber, status: 'available' },
|
||||
{ status: 'reserved', customerName },
|
||||
{ new: true }
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelReservation', () => {
|
||||
test('harus membatalkan reservasi dan mengembalikan status 200', async () => {
|
||||
// Arrange
|
||||
const tableNumber = '3';
|
||||
const mockMeja = {
|
||||
tableNumber: 3,
|
||||
capacity: 4,
|
||||
status: 'available',
|
||||
customerName: ''
|
||||
};
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(mockMeja);
|
||||
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.cancelReservation(req, res);
|
||||
|
||||
// Assert
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber, status: 'reserved' },
|
||||
{ status: 'available', customerName: '', updatedAt: expect.any(Number) },
|
||||
{ new: true }
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: `Reservation for table ${tableNumber} has been cancelled`,
|
||||
data: mockMeja
|
||||
});
|
||||
});
|
||||
|
||||
test('harus mengembalikan 404 ketika meja tidak ditemukan atau tidak sedang dipesan', async () => {
|
||||
// Arrange
|
||||
const tableNumber = '3';
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(null);
|
||||
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.cancelReservation(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: 'Table not found or not currently reserved'
|
||||
});
|
||||
});
|
||||
|
||||
test('harus menangani error dan mengembalikan status 400', async () => {
|
||||
// Arrange
|
||||
const tableNumber = '3';
|
||||
const errorMessage = 'Database error';
|
||||
|
||||
Meja.findOneAndUpdate.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
// Act
|
||||
await mejaController.cancelReservation(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../../app");
|
||||
const connectDB = require("../../src/config/database");
|
||||
|
||||
describe("Module 1 - Database & Basic API", () => {
|
||||
beforeAll(async () => {
|
||||
await connectDB();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe("Database Connection", () => {
|
||||
test("harus berhasil terhubung dengan MongoDB", () => {
|
||||
const connectionState = mongoose.connection.readyState;
|
||||
expect(connectionState).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test API Route", () => {
|
||||
test("harus mengembalikan format respons yang benar dari rute pengujian", async () => {
|
||||
const response = await request(app).get("/test");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
status: "success",
|
||||
message: "Welcome to Restaurant Reservation API",
|
||||
version: "1.0.0",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../../app");
|
||||
const Menu = require("../../src/models/menuModel");
|
||||
const connectDB = require("../../src/config/database");
|
||||
|
||||
describe("Module 2 - Menu API", () => {
|
||||
beforeAll(async () => {
|
||||
await connectDB();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await Menu.deleteMany({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe("POST /createMenu", () => {
|
||||
test("harus berhasil membuat item menu baru", async () => {
|
||||
const menuItem = {
|
||||
name: "Nasi Goreng",
|
||||
description: "Nasi goreng dengan telur mata sapi dan ayam kampung",
|
||||
price: 25000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createMenu").send(menuItem);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty("name", "Nasi Goreng");
|
||||
expect(response.body).toHaveProperty("price", 25000);
|
||||
expect(response.body).toHaveProperty("category", "main");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /menu", () => {
|
||||
test("harus mengembalikan seluruh item menu", async () => {
|
||||
const firstItem = await Menu.create({
|
||||
name: "Nasi Putih",
|
||||
price: 5000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
});
|
||||
const secondItem = await Menu.create({
|
||||
name: "Kerupuk Udang",
|
||||
price: 3000,
|
||||
category: "appetizer",
|
||||
isAvailable: true,
|
||||
});
|
||||
|
||||
const response = await request(app).get("/menu");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.length).toBeGreaterThanOrEqual(2);
|
||||
const nasiInResponse = response.body.find(
|
||||
(item) => item._id === firstItem._id.toString(),
|
||||
);
|
||||
const kerupukInResponse = response.body.find(
|
||||
(item) => item._id === secondItem._id.toString(),
|
||||
);
|
||||
|
||||
expect(nasiInResponse).toBeDefined();
|
||||
expect(kerupukInResponse).toBeDefined();
|
||||
expect(nasiInResponse).toHaveProperty("name", "Nasi Putih");
|
||||
expect(kerupukInResponse).toHaveProperty("name", "Kerupuk Udang");
|
||||
});
|
||||
|
||||
test("harus mengembalikan array kosong jika tidak ada item menu", async () => {
|
||||
await Menu.deleteMany({});
|
||||
|
||||
const response = await request(app).get("/menu");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /menu/:category", () => {
|
||||
test("harus mengembalikan item menu berdasarkan kategori yang ditentukan", async () => {
|
||||
await Menu.deleteMany({});
|
||||
|
||||
await Menu.create([
|
||||
{
|
||||
name: "Nasi Rames",
|
||||
price: 20000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
},
|
||||
{
|
||||
name: "Gado-gado",
|
||||
price: 15000,
|
||||
category: "appetizer",
|
||||
isAvailable: true,
|
||||
},
|
||||
{
|
||||
name: "Soto Ayam",
|
||||
price: 18000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const response = await request(app).get("/menu/main");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(2);
|
||||
|
||||
response.body.forEach((item) => {
|
||||
expect(item.category).toBe("main");
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 404 jika kategori tidak ditemukan", async () => {
|
||||
const response = await request(app).get("/menu/bukanmakanan");
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Menu with category 'bukanmakanan' not found",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../../app");
|
||||
const Meja = require("../../src/models/mejaModel");
|
||||
const connectDB = require("../../src/config/database");
|
||||
|
||||
describe("Module 3 - Meja/Table API", () => {
|
||||
beforeAll(async () => {
|
||||
await connectDB();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await Meja.deleteMany({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe("POST /createMeja", () => {
|
||||
test("harus berhasil membuat meja baru", async () => {
|
||||
const newTable = {
|
||||
tableNumber: 5,
|
||||
capacity: 4,
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createMeja").send(newTable);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
expect(response.body.data).toHaveProperty("tableNumber", 5);
|
||||
expect(response.body.data).toHaveProperty("capacity", 4);
|
||||
expect(response.body.data).toHaveProperty("status", "available");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /meja", () => {
|
||||
test("harus mengembalikan semua meja", async () => {
|
||||
await Meja.create({ tableNumber: 1, capacity: 2 });
|
||||
await Meja.create({ tableNumber: 2, capacity: 4 });
|
||||
|
||||
const response = await request(app).get("/meja");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
expect(response.body.data.length).toBe(2);
|
||||
|
||||
const returnedTables = response.body.data;
|
||||
const returnedTable1 = returnedTables.find((t) => t.tableNumber === 1);
|
||||
const returnedTable2 = returnedTables.find((t) => t.tableNumber === 2);
|
||||
|
||||
expect(returnedTable1 || returnedTable2).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /meja/:tableNumber/reserve", () => {
|
||||
test("harus berhasil memesan meja yang tersedia", async () => {
|
||||
const tableNumber = 10;
|
||||
|
||||
await Meja.create({
|
||||
tableNumber: tableNumber,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const tableBeforeReserve = await Meja.findOne({ tableNumber });
|
||||
expect(tableBeforeReserve).toBeDefined();
|
||||
expect(tableBeforeReserve.status).toBe("available");
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/meja/${tableNumber}/reserve`)
|
||||
.send({ customerName: "Susilo Bambang" });
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Unexpected status code:", response.status);
|
||||
console.log("Response body:", response.body);
|
||||
}
|
||||
|
||||
expect(response.body).toHaveProperty("success");
|
||||
if (response.body.success) {
|
||||
expect(response.body.data).toHaveProperty("customerName");
|
||||
expect(response.body.data).toHaveProperty("status", "reserved");
|
||||
expect(response.body.data.customerName).toBe("Susilo Bambang");
|
||||
}
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 400 jika nama pelanggan tidak disediakan", async () => {
|
||||
await Meja.create({
|
||||
tableNumber: 11,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const response = await request(app).put("/meja/11/reserve").send({});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Nama pelanggan harus diisi",
|
||||
);
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 404 jika meja tidak tersedia", async () => {
|
||||
await Meja.create({
|
||||
tableNumber: 12,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
customerName: "Dewi Sartika",
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put("/meja/12/reserve")
|
||||
.send({ customerName: "Budi Sudarsono" });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty("error", "Meja tidak tersedia");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /meja/:tableNumber/cancel", () => {
|
||||
test("harus berhasil membatalkan pemesanan meja", async () => {
|
||||
// Create a reserved table first with correct status and customerName
|
||||
await Meja.create({
|
||||
tableNumber: 15,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
customerName: "Anita Wijaya",
|
||||
});
|
||||
|
||||
// Verify that the table was created with the right status
|
||||
const createdTable = await Meja.findOne({ tableNumber: 15 });
|
||||
expect(createdTable).toBeDefined();
|
||||
expect(createdTable.status).toBe("reserved");
|
||||
|
||||
const response = await request(app).put("/meja/15/cancel").send({});
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Unexpected status code for cancel:", response.status);
|
||||
console.log("Cancel response body:", response.body);
|
||||
}
|
||||
|
||||
expect(response.body).toHaveProperty("success");
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 404 jika meja tidak ditemukan", async () => {
|
||||
const response = await request(app).put("/meja/99/cancel").send({});
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Table not found or not currently reserved",
|
||||
);
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 404 jika meja tidak dalam status dipesan", async () => {
|
||||
// Create an available table first
|
||||
await Meja.create({
|
||||
tableNumber: 16,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const response = await request(app).put("/meja/16/cancel").send({});
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Table not found or not currently reserved",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../../app");
|
||||
const Order = require("../../src/models/orderModel");
|
||||
const Meja = require("../../src/models/mejaModel");
|
||||
const Menu = require("../../src/models/menuModel");
|
||||
const connectDB = require("../../src/config/database");
|
||||
|
||||
describe("Module 4 - Order API", () => {
|
||||
let testMenuId1, testMenuId2;
|
||||
|
||||
beforeAll(async () => {
|
||||
await connectDB();
|
||||
|
||||
const menu1 = await Menu.create({
|
||||
name: "Nasi Goreng Spesial",
|
||||
description: "Nasi goreng dengan telur, ayam, dan sayuran segar",
|
||||
price: 25000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
});
|
||||
|
||||
const menu2 = await Menu.create({
|
||||
name: "Sate Ayam",
|
||||
description: "Sate ayam dengan bumbu kacang khas Indonesia",
|
||||
price: 20000,
|
||||
category: "appetizer",
|
||||
isAvailable: true,
|
||||
});
|
||||
|
||||
testMenuId1 = menu1._id;
|
||||
testMenuId2 = menu2._id;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await Order.deleteMany({});
|
||||
await Meja.deleteMany({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await Menu.deleteMany({});
|
||||
await Order.deleteMany({});
|
||||
await Meja.deleteMany({});
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe("POST /createOrders", () => {
|
||||
test("harus berhasil membuat pesanan baru", async () => {
|
||||
const table = await Meja.create({
|
||||
tableNumber: 3,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const orderData = {
|
||||
tableNumber: 3,
|
||||
items: [
|
||||
{ menuId: testMenuId1, quantity: 1 },
|
||||
{ menuId: testMenuId2, quantity: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createOrders").send(orderData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
expect(response.body.data).toHaveProperty("tableNumber", 3);
|
||||
expect(response.body.data).toHaveProperty("total", 65000);
|
||||
expect(response.body.data).toHaveProperty("status", "pending");
|
||||
|
||||
const updatedTable = await Meja.findOne({ tableNumber: 3 });
|
||||
expect(updatedTable.status).toBe("reserved");
|
||||
});
|
||||
|
||||
test("harus mengembalikan error ketika meja tidak tersedia", async () => {
|
||||
const table = await Meja.create({
|
||||
tableNumber: 4,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
});
|
||||
|
||||
const orderData = {
|
||||
tableNumber: 4,
|
||||
items: [{ menuId: testMenuId1, quantity: 1 }],
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createOrders").send(orderData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Meja tidak tersedia atau sedang dipesan",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /orders", () => {
|
||||
test("harus mengembalikan daftar pesanan", async () => {
|
||||
await Meja.create({
|
||||
tableNumber: 2,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const orderData = {
|
||||
tableNumber: 2,
|
||||
items: [
|
||||
{ menuId: testMenuId1, quantity: 1 },
|
||||
{ menuId: testMenuId2, quantity: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
await request(app).post("/createOrders").send(orderData);
|
||||
|
||||
const response = await request(app).get("/orders");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
expect(response.body.data.length).toBeGreaterThan(0);
|
||||
expect(response.body.data[0]).toHaveProperty("tableNumber", 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /orders/:orderId/status", () => {
|
||||
test("harus mengubah status pesanan", async () => {
|
||||
const table = await Meja.create({
|
||||
tableNumber: 5,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
});
|
||||
|
||||
const order = await Order.create({
|
||||
tableNumber: 5,
|
||||
items: [
|
||||
{ menuId: testMenuId1, quantity: 1 },
|
||||
{ menuId: testMenuId2, quantity: 2 },
|
||||
],
|
||||
total: 65000,
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/orders/${order._id}/status`)
|
||||
.send({ status: "completed" });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
expect(response.body.data).toHaveProperty("status", "completed");
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 404 jika pesanan tidak ditemukan", async () => {
|
||||
const nonExistentOrderId = new mongoose.Types.ObjectId();
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/orders/${nonExistentOrderId}/status`)
|
||||
.send({ status: "completed" });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty("error", "Pesanan tidak ditemukan");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../../app");
|
||||
const Order = require("../../src/models/orderModel");
|
||||
const Meja = require("../../src/models/mejaModel");
|
||||
const Menu = require("../../src/models/menuModel");
|
||||
const connectDB = require("../../src/config/database");
|
||||
|
||||
describe("Module 5 - Error Handling", () => {
|
||||
let testMenuId;
|
||||
|
||||
beforeAll(async () => {
|
||||
await connectDB();
|
||||
|
||||
// Create a test menu item
|
||||
const menu = await Menu.create({
|
||||
name: "Nasi Goreng",
|
||||
description: "Nasi goreng spesial dengan telur dan ayam",
|
||||
price: 25000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
});
|
||||
|
||||
testMenuId = menu._id;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean collections before each test
|
||||
await Order.deleteMany({});
|
||||
await Meja.deleteMany({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await Menu.deleteMany({});
|
||||
await Order.deleteMany({});
|
||||
await Meja.deleteMany({});
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe("POST /createOrders - Error handling", () => {
|
||||
test("harus mengembalikan status 400 ketika meja tidak tersedia", async () => {
|
||||
await Meja.create({
|
||||
tableNumber: 7,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
customerName: "Budi Santoso",
|
||||
});
|
||||
|
||||
const orderData = {
|
||||
tableNumber: 7,
|
||||
items: [{ menuId: testMenuId, quantity: 2 }],
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createOrders").send(orderData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Meja tidak tersedia atau sedang dipesan",
|
||||
);
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 400 ketika item menu tidak valid", async () => {
|
||||
// Create available table
|
||||
await Meja.create({
|
||||
tableNumber: 8,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const invalidMenuId = new mongoose.Types.ObjectId();
|
||||
|
||||
const orderData = {
|
||||
tableNumber: 8,
|
||||
items: [{ menuId: invalidMenuId, quantity: 1 }],
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createOrders").send(orderData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body.error).toContain("Beberapa item menu tidak valid");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Global error handling", () => {
|
||||
test("harus menangani rute yang tidak ada dengan status 404", async () => {
|
||||
const response = await request(app).get("/non-existent-endpoint");
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const connectDB = require("../../src/config/database");
|
||||
const errorHandler = require("../../src/middleware/errorHandler");
|
||||
|
||||
const mockRequest = (body = {}, params = {}) => ({
|
||||
body,
|
||||
params,
|
||||
});
|
||||
|
||||
const mockResponse = () => {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
describe("Modul 1 - Unit Tests", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
console.error = jest.fn();
|
||||
});
|
||||
|
||||
describe("Error Handler Middleware", () => {
|
||||
test("harus menangani error dengan status code yang disediakan", () => {
|
||||
const err = new Error("Bad Request Error");
|
||||
err.statusCode = 400;
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
const next = jest.fn();
|
||||
|
||||
errorHandler(err, req, res, next);
|
||||
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Bad Request Error",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus menggunakan default status code 500 jika tidak disediakan", () => {
|
||||
const err = new Error("Server Error Without Status Code");
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
const next = jest.fn();
|
||||
|
||||
errorHandler(err, req, res, next);
|
||||
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Server Error Without Status Code",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Custom Error Creation", () => {
|
||||
test("harus dapat membuat custom error dengan status code", () => {
|
||||
const createCustomError = (message, statusCode) => {
|
||||
const error = new Error(message);
|
||||
error.statusCode = statusCode;
|
||||
return error;
|
||||
};
|
||||
|
||||
const notFoundError = createCustomError("Resource not found", 404);
|
||||
const validationError = createCustomError("Validation failed", 400);
|
||||
|
||||
expect(notFoundError).toBeInstanceOf(Error);
|
||||
expect(notFoundError.message).toBe("Resource not found");
|
||||
expect(notFoundError.statusCode).toBe(404);
|
||||
|
||||
expect(validationError).toBeInstanceOf(Error);
|
||||
expect(validationError.message).toBe("Validation failed");
|
||||
expect(validationError.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test("harus dapat meneruskan error ke middleware error handler", () => {
|
||||
const err = new Error("Test Error");
|
||||
err.statusCode = 422;
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
const next = jest.fn();
|
||||
|
||||
next(err);
|
||||
errorHandler(err, req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(err);
|
||||
expect(res.status).toHaveBeenCalledWith(422);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Test Error",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Express Middleware Structure", () => {
|
||||
test("harus memiliki struktur errorHandler yang benar", () => {
|
||||
expect(errorHandler).toBeInstanceOf(Function);
|
||||
expect(errorHandler.length).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
const Menu = require("../../src/models/menuModel");
|
||||
const menuController = require("../../src/controllers/menuController");
|
||||
|
||||
jest.mock("../../src/models/menuModel");
|
||||
|
||||
jest.mock("../../src/controllers/menuController", () => {
|
||||
const originalModule = jest.requireActual(
|
||||
"../../src/controllers/menuController",
|
||||
);
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
|
||||
withCallback: (promise, callback) => {
|
||||
return promise
|
||||
.then((data) => callback(null, data))
|
||||
.catch((err) => callback(err));
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("Modul 2 - Menu Controller", () => {
|
||||
let req, res;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
req = {
|
||||
body: {
|
||||
name: "Nasi Goreng",
|
||||
description: "Nasi goreng dengan telur mata sapi",
|
||||
price: 25000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
},
|
||||
params: {
|
||||
category: "main",
|
||||
},
|
||||
};
|
||||
|
||||
res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("createMenuItem", () => {
|
||||
test("harus berhasil membuat menu baru", (done) => {
|
||||
const mockSavedMenu = { ...req.body, _id: "menu123" };
|
||||
|
||||
Menu.mockImplementation(() => ({
|
||||
save: jest.fn().mockResolvedValue(mockSavedMenu),
|
||||
}));
|
||||
|
||||
res.status = jest.fn().mockReturnThis();
|
||||
res.json = jest.fn().mockImplementation(() => {
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith(mockSavedMenu);
|
||||
done();
|
||||
});
|
||||
|
||||
menuController.createMenuItem(req, res);
|
||||
});
|
||||
|
||||
test("harus menangani error saat pembuatan menu", (done) => {
|
||||
const errorMessage = "Validation error";
|
||||
|
||||
Menu.mockImplementation(() => ({
|
||||
save: jest.fn().mockRejectedValue(new Error(errorMessage)),
|
||||
}));
|
||||
|
||||
res.status = jest.fn().mockReturnThis();
|
||||
res.json = jest.fn().mockImplementation(() => {
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({ error: errorMessage });
|
||||
done();
|
||||
});
|
||||
|
||||
menuController.createMenuItem(req, res);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllMenuItems", () => {
|
||||
test("harus mengembalikan semua menu", (done) => {
|
||||
const mockItems = [
|
||||
{ name: "Nasi Goreng", price: 25000, category: "main" },
|
||||
{ name: "Sate Ayam", price: 20000, category: "appetizer" },
|
||||
];
|
||||
|
||||
Menu.find.mockResolvedValue(mockItems);
|
||||
|
||||
res.json = jest.fn().mockImplementation((data) => {
|
||||
expect(Menu.find).toHaveBeenCalledWith({});
|
||||
expect(data).toEqual(mockItems);
|
||||
done();
|
||||
return res;
|
||||
});
|
||||
|
||||
menuController.getAllMenuItems(req, res);
|
||||
});
|
||||
|
||||
test("harus menangani error saat mengambil menu", (done) => {
|
||||
const errorMessage = "Database error";
|
||||
|
||||
Menu.find.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
res.status = jest.fn().mockReturnThis();
|
||||
res.json = jest.fn().mockImplementation(() => {
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({ error: errorMessage });
|
||||
done();
|
||||
return res;
|
||||
});
|
||||
|
||||
menuController.getAllMenuItems(req, res);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMenuByCategory", () => {
|
||||
test("harus mengembalikan menu berdasarkan kategori", (done) => {
|
||||
const mockCategoryItems = [
|
||||
{ name: "Nasi Goreng", price: 25000, category: "main" },
|
||||
{ name: "Mie Goreng", price: 23000, category: "main" },
|
||||
];
|
||||
|
||||
Menu.find.mockResolvedValue(mockCategoryItems);
|
||||
|
||||
res.json = jest.fn().mockImplementation((data) => {
|
||||
expect(Menu.find).toHaveBeenCalledWith({ category: "main" });
|
||||
expect(data).toEqual(mockCategoryItems);
|
||||
done();
|
||||
return res;
|
||||
});
|
||||
|
||||
menuController.getMenuByCategory(req, res);
|
||||
});
|
||||
|
||||
test("harus mengembalikan 404 ketika kategori tidak ditemukan", (done) => {
|
||||
Menu.find.mockResolvedValue([]);
|
||||
|
||||
res.status = jest.fn().mockReturnThis();
|
||||
res.json = jest.fn().mockImplementation(() => {
|
||||
expect(Menu.find).toHaveBeenCalledWith({ category: "main" });
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: "Menu with category 'main' not found",
|
||||
});
|
||||
done();
|
||||
return res;
|
||||
});
|
||||
|
||||
menuController.getMenuByCategory(req, res);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
const Meja = require("../../src/models/mejaModel");
|
||||
const mejaController = require("../../src/controllers/mejaController");
|
||||
|
||||
const mockRequest = (body = {}, params = {}) => ({
|
||||
body,
|
||||
params,
|
||||
});
|
||||
|
||||
const mockResponse = () => {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
jest.mock("../../src/models/mejaModel");
|
||||
|
||||
describe("Modul 3 - Meja Controller", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("createMeja", () => {
|
||||
test("harus berhasil membuat meja baru", async () => {
|
||||
const mockMeja = {
|
||||
tableNumber: 5,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
};
|
||||
|
||||
Meja.create.mockResolvedValue(mockMeja);
|
||||
|
||||
const req = mockRequest({ tableNumber: 5, capacity: 4 });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.createMeja(req, res);
|
||||
|
||||
expect(Meja.create).toHaveBeenCalledWith({
|
||||
tableNumber: 5,
|
||||
capacity: 4,
|
||||
});
|
||||
expect(res.status).toHaveBeenCalledWith(201);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockMeja,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 400 ketika validasi gagal", async () => {
|
||||
const errorMessage = "Validation error";
|
||||
Meja.create.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const req = mockRequest({ tableNumber: 5 });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.createMeja(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllMeja", () => {
|
||||
test("harus mengembalikan semua data meja", async () => {
|
||||
const mockMejaList = [
|
||||
{ tableNumber: 1, capacity: 2, status: "available" },
|
||||
{ tableNumber: 2, capacity: 4, status: "reserved" },
|
||||
];
|
||||
|
||||
const mockSort = jest.fn().mockResolvedValue(mockMejaList);
|
||||
const mockFind = jest.fn().mockReturnValue({ sort: mockSort });
|
||||
Meja.find = mockFind;
|
||||
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.getAllMeja(req, res);
|
||||
|
||||
expect(Meja.find).toHaveBeenCalled();
|
||||
expect(mockSort).toHaveBeenCalledWith({ tableNumber: 1 });
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockMejaList,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 400 ketika terjadi kesalahan database", async () => {
|
||||
const errorMessage = "Database error";
|
||||
const mockSort = jest.fn().mockRejectedValue(new Error(errorMessage));
|
||||
const mockFind = jest.fn().mockReturnValue({ sort: mockSort });
|
||||
Meja.find = mockFind;
|
||||
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.getAllMeja(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reserveMeja", () => {
|
||||
test("harus berhasil mereservasi meja", async () => {
|
||||
const tableNumber = "10";
|
||||
const customerName = "Susilo Bambang";
|
||||
const mockMeja = {
|
||||
tableNumber: 10,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
customerName: "Susilo Bambang",
|
||||
};
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(mockMeja);
|
||||
|
||||
const req = mockRequest({ customerName }, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.reserveMeja(req, res);
|
||||
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber, status: "available" },
|
||||
{ status: "reserved", customerName },
|
||||
{ new: true },
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockMeja,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 400 ketika nama pelanggan tidak disediakan", async () => {
|
||||
const tableNumber = "10";
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.reserveMeja(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Nama pelanggan harus diisi",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 404 ketika meja tidak tersedia", async () => {
|
||||
const tableNumber = "10";
|
||||
const customerName = "Susilo Bambang";
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(null);
|
||||
|
||||
const req = mockRequest({ customerName }, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.reserveMeja(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Meja tidak tersedia",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelReservation", () => {
|
||||
test("harus berhasil membatalkan reservasi meja", async () => {
|
||||
const tableNumber = "15";
|
||||
const mockMeja = {
|
||||
tableNumber: 15,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
customerName: "",
|
||||
};
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(mockMeja);
|
||||
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.cancelReservation(req, res);
|
||||
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber, status: "reserved" },
|
||||
{
|
||||
status: "available",
|
||||
customerName: "",
|
||||
updatedAt: expect.any(Number),
|
||||
},
|
||||
{ new: true },
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: `Reservation for table ${tableNumber} has been cancelled`,
|
||||
data: mockMeja,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 404 ketika meja tidak ditemukan", async () => {
|
||||
const tableNumber = "99";
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(null);
|
||||
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.cancelReservation(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Table not found or not currently reserved",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 400 ketika terjadi kesalahan database", async () => {
|
||||
const tableNumber = "15";
|
||||
const errorMessage = "Database error";
|
||||
|
||||
Meja.findOneAndUpdate.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.cancelReservation(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const Order = require("../../src/models/orderModel");
|
||||
const Meja = require("../../src/models/mejaModel");
|
||||
const Menu = require("../../src/models/menuModel");
|
||||
const orderController = require("../../src/controllers/orderController");
|
||||
|
||||
const mockRequest = (body = {}, params = {}) => ({
|
||||
body,
|
||||
params,
|
||||
});
|
||||
|
||||
const mockResponse = () => {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
jest.mock("../../src/models/orderModel");
|
||||
jest.mock("../../src/models/mejaModel");
|
||||
jest.mock("../../src/models/menuModel");
|
||||
|
||||
const mockObjectId = new mongoose.Types.ObjectId();
|
||||
|
||||
describe("Modul 4 - Order Controller", () => {
|
||||
let req, res, next;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
req = {
|
||||
body: {
|
||||
tableNumber: 3,
|
||||
items: [
|
||||
{ menuId: mockObjectId.toString(), quantity: 1 },
|
||||
{ menuId: mockObjectId.toString(), quantity: 2 },
|
||||
],
|
||||
},
|
||||
params: {
|
||||
orderId: mockObjectId.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
res = mockResponse();
|
||||
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
describe("createOrder", () => {
|
||||
test("harus berhasil membuat pesanan baru", async () => {
|
||||
const mockMeja = {
|
||||
tableNumber: 3,
|
||||
status: "available",
|
||||
};
|
||||
|
||||
const mockMenuItems = [
|
||||
{ _id: mockObjectId, price: 25000 },
|
||||
{ _id: mockObjectId, price: 20000 },
|
||||
];
|
||||
|
||||
const mockSavedOrder = {
|
||||
_id: mockObjectId,
|
||||
tableNumber: 3,
|
||||
items: req.body.items,
|
||||
total: 65000,
|
||||
status: "pending",
|
||||
};
|
||||
|
||||
Meja.findOne.mockResolvedValue(mockMeja);
|
||||
Menu.find.mockResolvedValue(mockMenuItems);
|
||||
Order.prototype.save = jest.fn().mockResolvedValue(mockSavedOrder);
|
||||
Meja.findOneAndUpdate.mockResolvedValue({ status: "reserved" });
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(Meja.findOne).toHaveBeenCalledWith({
|
||||
tableNumber: 3,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
expect(Menu.find).toHaveBeenCalled();
|
||||
expect(Order.prototype.save).toHaveBeenCalled();
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber: 3 },
|
||||
{ status: "reserved" },
|
||||
);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(201);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockSavedOrder,
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("harus menangani error ketika meja tidak tersedia", async () => {
|
||||
Meja.findOne.mockResolvedValue(null);
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(Meja.findOne).toHaveBeenCalledWith({
|
||||
tableNumber: 3,
|
||||
status: "available",
|
||||
});
|
||||
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(next.mock.calls[0][0].message).toBe(
|
||||
"Meja tidak tersedia atau sedang dipesan",
|
||||
);
|
||||
expect(next.mock.calls[0][0].statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test("harus menangani error ketika item menu tidak valid", async () => {
|
||||
Meja.findOne.mockResolvedValue({ tableNumber: 3, status: "available" });
|
||||
Menu.find.mockResolvedValue([]);
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(Menu.find).toHaveBeenCalled();
|
||||
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(next.mock.calls[0][0].message).toBe(
|
||||
"Beberapa item menu tidak valid",
|
||||
);
|
||||
expect(next.mock.calls[0][0].statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllOrders", () => {
|
||||
test("harus mengembalikan semua pesanan", async () => {
|
||||
const mockOrders = [
|
||||
{ _id: mockObjectId, tableNumber: 1, total: 50000, status: "pending" },
|
||||
{
|
||||
_id: mockObjectId,
|
||||
tableNumber: 2,
|
||||
total: 75000,
|
||||
status: "completed",
|
||||
},
|
||||
];
|
||||
|
||||
const mockSort = jest.fn().mockResolvedValue(mockOrders);
|
||||
Order.find = jest.fn().mockReturnValue({ sort: mockSort });
|
||||
|
||||
await orderController.getAllOrders(req, res);
|
||||
|
||||
expect(Order.find).toHaveBeenCalled();
|
||||
expect(mockSort).toHaveBeenCalledWith({ createdAt: -1 });
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockOrders,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus menangani error saat mengambil pesanan", async () => {
|
||||
const errorMessage = "Database error";
|
||||
Order.find = jest.fn().mockImplementation(() => {
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
|
||||
await orderController.getAllOrders(req, res);
|
||||
|
||||
expect(Order.find).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateOrderStatus", () => {
|
||||
test("harus berhasil mengupdate status pesanan", async () => {
|
||||
req.body = { status: "completed" };
|
||||
|
||||
const mockUpdatedOrder = {
|
||||
_id: mockObjectId,
|
||||
tableNumber: 3,
|
||||
status: "completed",
|
||||
};
|
||||
|
||||
Order.findByIdAndUpdate.mockResolvedValue(mockUpdatedOrder);
|
||||
Meja.findOneAndUpdate.mockResolvedValue({ status: "available" });
|
||||
|
||||
await orderController.updateOrderStatus(req, res);
|
||||
|
||||
expect(Order.findByIdAndUpdate).toHaveBeenCalledWith(
|
||||
mockObjectId.toString(),
|
||||
{ status: "completed" },
|
||||
{ new: true },
|
||||
);
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber: 3 },
|
||||
{ status: "available" },
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockUpdatedOrder,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan 404 ketika pesanan tidak ditemukan", async () => {
|
||||
req.body = { status: "completed" };
|
||||
|
||||
Order.findByIdAndUpdate.mockResolvedValue(null);
|
||||
|
||||
await orderController.updateOrderStatus(req, res);
|
||||
|
||||
expect(Order.findByIdAndUpdate).toHaveBeenCalledWith(
|
||||
mockObjectId.toString(),
|
||||
{ status: "completed" },
|
||||
{ new: true },
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Pesanan tidak ditemukan",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus menangani error saat mengupdate status", async () => {
|
||||
req.body = { status: "completed" };
|
||||
|
||||
const errorMessage = "Database error";
|
||||
Order.findByIdAndUpdate.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
await orderController.updateOrderStatus(req, res);
|
||||
|
||||
expect(Order.findByIdAndUpdate).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
const errorHandler = require("../../src/middleware/errorHandler");
|
||||
const mongoose = require("mongoose");
|
||||
const Order = require("../../src/models/orderModel");
|
||||
const Meja = require("../../src/models/mejaModel");
|
||||
const Menu = require("../../src/models/menuModel");
|
||||
const orderController = require("../../src/controllers/orderController");
|
||||
|
||||
const mockRequest = (body = {}, params = {}) => ({
|
||||
body,
|
||||
params,
|
||||
});
|
||||
|
||||
const mockResponse = () => {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
jest.mock("../../src/models/orderModel");
|
||||
jest.mock("../../src/models/mejaModel");
|
||||
jest.mock("../../src/models/menuModel");
|
||||
|
||||
const mockObjectId = new mongoose.Types.ObjectId();
|
||||
|
||||
describe("Modul 5 - Error Handling", () => {
|
||||
let req, res, next;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
console.error = jest.fn();
|
||||
|
||||
req = {
|
||||
body: {
|
||||
tableNumber: 7,
|
||||
items: [{ menuId: mockObjectId.toString(), quantity: 2 }],
|
||||
},
|
||||
params: {},
|
||||
};
|
||||
|
||||
res = mockResponse();
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
describe("Error Handler Middleware", () => {
|
||||
test("harus menangani error dengan status code dari error", () => {
|
||||
const error = new Error("Validation error");
|
||||
error.statusCode = 400;
|
||||
|
||||
errorHandler(error, req, res, next);
|
||||
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Validation error",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus menangani error tanpa status code dengan default 500", () => {
|
||||
const error = new Error("Server error");
|
||||
|
||||
errorHandler(error, req, res, next);
|
||||
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Server error",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Order Controller Error Handling", () => {
|
||||
test("harus menangani error meja tidak tersedia dengan status 400", async () => {
|
||||
Meja.findOne.mockResolvedValue(null);
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(next.mock.calls[0][0].message).toBe(
|
||||
"Meja tidak tersedia atau sedang dipesan",
|
||||
);
|
||||
expect(next.mock.calls[0][0].statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test("harus menangani error menu tidak valid dengan status 400", async () => {
|
||||
Meja.findOne.mockResolvedValue({ tableNumber: 7, status: "available" });
|
||||
Menu.find.mockResolvedValue([]);
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(next.mock.calls[0][0].message).toBe(
|
||||
"Beberapa item menu tidak valid",
|
||||
);
|
||||
expect(next.mock.calls[0][0].statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test("harus menangani exception dengan middleware error", async () => {
|
||||
const errorMessage = "Database connection failed";
|
||||
Meja.findOne.mockImplementation(() => {
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(next.mock.calls[0][0].message).toBe(errorMessage);
|
||||
});
|
||||
|
||||
test("harus mengubah error menjadi format error response", () => {
|
||||
const error = new Error("Custom error");
|
||||
error.statusCode = 422;
|
||||
|
||||
errorHandler(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(422);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Custom error",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Custom Error Creation", () => {
|
||||
test("harus membuat error dengan statusCode yang tepat", async () => {
|
||||
const createError = (message, statusCode) => {
|
||||
const error = new Error(message);
|
||||
error.statusCode = statusCode;
|
||||
return error;
|
||||
};
|
||||
|
||||
const error = createError("Resource not found", 404);
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.message).toBe("Resource not found");
|
||||
expect(error.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../../app");
|
||||
const connectDB = require("../../src/config/database");
|
||||
|
||||
describe("Module 1 - Database & Basic API", () => {
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => { });
|
||||
|
||||
await connectDB();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe("Database Connection", () => {
|
||||
test("harus berhasil terhubung dengan MongoDB", () => {
|
||||
const connectionState = mongoose.connection.readyState;
|
||||
expect(connectionState).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test API Route", () => {
|
||||
test("harus mengembalikan format respons yang benar dari rute pengujian", async () => {
|
||||
const response = await request(app).get("/test");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
message: "Welcome to Restaurant Reservation API",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../../app");
|
||||
const Menu = require("../../src/models/menuModel");
|
||||
const connectDB = require("../../src/config/database");
|
||||
|
||||
describe("Module 2 - Menu API", () => {
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => { });
|
||||
|
||||
await connectDB();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await Menu.deleteMany({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe("POST /createMenu", () => {
|
||||
test("harus berhasil membuat item menu baru", async () => {
|
||||
const menuItem = {
|
||||
name: "Nasi Goreng",
|
||||
description: "Nasi goreng dengan telur mata sapi dan ayam kampung",
|
||||
price: 25000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createMenu").send(menuItem);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty("name", "Nasi Goreng");
|
||||
expect(response.body).toHaveProperty("price", 25000);
|
||||
expect(response.body).toHaveProperty("category", "main");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /menu", () => {
|
||||
test("harus mengembalikan seluruh item menu", async () => {
|
||||
const firstItem = await Menu.create({
|
||||
name: "Nasi Putih",
|
||||
price: 5000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
});
|
||||
const secondItem = await Menu.create({
|
||||
name: "Kerupuk Udang",
|
||||
price: 3000,
|
||||
category: "appetizer",
|
||||
isAvailable: true,
|
||||
});
|
||||
|
||||
const response = await request(app).get("/menu");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.length).toBeGreaterThanOrEqual(2);
|
||||
const nasiInResponse = response.body.find(
|
||||
(item) => item._id === firstItem._id.toString(),
|
||||
);
|
||||
const kerupukInResponse = response.body.find(
|
||||
(item) => item._id === secondItem._id.toString(),
|
||||
);
|
||||
|
||||
expect(nasiInResponse).toBeDefined();
|
||||
expect(kerupukInResponse).toBeDefined();
|
||||
expect(nasiInResponse).toHaveProperty("name", "Nasi Putih");
|
||||
expect(kerupukInResponse).toHaveProperty("name", "Kerupuk Udang");
|
||||
});
|
||||
|
||||
test("harus mengembalikan array kosong jika tidak ada item menu", async () => {
|
||||
await Menu.deleteMany({});
|
||||
|
||||
const response = await request(app).get("/menu");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /menu/:category", () => {
|
||||
test("harus mengembalikan item menu berdasarkan kategori yang ditentukan", async () => {
|
||||
await Menu.deleteMany({});
|
||||
|
||||
await Menu.create([
|
||||
{
|
||||
name: "Nasi Rames",
|
||||
price: 20000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
},
|
||||
{
|
||||
name: "Gado-gado",
|
||||
price: 15000,
|
||||
category: "appetizer",
|
||||
isAvailable: true,
|
||||
},
|
||||
{
|
||||
name: "Soto Ayam",
|
||||
price: 18000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const response = await request(app).get("/menu/main");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(2);
|
||||
|
||||
response.body.forEach((item) => {
|
||||
expect(item.category).toBe("main");
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 404 jika kategori tidak ditemukan", async () => {
|
||||
const response = await request(app).get("/menu/bukanmakanan");
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Menu with category 'bukanmakanan' not found",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../../app");
|
||||
const Meja = require("../../src/models/mejaModel");
|
||||
const connectDB = require("../../src/config/database");
|
||||
|
||||
describe("Module 3 - Meja/Table API", () => {
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => { });
|
||||
jest.spyOn(console, 'error').mockImplementation(() => { });
|
||||
|
||||
await connectDB();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await Meja.deleteMany({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe("POST /createMeja", () => {
|
||||
test("harus berhasil membuat meja baru", async () => {
|
||||
const newTable = {
|
||||
tableNumber: 5,
|
||||
capacity: 4,
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createMeja").send(newTable);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
expect(response.body.data).toHaveProperty("tableNumber", 5);
|
||||
expect(response.body.data).toHaveProperty("capacity", 4);
|
||||
expect(response.body.data).toHaveProperty("status", "available");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /meja", () => {
|
||||
test("harus mengembalikan semua meja", async () => {
|
||||
await Meja.create({ tableNumber: 1, capacity: 2 });
|
||||
await Meja.create({ tableNumber: 2, capacity: 4 });
|
||||
|
||||
const response = await request(app).get("/meja");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
expect(response.body.data.length).toBe(2);
|
||||
|
||||
const returnedTables = response.body.data;
|
||||
const returnedTable1 = returnedTables.find((t) => t.tableNumber === 1);
|
||||
const returnedTable2 = returnedTables.find((t) => t.tableNumber === 2);
|
||||
|
||||
expect(returnedTable1 || returnedTable2).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /meja/:tableNumber/reserve", () => {
|
||||
test("harus berhasil memesan meja yang tersedia", async () => {
|
||||
const tableNumber = 10;
|
||||
|
||||
await Meja.create({
|
||||
tableNumber: tableNumber,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const tableBeforeReserve = await Meja.findOne({ tableNumber });
|
||||
expect(tableBeforeReserve).toBeDefined();
|
||||
expect(tableBeforeReserve.status).toBe("available");
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/meja/${tableNumber}/reserve`)
|
||||
.send({ customerName: "Susilo Bambang" });
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Unexpected status code:", response.status);
|
||||
console.log("Response body:", response.body);
|
||||
}
|
||||
|
||||
expect(response.body).toHaveProperty("success");
|
||||
if (response.body.success) {
|
||||
expect(response.body.data).toHaveProperty("customerName");
|
||||
expect(response.body.data).toHaveProperty("status", "reserved");
|
||||
expect(response.body.data.customerName).toBe("Susilo Bambang");
|
||||
}
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 400 jika nama pelanggan tidak disediakan", async () => {
|
||||
await Meja.create({
|
||||
tableNumber: 11,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const response = await request(app).put("/meja/11/reserve").send({});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Nama pelanggan harus diisi",
|
||||
);
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 404 jika meja tidak tersedia", async () => {
|
||||
await Meja.create({
|
||||
tableNumber: 12,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
customerName: "Dewi Sartika",
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put("/meja/12/reserve")
|
||||
.send({ customerName: "Budi Sudarsono" });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty("error", "Meja tidak tersedia");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /meja/:tableNumber/cancel", () => {
|
||||
test("harus berhasil membatalkan pemesanan meja", async () => {
|
||||
// Create a reserved table first with correct status and customerName
|
||||
await Meja.create({
|
||||
tableNumber: 15,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
customerName: "Anita Wijaya",
|
||||
});
|
||||
|
||||
// Verify that the table was created with the right status
|
||||
const createdTable = await Meja.findOne({ tableNumber: 15 });
|
||||
expect(createdTable).toBeDefined();
|
||||
expect(createdTable.status).toBe("reserved");
|
||||
|
||||
const response = await request(app).put("/meja/15/cancel").send({});
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Unexpected status code for cancel:", response.status);
|
||||
console.log("Cancel response body:", response.body);
|
||||
}
|
||||
|
||||
expect(response.body).toHaveProperty("success");
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 404 jika meja tidak ditemukan", async () => {
|
||||
const response = await request(app).put("/meja/99/cancel").send({});
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Table not found or not currently reserved",
|
||||
);
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 404 jika meja tidak dalam status dipesan", async () => {
|
||||
// Create an available table first
|
||||
await Meja.create({
|
||||
tableNumber: 16,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const response = await request(app).put("/meja/16/cancel").send({});
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Table not found or not currently reserved",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../../app");
|
||||
const Order = require("../../src/models/orderModel");
|
||||
const Meja = require("../../src/models/mejaModel");
|
||||
const Menu = require("../../src/models/menuModel");
|
||||
const connectDB = require("../../src/config/database");
|
||||
|
||||
describe("Module 4 - Order API", () => {
|
||||
let testMenuId1, testMenuId2;
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => { });
|
||||
jest.spyOn(console, 'error').mockImplementation(() => { });
|
||||
|
||||
await connectDB();
|
||||
|
||||
const menu1 = await Menu.create({
|
||||
name: "Nasi Goreng Spesial",
|
||||
description: "Nasi goreng dengan telur, ayam, dan sayuran segar",
|
||||
price: 25000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
});
|
||||
|
||||
const menu2 = await Menu.create({
|
||||
name: "Sate Ayam",
|
||||
description: "Sate ayam dengan bumbu kacang khas Indonesia",
|
||||
price: 20000,
|
||||
category: "appetizer",
|
||||
isAvailable: true,
|
||||
});
|
||||
|
||||
testMenuId1 = menu1._id;
|
||||
testMenuId2 = menu2._id;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await Order.deleteMany({});
|
||||
await Meja.deleteMany({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await Menu.deleteMany({});
|
||||
await Order.deleteMany({});
|
||||
await Meja.deleteMany({});
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe("POST /createOrders", () => {
|
||||
test("harus berhasil membuat pesanan baru", async () => {
|
||||
const table = await Meja.create({
|
||||
tableNumber: 3,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const orderData = {
|
||||
tableNumber: 3,
|
||||
items: [
|
||||
{ menuId: testMenuId1, quantity: 1 },
|
||||
{ menuId: testMenuId2, quantity: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createOrders").send(orderData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
expect(response.body.data).toHaveProperty("tableNumber", 3);
|
||||
expect(response.body.data).toHaveProperty("total", 65000);
|
||||
expect(response.body.data).toHaveProperty("status", "pending");
|
||||
|
||||
const updatedTable = await Meja.findOne({ tableNumber: 3 });
|
||||
expect(updatedTable.status).toBe("reserved");
|
||||
});
|
||||
|
||||
test("harus mengembalikan error ketika meja tidak tersedia", async () => {
|
||||
const table = await Meja.create({
|
||||
tableNumber: 4,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
});
|
||||
|
||||
const orderData = {
|
||||
tableNumber: 4,
|
||||
items: [{ menuId: testMenuId1, quantity: 1 }],
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createOrders").send(orderData);
|
||||
|
||||
console.log(response.status);
|
||||
console.log(response.body);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Meja tidak tersedia atau sedang dipesan",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /orders", () => {
|
||||
test("harus mengembalikan daftar pesanan", async () => {
|
||||
await Meja.create({
|
||||
tableNumber: 2,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const orderData = {
|
||||
tableNumber: 2,
|
||||
items: [
|
||||
{ menuId: testMenuId1, quantity: 1 },
|
||||
{ menuId: testMenuId2, quantity: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
await request(app).post("/createOrders").send(orderData);
|
||||
|
||||
const response = await request(app).get("/orders");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
expect(response.body.data.length).toBeGreaterThan(0);
|
||||
expect(response.body.data[0]).toHaveProperty("tableNumber", 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /orders/:orderId/status", () => {
|
||||
test("harus mengubah status pesanan", async () => {
|
||||
const table = await Meja.create({
|
||||
tableNumber: 5,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
});
|
||||
|
||||
const order = await Order.create({
|
||||
tableNumber: 5,
|
||||
items: [
|
||||
{ menuId: testMenuId1, quantity: 1 },
|
||||
{ menuId: testMenuId2, quantity: 2 },
|
||||
],
|
||||
total: 65000,
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/orders/${order._id}/status`)
|
||||
.send({ status: "completed" });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty("success", true);
|
||||
expect(response.body.data).toHaveProperty("status", "completed");
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 404 jika pesanan tidak ditemukan", async () => {
|
||||
const nonExistentOrderId = new mongoose.Types.ObjectId();
|
||||
|
||||
const response = await request(app)
|
||||
.put(`/orders/${nonExistentOrderId}/status`)
|
||||
.send({ status: "completed" });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty("error", "Pesanan tidak ditemukan");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const request = require("supertest");
|
||||
const { app } = require("../../app");
|
||||
const Order = require("../../src/models/orderModel");
|
||||
const Meja = require("../../src/models/mejaModel");
|
||||
const Menu = require("../../src/models/menuModel");
|
||||
const connectDB = require("../../src/config/database");
|
||||
|
||||
describe("Module 5 - Error Handling", () => {
|
||||
let testMenuId;
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => { });
|
||||
jest.spyOn(console, 'error').mockImplementation(() => { });
|
||||
|
||||
await connectDB();
|
||||
|
||||
// Create a test menu item
|
||||
const menu = await Menu.create({
|
||||
name: "Nasi Goreng",
|
||||
description: "Nasi goreng spesial dengan telur dan ayam",
|
||||
price: 25000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
});
|
||||
|
||||
testMenuId = menu._id;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean collections before each test
|
||||
await Order.deleteMany({});
|
||||
await Meja.deleteMany({});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await Menu.deleteMany({});
|
||||
await Order.deleteMany({});
|
||||
await Meja.deleteMany({});
|
||||
await mongoose.connection.close();
|
||||
});
|
||||
|
||||
describe("POST /createOrders - Error handling", () => {
|
||||
test("harus mengembalikan status 400 ketika meja tidak tersedia", async () => {
|
||||
await Meja.create({
|
||||
tableNumber: 7,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
customerName: "Budi Santoso",
|
||||
});
|
||||
|
||||
const orderData = {
|
||||
tableNumber: 7,
|
||||
items: [{ menuId: testMenuId, quantity: 2 }],
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createOrders").send(orderData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body).toHaveProperty(
|
||||
"error",
|
||||
"Meja tidak tersedia atau sedang dipesan",
|
||||
);
|
||||
});
|
||||
|
||||
test("harus mengembalikan status 400 ketika item menu tidak valid", async () => {
|
||||
// Create available table
|
||||
await Meja.create({
|
||||
tableNumber: 8,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
const invalidMenuId = new mongoose.Types.ObjectId();
|
||||
|
||||
const orderData = {
|
||||
tableNumber: 8,
|
||||
items: [{ menuId: invalidMenuId, quantity: 1 }],
|
||||
};
|
||||
|
||||
const response = await request(app).post("/createOrders").send(orderData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body).toHaveProperty("success", false);
|
||||
expect(response.body.error).toContain("Beberapa item menu tidak valid");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Global error handling", () => {
|
||||
test("harus menangani rute yang tidak ada dengan status 404", async () => {
|
||||
const response = await request(app).get("/non-existent-endpoint");
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const connectDB = require("../../src/config/database");
|
||||
const errorHandler = require("../../src/middleware/errorHandler");
|
||||
|
||||
const mockRequest = (body = {}, params = {}) => ({
|
||||
body,
|
||||
params,
|
||||
});
|
||||
|
||||
const mockResponse = () => {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
describe("Modul 1 - Unit Tests", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => { });
|
||||
jest.clearAllMocks();
|
||||
console.error = jest.fn();
|
||||
});
|
||||
|
||||
describe("Error Handler Middleware", () => {
|
||||
test("harus menangani error dengan status code yang disediakan", () => {
|
||||
const err = new Error("Bad Request Error");
|
||||
err.statusCode = 400;
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
const next = jest.fn();
|
||||
|
||||
errorHandler(err, req, res, next);
|
||||
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Bad Request Error",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus menggunakan default status code 500 jika tidak disediakan", () => {
|
||||
const err = new Error("Server Error Without Status Code");
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
const next = jest.fn();
|
||||
|
||||
errorHandler(err, req, res, next);
|
||||
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Server Error Without Status Code",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Custom Error Creation", () => {
|
||||
test("harus dapat membuat custom error dengan status code", () => {
|
||||
const createCustomError = (message, statusCode) => {
|
||||
const error = new Error(message);
|
||||
error.statusCode = statusCode;
|
||||
return error;
|
||||
};
|
||||
|
||||
const notFoundError = createCustomError("Resource not found", 404);
|
||||
const validationError = createCustomError("Validation failed", 400);
|
||||
|
||||
expect(notFoundError).toBeInstanceOf(Error);
|
||||
expect(notFoundError.message).toBe("Resource not found");
|
||||
expect(notFoundError.statusCode).toBe(404);
|
||||
|
||||
expect(validationError).toBeInstanceOf(Error);
|
||||
expect(validationError.message).toBe("Validation failed");
|
||||
expect(validationError.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test("harus dapat meneruskan error ke middleware error handler", () => {
|
||||
const err = new Error("Test Error");
|
||||
err.statusCode = 422;
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
const next = jest.fn();
|
||||
|
||||
next(err);
|
||||
errorHandler(err, req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(err);
|
||||
expect(res.status).toHaveBeenCalledWith(422);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Test Error",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Express Middleware Structure", () => {
|
||||
test("harus memiliki struktur errorHandler yang benar", () => {
|
||||
expect(errorHandler).toBeInstanceOf(Function);
|
||||
expect(errorHandler.length).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
const Menu = require("../../src/models/menuModel");
|
||||
const menuController = require("../../src/controllers/menuController");
|
||||
|
||||
jest.mock("../../src/models/menuModel");
|
||||
|
||||
jest.mock("../../src/controllers/menuController", () => {
|
||||
const originalModule = jest.requireActual(
|
||||
"../../src/controllers/menuController",
|
||||
);
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
|
||||
withCallback: (promise, callback) => {
|
||||
return promise
|
||||
.then((data) => callback(null, data))
|
||||
.catch((err) => callback(err));
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("Modul 2 - Menu Controller", () => {
|
||||
let req, res;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => { });
|
||||
jest.clearAllMocks();
|
||||
|
||||
req = {
|
||||
body: {
|
||||
name: "Nasi Goreng",
|
||||
description: "Nasi goreng dengan telur mata sapi",
|
||||
price: 25000,
|
||||
category: "main",
|
||||
isAvailable: true,
|
||||
},
|
||||
params: {
|
||||
category: "main",
|
||||
},
|
||||
};
|
||||
|
||||
res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("createMenuItem", () => {
|
||||
test("harus berhasil membuat menu baru", (done) => {
|
||||
const mockSavedMenu = { ...req.body, _id: "menu123" };
|
||||
|
||||
Menu.mockImplementation(() => ({
|
||||
save: jest.fn().mockResolvedValue(mockSavedMenu),
|
||||
}));
|
||||
|
||||
res.status = jest.fn().mockReturnThis();
|
||||
res.json = jest.fn().mockImplementation(() => {
|
||||
expect(res.status).toHaveBeenCalledWith(201);
|
||||
expect(res.json).toHaveBeenCalledWith(mockSavedMenu);
|
||||
done();
|
||||
});
|
||||
|
||||
menuController.createMenuItem(req, res);
|
||||
});
|
||||
|
||||
test("harus menangani error saat pembuatan menu", (done) => {
|
||||
const errorMessage = "Validation error";
|
||||
|
||||
Menu.mockImplementation(() => ({
|
||||
save: jest.fn().mockRejectedValue(new Error(errorMessage)),
|
||||
}));
|
||||
|
||||
res.status = jest.fn().mockReturnThis();
|
||||
res.json = jest.fn().mockImplementation(() => {
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({ error: errorMessage });
|
||||
done();
|
||||
});
|
||||
|
||||
menuController.createMenuItem(req, res);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllMenuItems", () => {
|
||||
test("harus mengembalikan semua menu", (done) => {
|
||||
const mockItems = [
|
||||
{ name: "Nasi Goreng", price: 25000, category: "main" },
|
||||
{ name: "Sate Ayam", price: 20000, category: "appetizer" },
|
||||
];
|
||||
|
||||
Menu.find.mockResolvedValue(mockItems);
|
||||
|
||||
res.json = jest.fn().mockImplementation((data) => {
|
||||
expect(Menu.find).toHaveBeenCalledWith({});
|
||||
expect(data).toEqual(mockItems);
|
||||
done();
|
||||
return res;
|
||||
});
|
||||
|
||||
menuController.getAllMenuItems(req, res);
|
||||
});
|
||||
|
||||
test("harus menangani error saat mengambil menu", (done) => {
|
||||
const errorMessage = "Database error";
|
||||
|
||||
Menu.find.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
res.status = jest.fn().mockReturnThis();
|
||||
res.json = jest.fn().mockImplementation(() => {
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({ error: errorMessage });
|
||||
done();
|
||||
return res;
|
||||
});
|
||||
|
||||
menuController.getAllMenuItems(req, res);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMenuByCategory", () => {
|
||||
test("harus mengembalikan menu berdasarkan kategori", (done) => {
|
||||
const mockCategoryItems = [
|
||||
{ name: "Nasi Goreng", price: 25000, category: "main" },
|
||||
{ name: "Mie Goreng", price: 23000, category: "main" },
|
||||
];
|
||||
|
||||
Menu.find.mockResolvedValue(mockCategoryItems);
|
||||
|
||||
res.json = jest.fn().mockImplementation((data) => {
|
||||
expect(Menu.find).toHaveBeenCalledWith({ category: "main" });
|
||||
expect(data).toEqual(mockCategoryItems);
|
||||
done();
|
||||
return res;
|
||||
});
|
||||
|
||||
menuController.getMenuByCategory(req, res);
|
||||
});
|
||||
|
||||
test("harus mengembalikan 404 ketika kategori tidak ditemukan", (done) => {
|
||||
Menu.find.mockResolvedValue([]);
|
||||
|
||||
res.status = jest.fn().mockReturnThis();
|
||||
res.json = jest.fn().mockImplementation(() => {
|
||||
expect(Menu.find).toHaveBeenCalledWith({ category: "main" });
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: "Menu with category 'main' not found",
|
||||
});
|
||||
done();
|
||||
return res;
|
||||
});
|
||||
|
||||
menuController.getMenuByCategory(req, res);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
const Meja = require("../../src/models/mejaModel");
|
||||
const mejaController = require("../../src/controllers/mejaController");
|
||||
|
||||
const mockRequest = (body = {}, params = {}) => ({
|
||||
body,
|
||||
params,
|
||||
});
|
||||
|
||||
const mockResponse = () => {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
jest.mock("../../src/models/mejaModel");
|
||||
|
||||
describe("Modul 3 - Meja Controller", () => {
|
||||
afterEach(() => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => { });
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("createMeja", () => {
|
||||
test("harus berhasil membuat meja baru", async () => {
|
||||
const mockMeja = {
|
||||
tableNumber: 5,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
};
|
||||
|
||||
Meja.create.mockResolvedValue(mockMeja);
|
||||
|
||||
const req = mockRequest({ tableNumber: 5, capacity: 4 });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.createMeja(req, res);
|
||||
|
||||
expect(Meja.create).toHaveBeenCalledWith({
|
||||
tableNumber: 5,
|
||||
capacity: 4,
|
||||
});
|
||||
expect(res.status).toHaveBeenCalledWith(201);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockMeja,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 400 ketika validasi gagal", async () => {
|
||||
const errorMessage = "Validation error";
|
||||
Meja.create.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const req = mockRequest({ tableNumber: 5 });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.createMeja(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllMeja", () => {
|
||||
test("harus mengembalikan semua data meja", async () => {
|
||||
const mockMejaList = [
|
||||
{ tableNumber: 1, capacity: 2, status: "available" },
|
||||
{ tableNumber: 2, capacity: 4, status: "reserved" },
|
||||
];
|
||||
|
||||
const mockSort = jest.fn().mockResolvedValue(mockMejaList);
|
||||
const mockThen = jest.fn().mockImplementation(callback => Promise.resolve(callback(mockMejaList)));
|
||||
const mockFindResult = { sort: mockSort, then: mockThen };
|
||||
|
||||
Meja.find = jest.fn().mockReturnValue(mockFindResult);
|
||||
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.getAllMeja(req, res);
|
||||
|
||||
expect(Meja.find).toHaveBeenCalled();
|
||||
// Don't test for sort being called specifically, as it may not be in all implementations
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockMejaList,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 400 ketika terjadi kesalahan database", async () => {
|
||||
const errorMessage = "Database error";
|
||||
|
||||
// Create a more flexible mock that handles both implementations
|
||||
const mockFindResult = {};
|
||||
|
||||
// For implementations with sort
|
||||
const mockSort = jest.fn().mockRejectedValue(new Error(errorMessage));
|
||||
mockFindResult.sort = mockSort;
|
||||
|
||||
// For implementations without sort
|
||||
mockFindResult.then = jest.fn().mockImplementation(() =>
|
||||
Promise.reject(new Error(errorMessage))
|
||||
);
|
||||
|
||||
Meja.find = jest.fn().mockReturnValue(mockFindResult);
|
||||
|
||||
const req = mockRequest();
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.getAllMeja(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reserveMeja", () => {
|
||||
test("harus berhasil mereservasi meja", async () => {
|
||||
const tableNumber = "10";
|
||||
const customerName = "Susilo Bambang";
|
||||
const mockMeja = {
|
||||
tableNumber: 10,
|
||||
capacity: 4,
|
||||
status: "reserved",
|
||||
customerName: "Susilo Bambang",
|
||||
};
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(mockMeja);
|
||||
|
||||
const req = mockRequest({ customerName }, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.reserveMeja(req, res);
|
||||
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber, status: "available" },
|
||||
{ status: "reserved", customerName },
|
||||
{ new: true },
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockMeja,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 400 ketika nama pelanggan tidak disediakan", async () => {
|
||||
const tableNumber = "10";
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.reserveMeja(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Nama pelanggan harus diisi",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 404 ketika meja tidak tersedia", async () => {
|
||||
const tableNumber = "10";
|
||||
const customerName = "Susilo Bambang";
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(null);
|
||||
|
||||
const req = mockRequest({ customerName }, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.reserveMeja(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Meja tidak tersedia",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelReservation", () => {
|
||||
test("harus berhasil membatalkan reservasi meja", async () => {
|
||||
const tableNumber = "15";
|
||||
const mockMeja = {
|
||||
tableNumber: 15,
|
||||
capacity: 4,
|
||||
status: "available",
|
||||
customerName: "",
|
||||
};
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(mockMeja);
|
||||
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.cancelReservation(req, res);
|
||||
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber, status: "reserved" },
|
||||
{
|
||||
status: "available",
|
||||
customerName: "",
|
||||
updatedAt: expect.any(Number),
|
||||
},
|
||||
{ new: true },
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
message: `Reservation for table ${tableNumber} has been cancelled`,
|
||||
data: mockMeja,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 404 ketika meja tidak ditemukan", async () => {
|
||||
const tableNumber = "99";
|
||||
|
||||
Meja.findOneAndUpdate.mockResolvedValue(null);
|
||||
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.cancelReservation(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Table not found or not currently reserved",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan error 400 ketika terjadi kesalahan database", async () => {
|
||||
const tableNumber = "15";
|
||||
const errorMessage = "Database error";
|
||||
|
||||
Meja.findOneAndUpdate.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const req = mockRequest({}, { tableNumber });
|
||||
const res = mockResponse();
|
||||
|
||||
await mejaController.cancelReservation(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
const mongoose = require("mongoose");
|
||||
const Order = require("../../src/models/orderModel");
|
||||
const Meja = require("../../src/models/mejaModel");
|
||||
const Menu = require("../../src/models/menuModel");
|
||||
const orderController = require("../../src/controllers/orderController");
|
||||
|
||||
const mockRequest = (body = {}, params = {}) => ({
|
||||
body,
|
||||
params,
|
||||
});
|
||||
|
||||
const mockResponse = () => {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
jest.mock("../../src/models/orderModel");
|
||||
jest.mock("../../src/models/mejaModel");
|
||||
jest.mock("../../src/models/menuModel");
|
||||
|
||||
const mockObjectId = new mongoose.Types.ObjectId();
|
||||
|
||||
describe("Modul 4 - Order Controller", () => {
|
||||
let req, res, next;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => { });
|
||||
jest.clearAllMocks();
|
||||
|
||||
req = {
|
||||
body: {
|
||||
tableNumber: 3,
|
||||
items: [
|
||||
{ menuId: mockObjectId.toString(), quantity: 1 },
|
||||
{ menuId: mockObjectId.toString(), quantity: 2 },
|
||||
],
|
||||
},
|
||||
params: {
|
||||
orderId: mockObjectId.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
res = mockResponse();
|
||||
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
describe("createOrder", () => {
|
||||
test("harus berhasil membuat pesanan baru", async () => {
|
||||
const mockMeja = {
|
||||
tableNumber: 3,
|
||||
status: "available",
|
||||
};
|
||||
|
||||
const mockMenuItems = [
|
||||
{ _id: mockObjectId, price: 25000 },
|
||||
{ _id: mockObjectId, price: 20000 },
|
||||
];
|
||||
|
||||
const mockSavedOrder = {
|
||||
_id: mockObjectId,
|
||||
tableNumber: 3,
|
||||
items: req.body.items,
|
||||
total: 65000,
|
||||
status: "pending",
|
||||
};
|
||||
|
||||
Meja.findOne.mockResolvedValue(mockMeja);
|
||||
Menu.find.mockResolvedValue(mockMenuItems);
|
||||
Order.prototype.save = jest.fn().mockResolvedValue(mockSavedOrder);
|
||||
Meja.findOneAndUpdate.mockResolvedValue({ status: "reserved" });
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(Meja.findOne).toHaveBeenCalledWith({
|
||||
tableNumber: 3,
|
||||
status: "available",
|
||||
});
|
||||
|
||||
expect(Menu.find).toHaveBeenCalled();
|
||||
expect(Order.prototype.save).toHaveBeenCalled();
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber: 3 },
|
||||
{ status: "reserved" },
|
||||
);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(201);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockSavedOrder,
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test("harus menangani error ketika meja tidak tersedia", async () => {
|
||||
Meja.findOne.mockResolvedValue(null);
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(Meja.findOne).toHaveBeenCalledWith({
|
||||
tableNumber: 3,
|
||||
status: "available",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus menangani error ketika item menu tidak valid", async () => {
|
||||
Meja.findOne.mockResolvedValue({ tableNumber: 3, status: "available" });
|
||||
Menu.find.mockResolvedValue([]);
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(Menu.find).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllOrders", () => {
|
||||
test("harus mengembalikan semua pesanan", async () => {
|
||||
const mockOrders = [
|
||||
{ _id: mockObjectId, tableNumber: 1, total: 50000, status: "pending" },
|
||||
{
|
||||
_id: mockObjectId,
|
||||
tableNumber: 2,
|
||||
total: 75000,
|
||||
status: "completed",
|
||||
},
|
||||
];
|
||||
|
||||
const mockSort = jest.fn().mockResolvedValue(mockOrders);
|
||||
Order.find = jest.fn().mockReturnValue({ sort: mockSort });
|
||||
|
||||
await orderController.getAllOrders(req, res);
|
||||
|
||||
expect(Order.find).toHaveBeenCalled();
|
||||
expect(mockSort).toHaveBeenCalledWith({ createdAt: -1 });
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockOrders,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus menangani error saat mengambil pesanan", async () => {
|
||||
const errorMessage = "Database error";
|
||||
Order.find = jest.fn().mockImplementation(() => {
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
|
||||
await orderController.getAllOrders(req, res);
|
||||
|
||||
expect(Order.find).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateOrderStatus", () => {
|
||||
test("harus berhasil mengupdate status pesanan", async () => {
|
||||
req.body = { status: "completed" };
|
||||
|
||||
const mockUpdatedOrder = {
|
||||
_id: mockObjectId,
|
||||
tableNumber: 3,
|
||||
status: "completed",
|
||||
};
|
||||
|
||||
Order.findByIdAndUpdate.mockResolvedValue(mockUpdatedOrder);
|
||||
Meja.findOneAndUpdate.mockResolvedValue({ status: "available" });
|
||||
|
||||
await orderController.updateOrderStatus(req, res);
|
||||
|
||||
expect(Order.findByIdAndUpdate).toHaveBeenCalledWith(
|
||||
mockObjectId.toString(),
|
||||
{ status: "completed" },
|
||||
{ new: true },
|
||||
);
|
||||
expect(Meja.findOneAndUpdate).toHaveBeenCalledWith(
|
||||
{ tableNumber: 3 },
|
||||
{ status: "available" },
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: true,
|
||||
data: mockUpdatedOrder,
|
||||
});
|
||||
});
|
||||
|
||||
test("harus mengembalikan 404 ketika pesanan tidak ditemukan", async () => {
|
||||
req.body = { status: "completed" };
|
||||
|
||||
Order.findByIdAndUpdate.mockResolvedValue(null);
|
||||
|
||||
await orderController.updateOrderStatus(req, res);
|
||||
|
||||
expect(Order.findByIdAndUpdate).toHaveBeenCalledWith(
|
||||
mockObjectId.toString(),
|
||||
{ status: "completed" },
|
||||
{ new: true },
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Pesanan tidak ditemukan",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus menangani error saat mengupdate status", async () => {
|
||||
req.body = { status: "completed" };
|
||||
|
||||
const errorMessage = "Database error";
|
||||
Order.findByIdAndUpdate.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
await orderController.updateOrderStatus(req, res);
|
||||
|
||||
expect(Order.findByIdAndUpdate).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
const errorHandler = require("../../src/middleware/errorHandler");
|
||||
const mongoose = require("mongoose");
|
||||
const Order = require("../../src/models/orderModel");
|
||||
const Meja = require("../../src/models/mejaModel");
|
||||
const Menu = require("../../src/models/menuModel");
|
||||
const orderController = require("../../src/controllers/orderController");
|
||||
|
||||
const mockRequest = (body = {}, params = {}) => ({
|
||||
body,
|
||||
params,
|
||||
});
|
||||
|
||||
const mockResponse = () => {
|
||||
const res = {};
|
||||
res.status = jest.fn().mockReturnValue(res);
|
||||
res.json = jest.fn().mockReturnValue(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
jest.mock("../../src/models/orderModel");
|
||||
jest.mock("../../src/models/mejaModel");
|
||||
jest.mock("../../src/models/menuModel");
|
||||
|
||||
const mockObjectId = new mongoose.Types.ObjectId();
|
||||
|
||||
describe("Modul 5 - Error Handling", () => {
|
||||
let req, res, next;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => { });
|
||||
jest.clearAllMocks();
|
||||
|
||||
console.error = jest.fn();
|
||||
|
||||
req = {
|
||||
body: {
|
||||
tableNumber: 7,
|
||||
items: [{ menuId: mockObjectId.toString(), quantity: 2 }],
|
||||
},
|
||||
params: {},
|
||||
};
|
||||
|
||||
res = mockResponse();
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
describe("Error Handler Middleware", () => {
|
||||
test("harus menangani error dengan status code dari error", () => {
|
||||
const error = new Error("Validation error");
|
||||
error.statusCode = 400;
|
||||
|
||||
errorHandler(error, req, res, next);
|
||||
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Validation error",
|
||||
});
|
||||
});
|
||||
|
||||
test("harus menangani error tanpa status code dengan default 500", () => {
|
||||
const error = new Error("Server error");
|
||||
|
||||
errorHandler(error, req, res, next);
|
||||
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Server error",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Order Controller Error Handling", () => {
|
||||
test("harus menangani error meja tidak tersedia dengan status 400", async () => {
|
||||
Meja.findOne.mockResolvedValue(null);
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(next.mock.calls[0][0].message).toBe(
|
||||
"Meja tidak tersedia atau sedang dipesan",
|
||||
);
|
||||
expect(next.mock.calls[0][0].statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test("harus menangani error menu tidak valid dengan status 400", async () => {
|
||||
Meja.findOne.mockResolvedValue({ tableNumber: 7, status: "available" });
|
||||
Menu.find.mockResolvedValue([]);
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(next.mock.calls[0][0].message).toBe(
|
||||
"Beberapa item menu tidak valid",
|
||||
);
|
||||
expect(next.mock.calls[0][0].statusCode).toBe(400);
|
||||
});
|
||||
|
||||
test("harus menangani exception dengan middleware error", async () => {
|
||||
const errorMessage = "Database connection failed";
|
||||
Meja.findOne.mockImplementation(() => {
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
|
||||
await orderController.createOrder(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(Error));
|
||||
expect(next.mock.calls[0][0].message).toBe(errorMessage);
|
||||
});
|
||||
|
||||
test("harus mengubah error menjadi format error response", () => {
|
||||
const error = new Error("Custom error");
|
||||
error.statusCode = 422;
|
||||
|
||||
errorHandler(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(422);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
error: "Custom error",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Custom Error Creation", () => {
|
||||
test("harus membuat error dengan statusCode yang tepat", async () => {
|
||||
const createError = (message, statusCode) => {
|
||||
const error = new Error(message);
|
||||
error.statusCode = statusCode;
|
||||
return error;
|
||||
};
|
||||
|
||||
const error = createError("Resource not found", 404);
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.message).toBe("Resource not found");
|
||||
expect(error.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
<img src="{{ asset('./images/Group.png') }}" alt="Group" style="height: 50px; margin-right: 10px;">
|
||||
<i class="fas fa-chevron-down" style="color: #0079FF;"></i>
|
||||
<div class="dropdown-content" id="dropdownContent">
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="GET">
|
||||
<form id="logout-form" action="{{ route('logoutt') }}" method="POST">
|
||||
@csrf
|
||||
<a href="/" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Logout</a>
|
||||
</form>
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
<div class="card p-0" style="width: 305px; height:375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src={{asset("./images/cards/Node.js.png")}} class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Web Application With NodeJS</h5>
|
||||
<h5 class="card-title">Web application with Node.JS</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src={{asset("./images/book.png ")}} style="width: 13px; height: 16px;">
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="/nodejs/dashboard" class="btn btn-primary">Start Learning</a>
|
||||
<a href="/nodejs" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,413 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
|
||||
|
||||
|
||||
<title>iCLOP</title>
|
||||
<link rel="icon" href={{asset("./images/logo.png")}} type="image/png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- NAVBAR -->
|
||||
<nav class="navbar navbar-expand-lg" style="background-color: #FEFEFE;">
|
||||
<div class="container-fluid">
|
||||
<!-- <a class="navbar-brand" href="#">Navbar</a> -->
|
||||
<img src={{asset("./images/logo.png")}} alt="logo" width="104" height="65">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<div class="mx-auto">
|
||||
<ul class="navbar-nav mb-2 mb-lg-0 justify-content-center">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">Dashboard Teacher</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">Tutorials</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="#">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<p style="margin-top: 10px; margin-right: 10px;">{{auth()->user()->name}}
|
||||
<img src="{{ asset('./images/Group.png') }}" alt="Group" style="height: 50px; margin-right: 10px;">
|
||||
<i class="fas fa-chevron-down" style="color: #0079FF;"></i>
|
||||
<div class="dropdown-content" id="dropdownContent">
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="GET">
|
||||
@csrf
|
||||
<a href="#" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Logout</a>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<!-- <button class="btn btn-primary custom-button-sign-up" onclick="window.location.href='register.html'">Sign Up</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@auth
|
||||
|
||||
<!-- CONTENT -->
|
||||
<div class="container" style="margin-top: 70px; justify-content: center; align-items: center;">
|
||||
<p style="font-size: 22px;">Choose your<br><span style="font-size: 35px; font-weight: 600; color: #34364A;">Learning Materials</span></p>
|
||||
|
||||
<!-- CARD 1 -->
|
||||
<!-- <div class="row" style="margin-top: 45px; display: flex; justify-content: center; align-items: center;"> -->
|
||||
<div class="row" style="margin-top: 45px;">
|
||||
<div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src={{asset("./images/cards/Android.png")}} class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Android programming
|
||||
with Java and Kotlin</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src={{asset("./images/book.png")}} style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>18 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="/android23/topic" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src={{asset("./images/cards/Flutter.png")}} class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Mobile programming with Flutter</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src={{asset("./images/book.png")}} style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>18 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="/flutter/start" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-0" style="width: 305px; height:375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src={{asset("./images/cards/Node.js.png")}} class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Web application with Node.JS</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src={{asset("./images/book.png ")}} style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>{{ $topicCount ?? '0' }} learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{{ route('dashboard.nodejs.teacher') }}" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ---------------------------------------------------------------------------------------------------- -->
|
||||
|
||||
<!-- CARD 2 -->
|
||||
<!-- <div class="row" style="margin-top: 45px; display: flex; justify-content: center; align-items: center;"> -->
|
||||
<div class="row" style="margin-top: 45px;">
|
||||
<div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src="{{asset("./images/cards/Python.png")}}" class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Python programming</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src="{{asset("./images/book.png ")}}" style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>18 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{{ route('learning_student') }}" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src="{{asset("./images/cards/MySQL.png")}}" class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">SQL Querying with MySQL</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src="{{asset("./images/book.png ")}}" style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>18 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{{ route('learning_student') }}" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src="{{asset("./images/cards/PostgreSQL.png")}}" class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">SQL Querying with PostgreSQL</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src="{{asset("./images/book.png ")}}" style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>18 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{{ route('learning_student') }}" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ---------------------------------------------------------------------------------------------------- -->
|
||||
|
||||
<!-- CARD 3 -->
|
||||
<!-- <div class="row" style="margin-top: 45px; display: flex; justify-content: center; align-items: center;"> -->
|
||||
<div class="row" style="margin-top: 45px;">
|
||||
<div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src="{{asset("./images/cards/Network.png ")}}" class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Network programming with Java</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src="{{asset("./images/book.png ")}}" style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>18 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{{ route('learning_student') }}" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src="{{asset("./images/cards/Unity.png")}}" class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Game programming with Unity</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src="{{asset("./images/book.png ")}}" style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>18 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{{ route('learning_student') }}" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src="{{asset("./images/cards/Data analytic.png ")}}" class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Data Analytics with Python</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src="{{asset("./images/book.png ")}}" style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>18 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{{ route('learning_student') }}" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 45px;">
|
||||
<div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src="{{asset("./images/cards/DB.png ")}}" class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Database with PHP Programming</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src="{{asset("./images/book.png ")}}" style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>18 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{{ route('welcome') }}" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src="{{asset("./images/cards/React.jpg")}}" class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Learn React JS</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src="{{asset("./images/book.png ")}}" style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>6 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{{ route('react_welcome') }}" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="card p-0" style="width: 305px; height: 375px; margin-left: 25px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
|
||||
<img src="{{asset("./images/cards/DB.png ")}}" class="card-img-top" style="width: auto; height: 200px;">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title">Database Programming with PHP</h5>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<img src="{{asset("./images/book.png ")}}" style="width: 13px; height: 16px;">
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>18 learning topics</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: auto;">
|
||||
<a href="{{ route('welcome') }}" class="btn btn-primary">Start Learning</a>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="container text-align" style="margin-top: 100px; background-color: #FAFAFA; padding-right: 50px; padding-left: 50px; height: 300px;">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="container" style="margin-top: 40px;">
|
||||
<img src="{{asset("./images/logo.png ")}}" alt="rocket" style="height: 60px;">
|
||||
<p style="font-size: 16px; color: #636363;">Intelligent Computer Assisted<br>Programming Learning Platform.</p>
|
||||
</div>
|
||||
<div class="container">
|
||||
<i class="fab fa-instagram fa-lg" style="padding-right: 2px; color: #636363;"></i>
|
||||
<i class="fab fa-github fa-lg" style="padding-right: 2px; color: #636363;"></i>
|
||||
<i class="fab fa-linkedin fa-lg" style="padding-right: 2px; color: #636363;"></i>
|
||||
<i class="fab fa-youtube fa-lg" style="padding-right: 2px; color: #636363;"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="container">
|
||||
<p style="font-size: 22px; font-weight: 600; color: #34364A; margin-top: 40px; margin-left: 55px;">Company</p>
|
||||
<p style="font-size: 16px; color: #636363; margin-left: 55px;">Privacy Policy</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="container">
|
||||
|
||||
<p style="font-size: 22px; font-weight: 600; color: #34364A; margin-top: 40px;">Contact Info</p>
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<i class="fas fa-map-marker-alt fa-lg" style="color: #636363; margin-top: 5px;"></i>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p style="font-size: 16px; color: #636363;">Jl. Candi Mendut, RT.02/RW.08, Mojolangu, Kec. Lowokwaru, Kota Malang, Jawa Timur 65142</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row align-items-start">
|
||||
<div class="col-1">
|
||||
<i class="fas fa-envelope" style="color: #636363; margin-top: 5px;"></i>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p style="font-size: 16px; color: #636363;">qulis@polinema.ac.id (Email)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
<p style="font-size: 16px; color: #636363; text-align: center; margin-top: 16px; margin-bottom: 16px;">© 2023 iCLOP. All rights reserved</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
</body>
|
||||
<script src="script.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$("#dropdownContainer").click(function() {
|
||||
$("#dropdownContainer").toggleClass("active");
|
||||
});
|
||||
$("#dropdownContent").click(function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
$(document).click(function() {
|
||||
$("#dropdownContainer").removeClass("active");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
transition: 0.3s;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.dropdown-content a:hover {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
.dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.dropdown.active .dropdown-content {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,341 +0,0 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Teacher Dashboard') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 pb-12">
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-16">
|
||||
<!-- Total Projects -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-indigo-500 bg-opacity-75">
|
||||
<i class="fas fa-project-diagram text-white text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Projects</p>
|
||||
<p class="text-lg font-semibold">{{ $totalProyek }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Students -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-blue-500 bg-opacity-75">
|
||||
<i class="fas fa-users text-white text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Students</p>
|
||||
<p class="text-lg font-semibold">{{ $totalSiswa }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Submissions -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-green-500 bg-opacity-75">
|
||||
<i class="fas fa-file-code text-white text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Submissions</p>
|
||||
<p class="text-lg font-semibold">{{ $totalSubmisi }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Submissions -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-purple-500 bg-opacity-75">
|
||||
<i class="fas fa-check-circle text-white text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Completed Submissions</p>
|
||||
<p class="text-lg font-semibold">{{ $submissionSelesai }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Failed Submissions -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-purple-500 bg-opacity-75">
|
||||
<i class="fas fa-times-circle text-white text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Failed Submissions</p>
|
||||
<p class="text-lg font-semibold">{{ $submissionGagal }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Average Submissions -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-yellow-500 bg-opacity-75">
|
||||
<i class="fas fa-calculator text-white text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Average Submissions Attempts</p>
|
||||
<p class="text-lg font-semibold">{{ $ratarataPecobaan }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-8 mt-4">
|
||||
<!-- Submission Status Chart -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Submission Status</h3>
|
||||
<div class="relative h-64">
|
||||
<canvas id="submissionStatusChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Distribution Chart -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Projects with vs without Submissions</h3>
|
||||
<div class="relative h-64">
|
||||
<canvas id="projectDistributionChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4 mb-8 mt-4">
|
||||
<!-- Student Participation Rate -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Student Participation</h3>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Active Students</p>
|
||||
<p class="text-2xl font-bold">{{ $siswaSubmisi }} / {{ $totalSiswa }}</p>
|
||||
</div>
|
||||
<div class="relative w-24 h-24">
|
||||
<canvas id="participationChart"></canvas>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<span class="text-lg font-bold text-gray-900 dark:text-gray-100">{{ $tingkatPartisipasiSiswa }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Rate -->
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Submission Success Rate</h3>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">Completed Submissions</p>
|
||||
<p class="text-2xl font-bold">{{ $submissionSelesai }} / {{ $totalSubmisi }}</p>
|
||||
</div>
|
||||
<div class="relative w-24 h-24">
|
||||
<canvas id="successRateChart"></canvas>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<span class="text-lg font-bold text-gray-900 dark:text-gray-100">{{ $tingkatKeberhasilan ?? 0 }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Detailed Statistics -->
|
||||
<div class="grid grid-cols-1 gap-4 mb-8 mt-4 w-full">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg w-full">
|
||||
<div class="p-6 w-full">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Detailed Statistics</h3>
|
||||
<div class="overflow-x-auto w-full">
|
||||
<table class="w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Metric</th>
|
||||
<th class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Value</th>
|
||||
<th class="px-6 py-3 bg-gray-50 dark:bg-gray-700 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">Projects with Submissions</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{ $proyekDenganSubmisi }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Number of projects that have at least one submission</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">Projects without Submissions</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{ $proyekTanpaSubmisi }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Number of projects that have no submissions yet</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">Processing Submissions</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{ $submisiDalamProses }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Number of submissions currently being processed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">Pending Submissions</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{ $submisiTertunda }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">Number of submissions awaiting action</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
// Submission Status Chart
|
||||
const submissionStatusCtx = document.getElementById('submissionStatusChart').getContext('2d');
|
||||
const submissionStatusChart = new Chart(submissionStatusCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Completed', 'Processing', 'Pending', 'Failed'], // Tambah label Failed
|
||||
datasets: [{
|
||||
label: 'Submission Status',
|
||||
data: [{{ $submissionSelesai }}, {{ $submisiDalamProses }}, {{ $submisiTertunda }}, {{ $submissionGagal }}], // Tambah data failed
|
||||
backgroundColor: [
|
||||
'rgba(72, 187, 120, 0.7)', // Green for completed
|
||||
'rgba(66, 153, 225, 0.7)', // Blue for processing
|
||||
'rgba(237, 137, 54, 0.7)', // Orange for pending
|
||||
'rgba(220, 53, 69, 0.7)' // Red for failed
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(72, 187, 120, 1)',
|
||||
'rgba(66, 153, 225, 1)',
|
||||
'rgba(237, 137, 54, 1)',
|
||||
'rgba(220, 53, 69, 1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
precision: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Project Distribution Chart
|
||||
const projectDistributionCtx = document.getElementById('projectDistributionChart').getContext('2d');
|
||||
const projectDistributionChart = new Chart(projectDistributionCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Projects with Submissions', 'Projects without Submissions'],
|
||||
datasets: [{
|
||||
data: [{{ $proyekDenganSubmisi }}, {{ $proyekTanpaSubmisi }}],
|
||||
backgroundColor: [
|
||||
'rgba(79, 209, 197, 0.7)', // Teal
|
||||
'rgba(159, 122, 234, 0.7)' // Purple
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(79, 209, 197, 1)',
|
||||
'rgba(159, 122, 234, 1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
|
||||
// Participation Rate Chart
|
||||
const participationCtx = document.getElementById('participationChart').getContext('2d');
|
||||
const participationChart = new Chart(participationCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Active', 'Inactive'],
|
||||
datasets: [{
|
||||
data: [{{ $siswaSubmisi }}, {{ $totalSiswa - $siswaSubmisi }}],
|
||||
backgroundColor: [
|
||||
'rgba(104, 211, 145, 0.7)', // Green
|
||||
'rgba(226, 232, 240, 0.7)' // Light gray
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(104, 211, 145, 1)',
|
||||
'rgba(226, 232, 240, 1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
cutout: '70%',
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Success Rate Chart
|
||||
const successRateCtx = document.getElementById('successRateChart').getContext('2d');
|
||||
const successRateChart = new Chart(successRateCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Completed', 'Incomplete'],
|
||||
datasets: [{
|
||||
data: [{{ $submissionSelesai }}, {{ $totalSubmisi - $submissionSelesai }}],
|
||||
backgroundColor: [
|
||||
'rgba(72, 187, 120, 0.7)', // Green
|
||||
'rgba(226, 232, 240, 0.7)' // Light gray
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(72, 187, 120, 1)',
|
||||
'rgba(226, 232, 240, 1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
cutout: '70%',
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
</x-app-layout>
|
||||
|
|
@ -1,15 +1,28 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Dashboard') }}
|
||||
</h2>
|
||||
<button id="resetIntroBtn" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
<i class="fas fa-question-circle mr-1"></i>Tutorial
|
||||
</button>
|
||||
</div>
|
||||
</x-slot>
|
||||
<div class="pt-5">
|
||||
|
||||
<div class="py-4">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<!-- Tips Alert for Dashboard -->
|
||||
<x-alert.tips-alert pageId="dashboard" title="Tips untuk Anda">
|
||||
<p>Selamat datang di dashboard! Berikut adalah beberapa tips untuk membantu Anda:</p>
|
||||
<ul class="list-disc pl-5 mt-2">
|
||||
<li>Gunakan panel Projects untuk melihat detail project</li>
|
||||
<li>Untuk submissions bisa dilihat di panel Submissions</li>
|
||||
<li>Pilih proyek dari dropdown sebelum mengunggah</li>
|
||||
<li>Unggah file dengan cara drag & drop ZIP atau klik Browse Atau, masukkan link GitHub lengkap di kolom yang tersedia. Lalu klik tombol SUBMIT</li>
|
||||
<li>Maka anda akan di arahkan ke proses pengujian</li>
|
||||
<li>Anda bisa kemabali ke halaman dashboard untuk melihat status pengumpulan</li>
|
||||
</ul>
|
||||
</x-alert.tips-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pb-5">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
@include('nodejs.dashboard.partials.projects.list')
|
||||
|
|
|
|||
|
|
@ -1,21 +1,12 @@
|
|||
<!-- Contoh komponen project card dengan implementasi Intro.js yang lebih lengkap -->
|
||||
<div class="flex flex-col max-w-sm rounded-lg overflow-hidden shadow-lg bg-white dark:bg-gray-900 h-full"
|
||||
data-step="1"
|
||||
data-intro="Klik Read More untuk melihat detail dari project ini."
|
||||
data-title="Project Card"
|
||||
data-position="right"
|
||||
data-disable-interaction="false">
|
||||
|
||||
<div class="flex flex-col max-w-sm rounded-lg overflow-hidden shadow-lg bg-white dark:bg-gray-900 h-full">
|
||||
<img class="w-40 mx-auto my-4" src="{{$project->getImageAttribute()}}" alt="Project {{$project->title}}"
|
||||
onerror="this.onerror=null;this.src='{{ asset('assets/nodejs/placeholder.png') }}';">
|
||||
<div class="px-6 py-4">
|
||||
<div class="font-bold text-xl mb-2 text-gray-800 dark:text-white">{{$project->title}}</div>
|
||||
|
||||
<p class="text-gray-700 text-base dark:text-gray-400 leading-tight line-clamp-3">
|
||||
{{ $project->description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto px-6 pt-4 pb-2">
|
||||
@forelse ($project->tech_stack as $key => $stack)
|
||||
<span
|
||||
|
|
@ -27,7 +18,6 @@ class="inline-block bg-gray-200 dark:bg-gray-700 rounded-full px-3 py-1 text-sm
|
|||
tech stack</span>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-2 bg-gray-100 dark:bg-secondary">
|
||||
<a href="{{route('projects.show', $project->id)}}"
|
||||
class="text-xs font-semibold text-secondary dark:text-white uppercase tracking-wide">
|
||||
|
|
@ -36,4 +26,3 @@ class="text-xs font-semibold text-secondary dark:text-white uppercase tracking-w
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@
|
|||
<div>
|
||||
<x-input-label for="project_id" :value="__('Select Project Before Uploading')" class="mb-2" />
|
||||
<select name="project_id" id="project_id"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-secondary-500 focus:border-secondary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-secondary-500 dark:focus:border-secondary-500"
|
||||
data-step="2"
|
||||
data-intro="Pilih project yang ingin Anda kirimkan. Jika Anda sudah mengirimkan project ini sebelumnya, Anda tidak dapat mengirimkannya lagi."
|
||||
data-title="Select Project"
|
||||
data-disable-interaction="false">
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-secondary-500 focus:border-secondary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-secondary-500 dark:focus:border-secondary-500">
|
||||
<option value="">Select Project</option>
|
||||
@foreach ($projects as $project)
|
||||
@php
|
||||
|
|
@ -32,30 +28,18 @@ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:
|
|||
</select>
|
||||
<x-input-error :messages="$errors->get('project_id')" class="mt-2" />
|
||||
</div>
|
||||
<div class="mt-4"
|
||||
data-step="3"
|
||||
data-intro="Upload file ZIP project Anda di sini. Pastikan file yang diunggah adalah file ZIP."
|
||||
data-title="Upload ZIP File"
|
||||
data-disable-interaction="false">
|
||||
<div class="mt-4">
|
||||
<x-input-label for="folder" :value="__('Submit The Source Code')" class="mb-2" />
|
||||
<input type="file" name="folder_path" id="folder" data-allow-reorder="true" data-max-file-size="3MB" />
|
||||
<x-input-error :messages="$errors->get('folder')" class="mt-2" />
|
||||
</div>
|
||||
<div class="mt-4"
|
||||
data-step="4"
|
||||
data-intro="Jika Anda tidak ingin mengunggah file ZIP, Anda dapat memasukkan tautan ke repositori GitHub Anda di sini."
|
||||
data-title="Or Github Link"
|
||||
data-disable-interaction="false">
|
||||
<div class="mt-4">
|
||||
<x-input-label for="github_url" :value="__('Or Github Link')" />
|
||||
<x-text-input id="github_url" class="block mt-1 w-full" type="text" name="github_url"
|
||||
:value="old('github_url')" placeholder="E.g. https://github.com/username/repository.git" />
|
||||
<x-input-error :messages="$errors->get('github_url')" class="mt-2" />
|
||||
</div>
|
||||
<div class="flex items-center justify-end mt-12"
|
||||
data-step="5"
|
||||
data-intro="Klik tombol 'Submit' untuk mengirimkan project Anda."
|
||||
data-title="Submit Project"
|
||||
data-disable-interaction="false">
|
||||
<div class="flex items-center justify-end mt-12">
|
||||
<x-primary-button class="ml-4">
|
||||
{{ __('Submit') }}
|
||||
</x-primary-button>
|
||||
|
|
@ -100,7 +84,7 @@ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:
|
|||
},
|
||||
},
|
||||
allowMultiple: false,
|
||||
acceptedFileTypes: ['application/x-zip-compressed', 'application/zip', 'application/octet-stream'],
|
||||
acceptedFileTypes: ['application/x-zip-compressed'],
|
||||
fileValidateTypeDetectType: (source, type) =>
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(type);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
<table class="table" id="submissions_table"
|
||||
data-step="6"
|
||||
data-intro="Tekan title dari project untuk melihat detail dari submission anda."
|
||||
data-title="Submission Table"
|
||||
data-disable-interaction="false">
|
||||
<table class="table" id="submissions_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
|
|
|
|||
|
|
@ -13,70 +13,25 @@
|
|||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Styles -->
|
||||
<!-- Scripts -->
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css" />
|
||||
<link href="https://unpkg.com/filepond@^4/dist/filepond.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/intro.js/minified/introjs.min.css" />
|
||||
<!-- Tambahan: tema Intro.js untuk tampilan yang lebih baik -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/intro.js/themes/introjs-modern.css" />
|
||||
|
||||
<link href="{{asset('css/nodejs_style.css')}}" rel="stylesheet">
|
||||
|
||||
<!-- Custom CSS for IntroJS modification -->
|
||||
<style>
|
||||
/* Hide the default skip button */
|
||||
.introjs-skipbutton {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Custom navigation container at bottom */
|
||||
.introjs-tooltipbuttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Custom skip button at bottom */
|
||||
.custom-skip-button {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
padding: 6px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
|
||||
color: white;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent; /* awalnya tidak terlihat */
|
||||
border-radius: 999px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.custom-skip-button:hover {
|
||||
border-color: white;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!-- Scripts -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<script src="https://code.jquery.com/jquery-3.6.4.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://unpkg.com/filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.js"></script>
|
||||
<script src="https://unpkg.com/filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.js">
|
||||
</script>
|
||||
<script src="https://unpkg.com/filepond@^4/dist/filepond.js"></script>
|
||||
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
|
||||
<script src="https://unpkg.com/pdfobject@2.2.10/pdfobject.min.js"></script>
|
||||
<link href="{{asset('css/nodejs_style.css')}}" rel="stylesheet">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://unpkg.com/intro.js/minified/intro.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="font-sans antialiased">
|
||||
|
|
@ -97,99 +52,6 @@
|
|||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Konfigurasi Intro.js yang dimodifikasi -->
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Cek apakah pengguna sudah pernah melihat intro (menggunakan localStorage)
|
||||
const hasSeenIntro = localStorage.getItem('hasSeenNodeJSIntro');
|
||||
|
||||
// Cek apakah ada elemen dengan data-step di halaman
|
||||
const hasIntroElements = document.querySelectorAll('[data-step]').length > 0;
|
||||
|
||||
if (hasIntroElements && !hasSeenIntro) {
|
||||
startTutorial();
|
||||
}
|
||||
|
||||
// Tambahkan event listener untuk tombol reset tutorial
|
||||
const resetIntroBtn = document.getElementById('resetIntroBtn');
|
||||
if (resetIntroBtn) {
|
||||
resetIntroBtn.addEventListener('click', function() {
|
||||
localStorage.removeItem('hasSeenNodeJSIntro');
|
||||
startTutorial();
|
||||
});
|
||||
}
|
||||
|
||||
// Fungsi untuk memulai tutorial
|
||||
function startTutorial() {
|
||||
if (document.querySelectorAll('[data-step]').length === 0) return;
|
||||
|
||||
// Konfigurasi yang lebih lengkap untuk intro.js
|
||||
const intro = introJs();
|
||||
|
||||
// Konfigurasi umum
|
||||
intro.setOptions({
|
||||
nextLabel: 'Lanjut',
|
||||
prevLabel: 'Kembali',
|
||||
skipLabel: 'Lewati', // Ini tidak akan terlihat karena kita sembunyikan button aslinya
|
||||
doneLabel: 'Selesai',
|
||||
hidePrev: false,
|
||||
hideNext: false,
|
||||
showStepNumbers: true,
|
||||
showBullets: true,
|
||||
showProgress: true,
|
||||
scrollToElement: true,
|
||||
scrollTo: 'element',
|
||||
disableInteraction: false,
|
||||
tooltipPosition: 'auto',
|
||||
highlightClass: 'intro-highlight',
|
||||
tooltipClass: 'customTooltip',
|
||||
exitOnOverlayClick: false,
|
||||
exitOnEsc: true,
|
||||
overlayOpacity: 0.8
|
||||
});
|
||||
|
||||
// Event untuk memodifikasi tooltips setelah mereka dibuat
|
||||
intro.onafterchange(function(targetElement) {
|
||||
// Sembunyikan tombol skip default dan buat tombol skip kustom
|
||||
setTimeout(function() {
|
||||
const tooltipButtons = document.querySelector('.introjs-tooltipbuttons');
|
||||
const skipExists = tooltipButtons.querySelector('.custom-skip-button');
|
||||
|
||||
// Hanya tambahkan tombol jika belum ada
|
||||
if (!skipExists) {
|
||||
const skipButton = document.createElement('button');
|
||||
skipButton.className = 'custom-skip-button';
|
||||
skipButton.innerHTML = 'Lewati';
|
||||
|
||||
// Tambahkan event untuk skip
|
||||
skipButton.addEventListener('click', function() {
|
||||
intro.exit();
|
||||
localStorage.setItem('hasSeenNodeJSIntro', 'true');
|
||||
});
|
||||
|
||||
// Tempatkan tombol Lewati di sebelah paling kanan
|
||||
const nextButtons = tooltipButtons.querySelector('.introjs-nextbutton');
|
||||
tooltipButtons.appendChild(skipButton);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
|
||||
// Jalankan tutorial
|
||||
intro.start();
|
||||
|
||||
// Simpan bahwa pengguna sudah melihat intro
|
||||
intro.oncomplete(function() {
|
||||
localStorage.setItem('hasSeenNodeJSIntro', 'true');
|
||||
});
|
||||
|
||||
intro.onexit(function() {
|
||||
localStorage.setItem('hasSeenNodeJSIntro', 'true');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@yield('scripts')
|
||||
</body>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +1,40 @@
|
|||
<nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- Left side: Logo and Navigation Links -->
|
||||
<div class="flex items-center">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<a href="#">
|
||||
<a href="{{ route('dashboard.nodejs') }}">
|
||||
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
|
||||
@if (Auth::user()->role === 'student')
|
||||
<x-nav-link :href="route('dashboard.nodejs')" :active="request()->routeIs('dashboard-student')">
|
||||
<x-nav-link :href="route('dashboard.nodejs')" :active="request()->routeIs('dashboard*')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('projects')" :active="request()->routeIs('projects.student')">
|
||||
<x-nav-link :href="route('projects')" :active="request()->routeIs('projects*')">
|
||||
{{ __('Projects') }}
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('submissions')" :active="request()->routeIs('submissions.student')">
|
||||
<x-nav-link :href="route('submissions')" :active="request()->routeIs('submissions*')">
|
||||
{{ __('Submissions') }}
|
||||
</x-nav-link>
|
||||
@elseif (Auth::user()->role === 'teacher')
|
||||
<x-nav-link :href="route('dashboard.nodejs.teacher')" :active="request()->routeIs('dashboard-teacher')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-nav-link>
|
||||
<x-nav-link :href="route('nodejs.teacher.rank')" :active="request()->routeIs('nodejs.teacher.rank')">
|
||||
{{ __('Rank') }}
|
||||
</x-nav-link>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side: User Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center ml-auto">
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ml-6">
|
||||
<x-dropdown align="right" width="48">
|
||||
<x-slot name="trigger">
|
||||
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
|
||||
<button
|
||||
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none transition ease-in-out duration-150">
|
||||
<div>{{ Auth::user()->name }}</div>
|
||||
|
||||
<div class="ml-1">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd" />
|
||||
|
|
@ -52,33 +44,24 @@
|
|||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{-- Profile Link (Bisa ditampilkan untuk semua role) --}}
|
||||
<x-dropdown-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
</x-dropdown-link>
|
||||
|
||||
{{-- Authentication / Logout Berdasarkan Role --}}
|
||||
@if (Auth::user()->role === 'student')
|
||||
<form method="GET" action="{{ route('dashboard-student') }}">
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<x-dropdown-link href="{{ route('dashboard-student') }}" onclick="event.preventDefault(); this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
@elseif (Auth::user()->role === 'teacher')
|
||||
<form method="GET" action="{{ route('dashboard-teacher') }}">
|
||||
@csrf
|
||||
<x-dropdown-link href="{{ route('dashboard-teacher') }}" onclick="event.preventDefault(); this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
@endif
|
||||
</x-slot>
|
||||
|
||||
<x-dropdown-link :href="route('logout')" onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger (Mobile) -->
|
||||
<!-- Hamburger -->
|
||||
<div class="-mr-2 flex items-center sm:hidden">
|
||||
<button @click="open = ! open"
|
||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out">
|
||||
|
|
@ -86,9 +69,8 @@ class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark
|
|||
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex"
|
||||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden"
|
||||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -98,7 +80,6 @@ class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark
|
|||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
@if (Auth::user()->role === 'student')
|
||||
<x-responsive-nav-link :href="route('dashboard.nodejs')" :active="request()->routeIs('dashboard*')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-responsive-nav-link>
|
||||
|
|
@ -108,14 +89,6 @@ class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark
|
|||
<x-responsive-nav-link :href="route('submissions')" :active="request()->routeIs('submissions*')">
|
||||
{{ __('Submissions') }}
|
||||
</x-responsive-nav-link>
|
||||
@elseif (Auth::user()->role === 'teacher')
|
||||
<x-responsive-nav-link :href="route('dashboard.nodejs.teacher')" :active="request()->routeIs('dashboard*')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-responsive-nav-link>
|
||||
<x-responsive-nav-link :href="route('nodejs.teacher.rank')" :active="request()->routeIs('nodejs.teacher.rank')">
|
||||
{{ __('Rank') }}
|
||||
</x-responsive-nav-link>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
|
|
@ -130,21 +103,15 @@ class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark
|
|||
{{ __('Profile') }}
|
||||
</x-responsive-nav-link>
|
||||
|
||||
@if (Auth::user()->role === 'student')
|
||||
<form method="GET" action="{{ route('dashboard-student') }}">
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<x-responsive-nav-link :href="route('dashboard-student')" onclick="event.preventDefault(); this.closest('form').submit();">
|
||||
|
||||
<x-responsive-nav-link :href="route('logout')" onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-responsive-nav-link>
|
||||
</form>
|
||||
@elseif (Auth::user()->role === 'teacher')
|
||||
<form method="GET" action="{{ route('dashboard-teacher') }}">
|
||||
@csrf
|
||||
<x-responsive-nav-link :href="route('dashboard-teacher')" onclick="event.preventDefault(); this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-responsive-nav-link>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@
|
|||
@php
|
||||
$guidesCount = count($project->getMedia('project_guides'));
|
||||
$supplementsCount = count($project->getMedia('project_supplements'));
|
||||
$testsIndividualCount = count($project->getMedia('project_tests'));
|
||||
$testsZipExists = $project->getMedia('project_zips')->where('file_name', 'tests.zip')->first();
|
||||
$testsCount = $testsIndividualCount > 0 || $testsZipExists ? max($testsIndividualCount, 1) : 0;
|
||||
$testsCount = count($project->getMedia('project_tests_api')) + count($project->getMedia('project_tests_web'));
|
||||
@endphp
|
||||
<div>
|
||||
@if ($guidesCount > 0)
|
||||
|
|
|
|||
|
|
@ -1,31 +1,42 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight" data-step="1" data-intro="Ini adalah judul dari project yang sedang Anda lihat saat ini" data-title="Project Name" data-position="top" data-disable-interaction="false">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Project:') . ' ' . $project->title }}
|
||||
</h2>
|
||||
<button id="resetIntroBtn" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
<i class="fas fa-question-circle mr-1"></i> Tutorial
|
||||
</button>
|
||||
</div>
|
||||
</x-slot>
|
||||
<div class="py-4">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<!-- Tips Alert for Dashboard -->
|
||||
<x-alert.tips-alert pageId="project" title="Tips untuk Anda">
|
||||
<p>Selamat datang di project! Berikut adalah beberapa tips untuk membantu Anda:</p>
|
||||
<ul class="list-disc pl-5 mt-2">
|
||||
<li>Klik ikon garis 3 di sebelah nama guide untuk membuka opsi</li>
|
||||
<li>Pilih "View" untuk melihat PDF di halaman yang sama</li>
|
||||
<li>Pilih "Open in a new tab" untuk membuka di tab baru</li>
|
||||
<li>Pilih "Download" untuk menyimpan PDF ke perangkat Anda</li>
|
||||
<li>Klik "All Guides" di panel Download untuk mengunduh semua panduan sekaligus</li>
|
||||
<li>Klik "All Supplements" di panel Download untuk mengunduh file tambahan/pendukung</li>
|
||||
</ul>
|
||||
</x-alert.tips-alert>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-5">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg" data-step="2" data-intro="Bagian ini berisi detail project termasuk judul, deskripsi dan teknologi yang digunakan dalam project ini" data-title="Project Detail" data-position="top" data-disable-interaction="false">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
@include('nodejs.projects.partials.details')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-5">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg pb-18" data-step="3" data-intro="Di bagian ini, Anda dapat melihat semua panduan PDF terkait project ini. Klik 'View' untuk membuka panduan PDF langsung di halaman ini" data-title="Project Guide" data-position="top" data-disable-interaction="false">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg pb-18">
|
||||
@include('nodejs.projects.partials.guides')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-5">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg" data-step="4" data-intro="Di sini Anda dapat mengunduh semua materi project sekaligus berdasarkan kategori - panduan, materi tambahan, dan tes" data-title="Project Download" data-position="top" data-disable-interaction="false">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
@include('nodejs.projects.partials.downloads')
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,212 +0,0 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Student Rankings') }}
|
||||
</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="w-full mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class=" dark:bg-slate-800 overflow-hidden shadow-xl sm:rounded-lg border dark:border-gray-700">
|
||||
<!-- Header with project selector -->
|
||||
<div class="flex flex-col md:flex-row justify-between items-center dark:bg-slate-900 p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<span class="text-xl font-bold text-gray-800 dark:text-white mb-4 md:mb-0">Student Rankings</span>
|
||||
<div class="w-full md:w-96">
|
||||
<select id="project-selector" class= "bg-gray-50 border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-secondary-500 focus:border-secondary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-secondary-500 dark:focus:border-secondary-500">
|
||||
<option value="">Select Project</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Loading indicator -->
|
||||
<div id="loading" class="text-center my-8 hidden">
|
||||
<div class="inline-block animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></div>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-400">Loading data...</p>
|
||||
</div>
|
||||
|
||||
<!-- No project selected message -->
|
||||
<div id="no-project-selected" class="text-center my-12">
|
||||
<svg class="mx-auto h-16 w-16 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
<p class="mt-4 text-lg font-medium text-gray-600 dark:text-gray-400">Please select a project to view rankings</p>
|
||||
</div>
|
||||
|
||||
<!-- Rankings data container -->
|
||||
<div id="ranking-data" class="hidden">
|
||||
<!-- Export button -->
|
||||
<div class="flex justify-end mb-5">
|
||||
<button id="export-csv" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center shadow-md">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
Export to CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Rankings table -->
|
||||
<div class="overflow-x-auto dark:bg-slate-800 rounded-lg shadow-md">
|
||||
<table class="w-full table-fixed divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700 text-gray-500 dark:text-gray-300">
|
||||
<tr>
|
||||
<th scope="col" class="w-1/12 px-6 py-4 text-left text-xs font-bold text-gray-600 dark:text-white uppercase tracking-wider">Rank</th>
|
||||
<th scope="col" class="w-3/12 px-6 py-4 text-left text-xs font-bold text-gray-600 dark:text-white uppercase tracking-wider">Name</th>
|
||||
<th scope="col" class="w-1/12 px-6 py-4 text-left text-xs font-bold text-gray-600 dark:text-white uppercase tracking-wider">Attempts</th>
|
||||
<th scope="col" class="w-1/12 px-6 py-4 text-left text-xs font-bold text-gray-600 dark:text-white uppercase tracking-wider">Score</th>
|
||||
<th scope="col" class="w-1/12 px-6 py-4 text-left text-xs font-bold text-gray-600 dark:text-white uppercase tracking-wider">Passed</th>
|
||||
<th scope="col" class="w-1/12 px-6 py-4 text-left text-xs font-bold text-gray-600 dark:text-white uppercase tracking-wider">Total</th>
|
||||
<th scope="col" class="w-2/12 px-6 py-4 text-left text-xs font-bold text-gray-600 dark:text-white uppercase tracking-wider">Last Submission</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ranking-table-body" class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<!-- Rankings will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No data message -->
|
||||
<div id="no-data" class="text-center my-12 hidden">
|
||||
<svg class="mx-auto h-16 w-16 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 14h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p class="mt-4 text-lg font-medium text-gray-600 dark:text-gray-400">No submissions found for this project</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Load projects for dropdown
|
||||
loadProjects();
|
||||
|
||||
// Project selection change event
|
||||
$('#project-selector').change(function() {
|
||||
const projectId = $(this).val();
|
||||
if (projectId) {
|
||||
loadRankings(projectId);
|
||||
} else {
|
||||
$('#ranking-data').hide();
|
||||
$('#no-project-selected').show();
|
||||
$('#no-data').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Export to CSV button click event
|
||||
$('#export-csv').click(function() {
|
||||
const projectId = $('#project-selector').val();
|
||||
if (projectId) {
|
||||
window.location.href = `{{ route('nodejs.teacher.rank.export') }}?project_id=${projectId}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function loadProjects() {
|
||||
$.ajax({
|
||||
url: "{{ route('nodejs.teacher.rank.projects') }}",
|
||||
type: "GET",
|
||||
beforeSend: function() {
|
||||
$('#loading').show();
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.status === 'success') {
|
||||
const projects = response.data.projects;
|
||||
let options = '<option value="">Select Project</option>';
|
||||
|
||||
projects.forEach(project => {
|
||||
options += `<option value="${project.id}">${project.title}</option>`;
|
||||
});
|
||||
|
||||
$('#project-selector').html(options);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.error('Error loading projects', xhr);
|
||||
alert('Gagal memuat proyek. Silakan coba lagi.');
|
||||
},
|
||||
complete: function() {
|
||||
$('#loading').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadRankings(projectId) {
|
||||
$.ajax({
|
||||
url: "{{ route('nodejs.teacher.rank.by-project') }}",
|
||||
type: "GET",
|
||||
data: {
|
||||
project_id: projectId
|
||||
},
|
||||
beforeSend: function() {
|
||||
$('#ranking-data').hide();
|
||||
$('#no-project-selected').hide();
|
||||
$('#no-data').hide();
|
||||
$('#loading').show();
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.status === 'success') {
|
||||
const rankings = response.data.rankings;
|
||||
|
||||
if (rankings.length > 0) {
|
||||
populateRankingsTable(rankings);
|
||||
$('#ranking-data').show();
|
||||
} else {
|
||||
$('#no-data').show();
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.error('Error loading rankings', xhr);
|
||||
alert('Failed to load ranking. Please try again..');
|
||||
},
|
||||
complete: function() {
|
||||
$('#loading').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function populateRankingsTable(rankings) {
|
||||
let tableRows = '';
|
||||
|
||||
rankings.forEach(student => {
|
||||
const submissionDate = new Date(student.submission_date).toLocaleString();
|
||||
|
||||
// Adding special class for top 3 performers with improved contrast
|
||||
let rowClass = '';
|
||||
if (student.rank <= 3) {
|
||||
rowClass = 'top-performer';
|
||||
}
|
||||
|
||||
tableRows += `
|
||||
<tr class="${rowClass}">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
|
||||
${student.rank <= 3 ?
|
||||
`<span class="inline-flex items-center justify-center w-6 h-6 rounded-full ${
|
||||
student.rank === 1 ? 'bg-yellow-400 gold-medal' :
|
||||
student.rank === 2 ? 'bg-gray-300 silver-medal' :
|
||||
'bg-amber-600 bronze-medal'} text-white font-bold">
|
||||
${student.rank}
|
||||
</span>` :
|
||||
student.rank
|
||||
}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">${student.user_name}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">${student.attempts}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white font-medium">${student.score}%</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">${student.passed_tests}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">${student.total_tests}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">${submissionDate}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#ranking-table-body').html(tableRows);
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
</x-app-layout>
|
||||
|
|
@ -1,17 +1,24 @@
|
|||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||
{{ __('Submissions') }}
|
||||
</h2>
|
||||
<button id="resetIntroBtn" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
<i class="fas fa-question-circle mr-1"></i>Tutorial
|
||||
</button>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-4"></div>
|
||||
|
||||
<div class="py-4">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<!-- Tips Alert for Dashboard -->
|
||||
<x-alert.tips-alert pageId="submission-index" title="Tips untuk Anda">
|
||||
<p>Selamat datang di Submission! Berikut adalah beberapa tips untuk membantu Anda:</p>
|
||||
<ul class="list-disc pl-5 mt-2">
|
||||
<li>Perhatikan kolom "Status" untuk melihat hasil pengajuan Anda</li>
|
||||
<li>Klik ikon menu garis 3 untuk melihat opsi tindakan</li>
|
||||
<li>Pilih "Restart submission" untuk mencoba ulang pengajuan yang gagal</li>
|
||||
<li>Pilih "Delete submission" untuk menghapus pengajuan dari daftar</li>
|
||||
<li>Klik judul project untuk melihat history dan lebih detail mengenai submission</li>
|
||||
</ul>
|
||||
</x-alert.tips-alert>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg pb-12">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@section('scripts')
|
||||
<script type="text/javascript">
|
||||
function requestServer(element){
|
||||
|
|
@ -89,80 +88,29 @@ function requestServer(element){
|
|||
window.location = "/nodejs/submissions";
|
||||
});
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
let errorMessage = "Something went wrong!";
|
||||
|
||||
console.log("Error status:", xhr.status);
|
||||
console.log("Error response:", xhr.responseText);
|
||||
|
||||
// Check if it's a 403 error (maximum attempts reached)
|
||||
if (xhr.status === 403) {
|
||||
try {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
errorMessage = response.message || "Maximum attempts reached. Cannot restart submission.";
|
||||
} catch (e) {
|
||||
errorMessage = "Maximum attempts reached. Cannot restart submission.";
|
||||
}
|
||||
} else if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
|
||||
swal({
|
||||
title: "Error!",
|
||||
text: errorMessage,
|
||||
icon: "error",
|
||||
button: "Ok",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "change-source-code":
|
||||
// First check if attempts limit is reached via AJAX
|
||||
$.ajax({
|
||||
url: '/nodejs/submissions/change/' + submission_id,
|
||||
type: 'GET',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token()}}'
|
||||
},
|
||||
success: function(data) {
|
||||
// If successful, redirect to the change source code page
|
||||
window.location = '/nodejs/submissions/change/' + submission_id;
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
// Handle 403 error (maximum attempts reached)
|
||||
if (xhr.status === 403) {
|
||||
let errorMessage = "Maximum attempts reached. Cannot change source code.";
|
||||
try {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
errorMessage = response.message || errorMessage;
|
||||
} catch (e) {
|
||||
// Use default message if parsing fails
|
||||
}
|
||||
|
||||
swal({
|
||||
title: "Error!",
|
||||
text: errorMessage,
|
||||
icon: "error",
|
||||
button: "Ok",
|
||||
});
|
||||
} else {
|
||||
error: function(data) {
|
||||
swal({
|
||||
title: "Error!",
|
||||
text: "Something went wrong!",
|
||||
icon: "error",
|
||||
button: "Ok",
|
||||
});
|
||||
}
|
||||
console.log(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "change-source-code":
|
||||
// redirect to change source code page
|
||||
window.location = '/nodejs/submissions/change/' + submission_id;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$(function () {
|
||||
var table = $('#submissions_table').DataTable({
|
||||
"processing": true,
|
||||
|
|
@ -187,51 +135,11 @@ function requestServer(element){
|
|||
},
|
||||
ajax: "{{ route('submissions') }}",
|
||||
columns: [
|
||||
{
|
||||
data: 'title',
|
||||
name: 'title',
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
createdCell: function (td) {
|
||||
$(td).attr({
|
||||
'data-step': '1',
|
||||
'data-intro': 'Klik Tittle Project untuk melihat lebih detail submission, anda dapat dapat melihat detail pengujian dan mendownload hasil pengujiannya.',
|
||||
'data-title': 'Submission Detail',
|
||||
'data-position': 'right',
|
||||
'data-disable-interaction': 'false'
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
data: 'attempts_count',
|
||||
name: 'attempts_count',
|
||||
orderable: true,
|
||||
searchable: false,
|
||||
className: "text-center"
|
||||
},
|
||||
{
|
||||
data: 'submission_status',
|
||||
name: 'submission_status',
|
||||
orderable: true,
|
||||
searchable: true,
|
||||
className: "text-center"
|
||||
},
|
||||
{
|
||||
data: 'action',
|
||||
name: 'action',
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
className: "text-center",
|
||||
createdCell: function (td) {
|
||||
$(td).attr({
|
||||
'data-step': '2',
|
||||
'data-intro': 'Jika anda sudah mensubmit sebuah project, anda dapat mengulang dan menghapus submission project tersebut dengan mengklik tombol yang muncul disini.',
|
||||
'data-title': 'Submission Action',
|
||||
'data-position': 'right',
|
||||
'data-disable-interaction': 'false'
|
||||
});
|
||||
}
|
||||
},
|
||||
{data: 'title', name: 'title', orderable: true, searchable: true},
|
||||
// {data: 'submission_count', name: 'submission_count', orderable: true, searchable: false, className: "text-center"},
|
||||
{data: 'attempts_count', name: 'attempts_count', orderable: true, searchable: false, className: "text-center"},
|
||||
{data: 'submission_status', name: 'submission_status', orderable: true, searchable: true, className: "text-center"},
|
||||
{data: 'action', name: 'action', orderable: false, searchable: false, className: "text-center"},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,16 @@
|
|||
</h2>
|
||||
</x-slot>
|
||||
<div class="py-4">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<!-- Tips Alert for Dashboard -->
|
||||
<x-alert.tips-alert pageId="submission-show" title="Tips untuk Anda">
|
||||
<p>Selamat datang di Submission! Berikut adalah beberapa tips untuk membantu Anda:</p>
|
||||
<ul class="list-disc pl-5 mt-2">
|
||||
<li>Klik "View" untuk melihat feedback atau error log dari sistem</li>
|
||||
<li>Klik "Download Results" untuk mengunduh laporan berupa json</li>
|
||||
</ul>
|
||||
</x-alert.tips-alert>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-5">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@
|
|||
use App\Http\Controllers\NodeJS\Student\SubmissionController;
|
||||
use App\Http\Controllers\NodeJS\Student\WelcomeController;
|
||||
use App\Http\Controllers\NodeJS\Student\ProfileController;
|
||||
use App\Http\Controllers\NodeJS\Teacher\DashboardController as TeacherDashboardController;
|
||||
use App\Http\Controllers\NodeJS\Teacher\RankController;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
|
|
@ -16,14 +14,11 @@
|
|||
|
||||
Route::middleware('auth')->group(function () {
|
||||
// Dashboard
|
||||
Route::get('/dashboard/teacher', [TeacherDashboardController::class, 'index'])->name('dashboard.nodejs.teacher');
|
||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard.nodejs');
|
||||
|
||||
// Profile
|
||||
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||
|
||||
// Projects
|
||||
Route::prefix('projects')->controller(ProjectController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('projects');
|
||||
|
|
@ -31,7 +26,6 @@
|
|||
Route::get('/project/{project_id}/download', 'download')->name('projects.download');
|
||||
Route::get('pdf', 'showPDF')->name('projects.pdf');
|
||||
});
|
||||
|
||||
// Submissions
|
||||
Route::prefix('submissions')->group(function () {
|
||||
// show all the submission based on the projects
|
||||
|
|
@ -63,17 +57,5 @@
|
|||
// update source code based on the submission id
|
||||
Route::post('/update/submission', [SubmissionController::class, 'update'])->name('submissions.update');
|
||||
});
|
||||
|
||||
// Teacher Rankings
|
||||
Route::prefix('teacher/rank')->controller(RankController::class)->group(function () {
|
||||
// Display ranking page
|
||||
Route::get('/', 'index')->name('nodejs.teacher.rank');
|
||||
// Get ranking data for a specific project
|
||||
Route::get('/by-project', 'getRankingByProject')->name('nodejs.teacher.rank.by-project');
|
||||
// Get all projects for the ranking dropdown
|
||||
Route::get('/projects', 'getProjects')->name('nodejs.teacher.rank.projects');
|
||||
// Export ranking data to CSV
|
||||
Route::get('/export', 'exportRanking')->name('nodejs.teacher.rank.export');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\DataController;
|
||||
use App\Http\Controllers\NodeJS\NodeJSController;
|
||||
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use App\Http\Controllers\SocialController;
|
||||
|
|
@ -23,7 +22,7 @@
|
|||
// Route::get('/', function () {
|
||||
// return view('welcome');
|
||||
// });
|
||||
Route::get('/p', [AuthController::class, 'logout']);
|
||||
|
||||
Route::post('/login',[AuthController::class,'proses'])->name('login');
|
||||
Route::post('/signup',[AuthController::class,'signup'])->name('post_signup');
|
||||
Route::get('/auth/redirect', action:[AuthController::class, 'redirect'])->name(name:'google.redirect');
|
||||
|
|
@ -43,21 +42,21 @@
|
|||
return view('signup');
|
||||
})->name('signup');
|
||||
|
||||
Route::post('/logout', [AuthController::class, 'logout'])
|
||||
->name('logout');
|
||||
Route::get('/logout', [AuthController::class, 'logout'])
|
||||
Route::post('/logoutt', [AuthController::class, 'logoutt'])
|
||||
->name('logoutt');
|
||||
Route::get('/logout', [AuthController::class, 'logoutt'])
|
||||
->name('logout');
|
||||
|
||||
// Route::group(["prefix" => 'test', 'middleware' => ['login'], 'as' => 'test.'], function(){
|
||||
|
||||
|
||||
Route::get('/dashboard_teacher', [NodeJSController::class, 'dashboardTeacher'])
|
||||
->name('dashboard-teacher')
|
||||
->middleware('auth');
|
||||
Route::get('/dashboard-student', function () {
|
||||
return view('dashboard_student');
|
||||
})->name('dashboard-student')->middleware('auth');
|
||||
|
||||
Route::get('/dashboard-student', [NodeJSController::class, 'dashboardStudent'])
|
||||
->name('dashboard-student')
|
||||
->middleware('auth');
|
||||
Route::get('/dashboard_teacher', function () {
|
||||
return view('dashboard_student');
|
||||
})->name('dashboard-teacher')->middleware('auth');
|
||||
|
||||
Route::get('/learning-student', function () {
|
||||
return view('learning_student');
|
||||
|
|
|
|||
2
storage/projects/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
!.env
|
||||
!.gitignore
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
MONGODB_URI=mongodb+srv://<username>:<password>@<cluster_name>.incnimg.mongodb.net/<database_name>?retryWrites=true&w=majority
|
||||
MONGODB_URI_TEST=mongodb+srv://<username>:<password>@<cluster_name>.incnimg.mongodb.net/<database_name-test>?retryWrites=true&w=majority
|
||||
PORT=choose_a_port
|
||||
|
||||
# replace all <> with your own credentials as provided by MongoDB Atlas (https://www.mongodb.com/cloud/atlas)
|
||||
# it is recommended to use a different database for testing hence the -test suffix in the MONGODB_URI_TEST
|
||||
# choose a port that is not already in use (e.g. 5000)
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
{
|
||||
"name": "api-experiment",
|
||||
"version": "1.0.0",
|
||||
"description": "NodeJS and MongoDB API application",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"api-testA01": "cross-env NODE_ENV=test jest -i tests/api/testA01.test.js --testTimeout=20000",
|
||||
"web-testA01": "cross-env NODE_ENV=test jest -i tests/web/testA01.test.js --testTimeout=20000",
|
||||
"api-testA02": "cross-env NODE_ENV=test jest -i tests/api/testA02.test.js --testTimeout=20000",
|
||||
"web-testA02": "cross-env NODE_ENV=test jest -i tests/web/testA02.test.js --testTimeout=20000",
|
||||
"api-testA03": "cross-env NODE_ENV=test jest -i tests/api/testA03.test.js --testTimeout=20000",
|
||||
"web-testA03": "cross-env NODE_ENV=test jest -i tests/web/testA03.test.js --testTimeout=20000",
|
||||
"api-testA04": "cross-env NODE_ENV=test jest -i tests/api/testA04.test.js --testTimeout=20000",
|
||||
"web-testA04": "cross-env NODE_ENV=test jest -i tests/web/testA04.test.js --testTimeout=20000",
|
||||
"api-testA05": "cross-env NODE_ENV=test jest -i tests/api/testA05.test.js --testTimeout=20000",
|
||||
"web-testA05": "cross-env NODE_ENV=test jest -i tests/web/testA05.test.js --testTimeout=20000",
|
||||
"testAA": "cross-env NODE_ENV=test jest --testTimeout=40000 --silent --detectOpenHandles"
|
||||
},
|
||||
"jest": {
|
||||
"setupFilesAfterEnv": [
|
||||
"jest-expect-message"
|
||||
],
|
||||
"noStackTrace": true,
|
||||
"silent": false
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Omar630603/api-experiment.git"
|
||||
},
|
||||
"keywords": [
|
||||
"NodeJS",
|
||||
"ExpressJS",
|
||||
"API",
|
||||
"MongoDB"
|
||||
],
|
||||
"author": "Omar630603",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Omar630603/api-experiment/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Omar630603/api-experiment#readme",
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^29.5.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jest-image-snapshot": "^6.1.0",
|
||||
"nodemon": "^2.0.21",
|
||||
"puppeteer": "^19.7.4",
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.8",
|
||||
"express": "^4.18.2",
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"mongoose": "^6.10.0",
|
||||
"mongoose-slug-generator": "^1.0.4"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 14 KiB |
|
|
@ -1,7 +0,0 @@
|
|||
MONGODB_URI=mongodb+srv://<username>:<password>@<cluster_name>.incnimg.mongodb.net/<database_name>?retryWrites=true&w=majority
|
||||
MONGODB_URI_TEST=mongodb+srv://<username>:<password>@<cluster_name>.incnimg.mongodb.net/<database_name-test>?retryWrites=true&w=majority
|
||||
PORT=choose_a_port
|
||||
|
||||
# replace all <> with your own credentials as provided by MongoDB Atlas (https://www.mongodb.com/cloud/atlas)
|
||||
# it is recommended to use a different database for testing hence the -test suffix in the MONGODB_URI_TEST
|
||||
# choose a port that is not already in use (e.g. 5000)
|
||||