From 528c120fc33edf573ac6471f9ebf66adc4f7e12f Mon Sep 17 00:00:00 2001 From: cukiprit Date: Wed, 4 Mar 2026 14:35:18 +0700 Subject: [PATCH] feat: Implement book stock and archive functionality, add password reset, and refine UI styling. --- .../Admin/AdminPeminjamanController.php | 105 ++++++-- app/Http/Controllers/Admin/BookController.php | 46 +++- app/Http/Controllers/Admin/UserController.php | 2 + .../Auth/RegisteredUserController.php | 11 +- .../Auth/ResetPasswordController.php | 103 ++++++++ app/Http/Controllers/ProfileController.php | 19 +- app/Models/Book.php | 2 +- app/Services/DummyDataService.php | 2 +- ..._03_04_065853_add_stok_to_books_table.php} | 12 +- ..._04_070748_add_is_arsip_to_books_table.php | 28 +++ database/seeders/BookSeeder.php | 6 +- database/seeders/DatabaseSeeder.php | 38 +-- resources/scss/_custom.scss | 84 ++++--- resources/views/admin/buku/create.blade.php | 11 +- resources/views/admin/buku/edit.blade.php | 9 +- resources/views/admin/buku/index.blade.php | 238 +++++++++++------- resources/views/admin/denda/index.blade.php | 44 ++-- .../views/admin/peminjaman/index.blade.php | 32 ++- .../views/admin/pengguna/create.blade.php | 10 +- resources/views/admin/pengguna/edit.blade.php | 10 +- .../views/admin/pengguna/index.blade.php | 79 +++--- resources/views/auth/login.blade.php | 2 +- .../auth/reset-password-request.blade.php | 100 +++++--- resources/views/profile/index.blade.php | 87 +++---- routes/web.php | 12 +- 25 files changed, 733 insertions(+), 359 deletions(-) create mode 100644 app/Http/Controllers/Auth/ResetPasswordController.php rename database/migrations/{2026_02_11_152425_create_master_induks_table.php => 2026_03_04_065853_add_stok_to_books_table.php} (54%) create mode 100644 database/migrations/2026_03_04_070748_add_is_arsip_to_books_table.php diff --git a/app/Http/Controllers/Admin/AdminPeminjamanController.php b/app/Http/Controllers/Admin/AdminPeminjamanController.php index 2b12e54..3d6a9cb 100644 --- a/app/Http/Controllers/Admin/AdminPeminjamanController.php +++ b/app/Http/Controllers/Admin/AdminPeminjamanController.php @@ -8,6 +8,7 @@ use App\Models\User; use Carbon\Carbon; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; class AdminPeminjamanController extends Controller { @@ -24,7 +25,7 @@ public function index(Request $request) $firstLoan = $userLoans->first(); return [ - 'id_peminjaman' => 'PIN-ADM-' . sprintf('%03d', $userId), + 'id_peminjaman' => 'PIN-ADM-'.sprintf('%03d', $userId), 'user_id' => $userId, 'peminjam' => $user->nama_lengkap ?? 'Unknown', 'nomor_hp' => $user->phone ?? '-', @@ -32,7 +33,7 @@ public function index(Request $request) 'tenggat_kembali' => $firstLoan->due_at, 'status' => $firstLoan->status, 'role' => $user->role, - 'books' => $userLoans->map(fn($l) => [ + 'books' => $userLoans->map(fn ($l) => [ 'id' => $l->book->id, 'judul' => $l->book->judul, 'cover' => $l->book->cover, @@ -63,11 +64,11 @@ public function create() $user->disabled = $user->kena_limit || $user->is_banned; if ($user->is_banned) { - $user->status_text = "(Akun Dibekukan)"; + $user->status_text = '(Akun Dibekukan)'; } elseif ($user->kena_limit) { - $user->status_text = "(Limit Penuh: 2/2)"; + $user->status_text = '(Limit Penuh: 2/2)'; } else { - $user->status_text = ""; + $user->status_text = ''; } return $user; @@ -75,6 +76,7 @@ public function create() $daftarBuku = Book::where('status', 'Tersedia') ->whereJsonContains('tipe_akses', 'offline') + ->where('stok', '>', 0) ->get(); return view('admin.peminjaman.create', [ @@ -104,28 +106,92 @@ public function store(Request $request) throw new \Exception("Buku '{$book->judul}' tidak tersedia."); } + // Check stock + if ($book->stok <= 0) { + throw new \Exception("Buku '{$book->judul}' stok habis."); + } + // Create loan record Loan::create([ 'user_id' => $validated['peminjam_id'], 'book_id' => $bookId, - 'loan_code' => 'LOAN-' . date('Ymd') . '-' . strtoupper(substr(md5(uniqid()), 0, 6)), + 'loan_code' => 'LOAN-'.date('Ymd').'-'.strtoupper(substr(md5(uniqid()), 0, 6)), 'borrowed_at' => $validated['tanggal_pinjam'], 'due_at' => $validated['tanggal_kembali'], 'status' => 'Dipinjam', ]); - // Update book status - $book->update(['status' => 'Dipinjam']); + // Update book status and decrement stock + $book->update([ + 'status' => 'Dipinjam', + 'stok' => $book->stok - 1, + ]); } \DB::commit(); + return redirect()->route('admin.peminjaman.index')->with('success', 'Peminjaman berhasil dibuat.'); } catch (\Exception $e) { \DB::rollBack(); + return back()->withErrors(['error' => $e->getMessage()])->withInput(); } } + public function export(Request $request) + { + $request->validate([ + 'bulan_laporan' => 'nullable|date', + ]); + + $query = Loan::with(['user', 'book'])->orderBy('borrowed_at', 'asc'); + + if ($request->filled('bulan_laporan')) { + $date = Carbon::parse($request->bulan_laporan); + $query->whereMonth('borrowed_at', $date->month) + ->whereYear('borrowed_at', $date->year); + $fileName = 'Laporan_Peminjaman_'.$date->format('Y-m').'.csv'; + } else { + $fileName = 'Laporan_Peminjaman_Semua.csv'; + } + + $loans = $query->get(); + + $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" + ]; + + $columns = ['NO', 'ID PEMINJAMAN', 'PEMINJAM', 'ROLE', 'JUDUL BUKU', 'TGL PINJAM', 'T tenggat KEMBALI', 'STATUS', 'DENDA KETERLAMBATAN']; + + $callback = function() use($loans, $columns) { + $file = fopen('php://output', 'w'); + fputcsv($file, $columns); + + $i = 1; + foreach ($loans as $loan) { + $row['NO'] = $i++; + $row['ID_PEMINJAMAN'] = $loan->loan_code; + $row['PEMINJAM'] = $loan->user->nama_lengkap ?? 'Unknown'; + $row['ROLE'] = $loan->user->role ?? '-'; + $row['JUDUL_BUKU'] = $loan->book->judul ?? 'Unknown'; + $row['TGL_PINJAM'] = $loan->borrowed_at->format('d/m/Y'); + $row['TENGGAT_KEMBALI'] = $loan->due_at ? $loan->due_at->format('d/m/Y') : '-'; + $row['STATUS'] = $loan->status; + $row['DENDA'] = $loan->fine_overdue ?? 0; + + fputcsv($file, array_values($row)); + } + + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } + public function dendaIndex() { $now = Carbon::now(); @@ -157,17 +223,19 @@ public function dendaIndex() $hp = $user->phone ?? ''; $waLink = '#'; if ($hp) { - if (substr($hp, 0, 1) == '0') $hp = '62' . substr($hp, 1); + if (substr($hp, 0, 1) == '0') { + $hp = '62'.substr($hp, 1); + } if ($isGuru && $hariTelat > 0) { $pesan = "Halo Bapak/Ibu {$user->nama_lengkap}, anda terlambat pengembalian buku selama {$hariTelat} hari. Mohon segera dikembalikan ke perpustakaan. Terima kasih."; } else { $pesan = $hariTelat > 0 - ? "Halo {$user->nama_lengkap}, anda terlambat pengembalian buku. Total Denda: Rp " . number_format($totalDenda, 0, ',', '.') + ? "Halo {$user->nama_lengkap}, anda terlambat pengembalian buku. Total Denda: Rp ".number_format($totalDenda, 0, ',', '.') : "Halo {$user->nama_lengkap}, akun anda sedang dinonaktifkan sementara. Mohon hubungi petugas."; } - $waLink = "https://wa.me/{$hp}?text=" . urlencode($pesan); + $waLink = "https://wa.me/{$hp}?text=".urlencode($pesan); } return [ @@ -182,7 +250,7 @@ public function dendaIndex() 'wa_link' => $waLink, 'is_banned' => $user->is_banned, 'tenggat_kembali' => $firstLoan->due_at, - 'books' => $userLoans->map(fn($l) => [ + 'books' => $userLoans->map(fn ($l) => [ 'id' => $l->book->id, 'judul' => $l->book->judul, ])->toArray(), @@ -194,7 +262,7 @@ public function dendaIndex() return view('admin.denda.index', [ 'pageTitle' => 'Manajemen Denda & Sanksi', 'siswaTelat' => $siswaTelat, - 'listKelas' => $listKelas + 'listKelas' => $listKelas, ]); } @@ -211,7 +279,7 @@ public function berikanSanksi(Request $request) return response()->json([ 'status' => 'success', - 'message' => $user->is_banned ? "Akun {$user->nama_lengkap} berhasil dibekukan." : "Akun {$user->nama_lengkap} telah diaktifkan kembali." + 'message' => $user->is_banned ? "Akun {$user->nama_lengkap} berhasil dibekukan." : "Akun {$user->nama_lengkap} telah diaktifkan kembali.", ]); } @@ -245,15 +313,20 @@ public function kembalikan(Request $request) 'return_notes' => $item['notes'], ]); - // Update book status - $loan->book->update(['status' => 'Tersedia']); + // Update book status and increment stock + $loan->book->update([ + 'status' => 'Tersedia', + 'stok' => $loan->book->stok + 1, + ]); } } \DB::commit(); + return response()->json(['status' => 'success', 'message' => 'Buku berhasil dikembalikan.']); } catch (\Exception $e) { \DB::rollBack(); + return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500); } } diff --git a/app/Http/Controllers/Admin/BookController.php b/app/Http/Controllers/Admin/BookController.php index caf1908..3f78646 100644 --- a/app/Http/Controllers/Admin/BookController.php +++ b/app/Http/Controllers/Admin/BookController.php @@ -12,29 +12,33 @@ class BookController extends Controller public function index(Request $request) { $filters = $request->only(['search']); - + $query = Book::with('category'); if ($request->filled('search')) { - $query->where('judul', 'like', '%' . $request->search . '%'); + $query->where('judul', 'like', '%'.$request->search.'%'); } $semuaBuku = $query->latest()->get(); - // Memisahkan buku menjadi dua koleksi: online dan offline secara independen $bukuOnline = $semuaBuku->filter(function ($buku) { - return in_array('online', $buku->tipe_akses ?? []); + return in_array('online', $buku->tipe_akses ?? []) && ! $buku->is_arsip; }); $bukuOffline = $semuaBuku->filter(function ($buku) { - return in_array('offline', $buku->tipe_akses ?? []); + return in_array('offline', $buku->tipe_akses ?? []) && ! $buku->is_arsip; + }); + + $bukuArsip = $semuaBuku->filter(function ($buku) { + return $buku->is_arsip; }); return view('admin.buku.index', [ 'pageTitle' => 'Manajemen Buku', 'bukuOnline' => $bukuOnline, 'bukuOffline' => $bukuOffline, - 'input' => $filters + 'bukuArsip' => $bukuArsip, + 'input' => $filters, ]); } @@ -45,7 +49,7 @@ public function create() { return view('admin.buku.create', [ 'pageTitle' => 'Tambah Buku Baru', - 'categories' => Category::all() + 'categories' => Category::all(), ]); } @@ -54,9 +58,9 @@ public function edit($id) $buku = Book::with('category')->findOrFail($id); return view('admin.buku.edit', [ - 'pageTitle' => 'Edit Buku: ' . $buku->judul, + 'pageTitle' => 'Edit Buku: '.$buku->judul, 'buku' => $buku, - 'categories' => Category::all() + 'categories' => Category::all(), ]); } @@ -68,6 +72,7 @@ public function store(Request $request) 'category_id' => 'required|exists:categories,id', 'tahun' => 'required|integer', 'kode_buku' => 'nullable|string', + 'stok' => 'required|integer|min:0', 'tipe_akses' => 'required|array', 'cover' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'file_pdf' => 'nullable|mimes:pdf|max:10240', @@ -75,7 +80,7 @@ public function store(Request $request) if ($request->hasFile('cover')) { $path = $request->file('cover')->store('covers', 'public'); - $validated['cover'] = 'storage/' . $path; + $validated['cover'] = 'storage/'.$path; } if ($request->hasFile('file_pdf')) { @@ -98,6 +103,7 @@ public function update(Request $request, $id) 'category_id' => 'required|exists:categories,id', 'tahun' => 'required|integer', 'kode_buku' => 'nullable|string', + 'stok' => 'required|integer|min:0', 'tipe_akses' => 'required|array', 'cover' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'file_pdf' => 'nullable|mimes:pdf|max:10240', @@ -105,7 +111,7 @@ public function update(Request $request, $id) if ($request->hasFile('cover')) { $path = $request->file('cover')->store('covers', 'public'); - $validated['cover'] = 'storage/' . $path; + $validated['cover'] = 'storage/'.$path; } if ($request->hasFile('file_pdf')) { @@ -125,4 +131,20 @@ public function destroy($id) return redirect()->route('admin.buku.index')->with('success', 'Buku berhasil dihapus.'); } -} \ No newline at end of file + + public function arsip(Request $request) + { + $buku = Book::findOrFail($request->id); + $buku->update(['is_arsip' => true]); + + return response()->json(['status' => 'success', 'message' => 'Buku berhasil diarsipkan.']); + } + + public function pulihkan(Request $request) + { + $buku = Book::findOrFail($request->id); + $buku->update(['is_arsip' => false]); + + return response()->json(['status' => 'success', 'message' => 'Buku berhasil dipulihkan.']); + } +} diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index ad343c4..e4c87fd 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -52,6 +52,7 @@ public function store(Request $request) 'phone' => 'nullable|string|max:20', 'role' => 'required|in:siswa,guru,penjaga perpus', 'kelas' => 'nullable|string|max:50', + 'golongan' => 'nullable|string|max:50', 'password' => 'required|string|min:8|confirmed', ]); @@ -74,6 +75,7 @@ public function update(Request $request, $id) 'phone' => 'nullable|string|max:20', 'role' => 'required|in:siswa,guru,penjaga perpus', 'kelas' => 'nullable|string|max:50', + 'golongan' => 'nullable|string|max:50', 'password' => 'nullable|string|min:8|confirmed', ]); diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index b4a33f1..08bc932 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -3,8 +3,8 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; -use App\Models\User; use App\Models\MasterInduk; +use App\Models\User; use Illuminate\Auth\Events\Registered; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -19,6 +19,7 @@ class RegisteredUserController extends Controller public function create(Request $request): View { $role = $request->query('role', 'siswa'); + return view('auth.register', ['role' => $role]); } @@ -26,7 +27,6 @@ public function store(Request $request): RedirectResponse { $role = $request->input('role'); - $rules = [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], @@ -43,27 +43,24 @@ public function store(Request $request): RedirectResponse $request->validate($rules); - $nomorInduk = ($role === 'siswa') ? $request->nisn : $request->nip; $isWhitelisted = MasterInduk::where('nomor_induk', $nomorInduk) ->where('role', $role) ->exists(); - if (!$isWhitelisted) { + if (! $isWhitelisted) { throw ValidationException::withMessages([ ($role === 'siswa' ? 'nisn' : 'nip') => ['Nomor induk tidak terdaftar di Data Sekolah. Hubungi petugas.'], ]); } - $user = User::create([ 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make($request->password), 'role' => $role, - 'nisn' => ($role === 'siswa') ? $nomorInduk : null, - 'nip' => ($role === 'guru') ? $nomorInduk : null, + 'nomor_induk' => $nomorInduk, ]); event(new Registered($user)); diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php new file mode 100644 index 0000000..b0e7334 --- /dev/null +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -0,0 +1,103 @@ +validate([ + 'user_id' => 'required|exists:users,id', + ]); + + $user = User::findOrFail($request->user_id); + + // Generate a 6-digit OTP + $otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT); + + // Store OTP in cache keyed by user email — expires in 15 minutes + Cache::put('reset_otp_' . $user->email, $otp, now()->addMinutes(15)); + + return response()->json([ + 'status' => 'success', + 'otp' => $otp, + ]); + } + + /** + * Verify the OTP provided by the user against cache. + */ + public function verifyOtp(Request $request) + { + $request->validate([ + 'otp' => 'required|string|size:6', + 'email' => 'required|email|exists:users,email', + ]); + + $cachedOtp = Cache::get('reset_otp_' . $request->email); + + if (!$cachedOtp) { + return response()->json([ + 'status' => 'error', + 'message' => 'OTP sudah kedaluwarsa. Minta admin untuk generate ulang.', + ], 422); + } + + if ($request->otp !== $cachedOtp) { + return response()->json([ + 'status' => 'error', + 'message' => 'Kode OTP salah! Cek lagi kode yang dikirim admin.', + ], 422); + } + + // Store verified email in a separate short-lived cache key + Cache::put('reset_otp_verified_' . $request->email, true, now()->addMinutes(15)); + + return response()->json([ + 'status' => 'success', + 'email' => $request->email, + ]); + } + + /** + * Update the password for the user whose OTP was verified. + */ + public function updatePassword(Request $request) + { + $request->validate([ + 'email' => 'required|email|exists:users,email', + 'password' => 'required|string|min:8|confirmed', + ]); + + // Ensure OTP was verified + if (!Cache::get('reset_otp_verified_' . $request->email)) { + return response()->json([ + 'status' => 'error', + 'message' => 'Sesi tidak valid. Silakan verifikasi OTP terlebih dahulu.', + ], 403); + } + + $user = User::where('email', $request->email)->firstOrFail(); + $user->password = Hash::make($request->password); + $user->save(); + + // Clear OTP cache data + Cache::forget('reset_otp_' . $request->email); + Cache::forget('reset_otp_verified_' . $request->email); + + return response()->json([ + 'status' => 'success', + 'message' => 'Password berhasil diperbarui.', + ]); + } +} diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 2b027da..baa8758 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -18,7 +18,7 @@ class ProfileController extends Controller /** * Tampilkan halaman profil user. */ - public function index(Request $request): View + public function index(Request $request): \Illuminate\View\View|\Illuminate\Http\RedirectResponse { $user = Auth::user(); if (!$user) { @@ -62,7 +62,22 @@ public function index(Request $request): View // Analytics for Guru (simplified for profile view) $viewData['laporan'] = [ - 'buku_terpopuler' => Book::withCount('loans')->orderBy('loans_count', 'desc')->take(3)->get(), + 'buku_terpopuler' => Book::withCount('loans') + ->orderBy('loans_count', 'desc') + ->take(3) + ->get() + ->map(fn($b) => [ + 'judul' => $b->judul, + 'total_pembaca' => $b->loans_count + ]), + 'kategori_populer' => \App\Models\Category::select('categories.name as nama') + ->join('books', 'categories.id', '=', 'books.category_id') + ->join('loans', 'books.id', '=', 'loans.book_id') + ->selectRaw('count(loans.id) as total_pembaca') + ->groupBy('categories.id', 'categories.name') + ->orderBy('total_pembaca', 'desc') + ->take(3) + ->get(), 'insight' => 'Siswa aktif meminjam buku kategori Sains.' ]; } else { diff --git a/app/Models/Book.php b/app/Models/Book.php index 1caa369..16930c5 100644 --- a/app/Models/Book.php +++ b/app/Models/Book.php @@ -8,7 +8,7 @@ class Book extends Model { protected $fillable = [ 'judul', 'penulis', 'cover', 'kode_buku', - 'category_id', 'tahun', 'status', 'is_new', 'tipe_akses', 'file_pdf' + 'category_id', 'tahun', 'status', 'is_new', 'tipe_akses', 'file_pdf', 'is_arsip' ]; protected $casts = [ diff --git a/app/Services/DummyDataService.php b/app/Services/DummyDataService.php index 4ead6c1..5695f27 100644 --- a/app/Services/DummyDataService.php +++ b/app/Services/DummyDataService.php @@ -37,7 +37,7 @@ public static function getAllSiswa(): array 'nisn' => '9988776655', 'nama_lengkap' => 'Siti Nurhaliza', 'email' => 'siti.nurhaliza@smkn1perpus.sch.id', - 'nomor_hp' => '081998877665', + 'nomor_hp' => '0895618643811', 'password' => 'password', 'role' => 'siswa', 'kelas' => 'XII RPL A', diff --git a/database/migrations/2026_02_11_152425_create_master_induks_table.php b/database/migrations/2026_03_04_065853_add_stok_to_books_table.php similarity index 54% rename from database/migrations/2026_02_11_152425_create_master_induks_table.php rename to database/migrations/2026_03_04_065853_add_stok_to_books_table.php index 477da4a..de7134a 100644 --- a/database/migrations/2026_02_11_152425_create_master_induks_table.php +++ b/database/migrations/2026_03_04_065853_add_stok_to_books_table.php @@ -11,12 +11,8 @@ */ public function up(): void { - Schema::create('master_induks', function (Blueprint $table) { - $table->id(); - $table->string('nomor_induk')->unique(); - $table->string('nama_pemilik'); - $table->string('role'); - $table->timestamps(); + Schema::table('books', function (Blueprint $table) { + $table->integer('stok')->default(1)->after('status'); }); } @@ -25,6 +21,8 @@ public function up(): void */ public function down(): void { - Schema::dropIfExists('master_induks'); + Schema::table('books', function (Blueprint $table) { + // + }); } }; diff --git a/database/migrations/2026_03_04_070748_add_is_arsip_to_books_table.php b/database/migrations/2026_03_04_070748_add_is_arsip_to_books_table.php new file mode 100644 index 0000000..533062d --- /dev/null +++ b/database/migrations/2026_03_04_070748_add_is_arsip_to_books_table.php @@ -0,0 +1,28 @@ +boolean('is_arsip')->default(false)->after('stok'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('books', function (Blueprint $table) { + // + }); + } +}; diff --git a/database/seeders/BookSeeder.php b/database/seeders/BookSeeder.php index df74ab0..01479dc 100644 --- a/database/seeders/BookSeeder.php +++ b/database/seeders/BookSeeder.php @@ -2,11 +2,10 @@ namespace Database\Seeders; -use Illuminate\Database\Seeder; use App\Models\Book; use App\Models\Category; use App\Services\DummyDataService; -use Illuminate\Support\Str; +use Illuminate\Database\Seeder; class BookSeeder extends Seeder { @@ -20,6 +19,8 @@ public function run(): void foreach ($books as $data) { $category = Category::where('name', $data['kategori'])->first(); + $stok = isset($data['stok']) ? $data['stok'] : (($data['status'] === 'Tersedia') ? 1 : 0); + Book::updateOrCreate( ['kode_buku' => $data['kode_buku']], [ @@ -30,6 +31,7 @@ public function run(): void 'category_id' => $category ? $category->id : null, 'tahun' => $data['tahun'], 'status' => $data['status'], + 'stok' => $stok, 'is_new' => $data['is_new'], 'tipe_akses' => is_array($data['tipe_akses']) ? $data['tipe_akses'] : [$data['tipe_akses']], ] diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index dc9f3eb..cf82272 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,11 +2,11 @@ namespace Database\Seeders; -use Illuminate\Database\Seeder; -use App\Models\User; use App\Models\MasterInduk; -use Illuminate\Support\Facades\Hash; +use App\Models\User; +use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Hash; class DatabaseSeeder extends Seeder { @@ -29,7 +29,7 @@ public function run() MasterInduk::create($w); } - // ISI USER ASLI + // ISI USER ASLI // ID 1: Silvi (Siswa) User::create([ @@ -38,10 +38,10 @@ public function run() 'email' => 'silvi.rahmawati@smkn1perpus.sch.id', 'password' => Hash::make('password'), 'role' => 'siswa', - 'nisn' => '1234567890', - 'no_hp' => '08123456789', + 'nomor_induk' => '1234567890', + 'phone' => '08123456789', 'kelas' => 'XII RPL', - 'golongan' => 'A' + 'golongan' => 'A', ]); // ID 2: Budi (Admin/Penjaga) @@ -51,7 +51,7 @@ public function run() 'email' => 'budi.santoso@smkn1perpus.sch.id', 'password' => Hash::make('password'), 'role' => 'penjaga perpus', - 'nip' => '197812312005011', + 'nomor_induk' => '197812312005011', ]); // ID 3: Siti (Siswa) @@ -61,10 +61,10 @@ public function run() 'email' => 'siti.nurhaliza@smkn1perpus.sch.id', 'password' => Hash::make('password'), 'role' => 'siswa', - 'nisn' => '9988776655', - 'no_hp' => '081998877665', + 'nomor_induk' => '9988776655', + 'phone' => '0895618643811', 'kelas' => 'XII RPL', - 'golongan' => 'B' + 'golongan' => 'B', ]); // ID 4: Andi (Siswa) @@ -74,10 +74,10 @@ public function run() 'email' => 'andi.pratama@smkn1perpus.sch.id', 'password' => Hash::make('password'), 'role' => 'siswa', - 'nisn' => '5566778899', - 'no_hp' => '081556677889', + 'nomor_induk' => '5566778899', + 'phone' => '081556677889', 'kelas' => 'XII RPL', - 'golongan' => 'C' + 'golongan' => 'C', ]); // ID 5: Rina (Guru) @@ -87,7 +87,15 @@ public function run() 'email' => 'rina.marlina@smkn1perpus.sch.id', 'password' => Hash::make('password'), 'role' => 'guru', - 'nip' => '198506152010012', + 'nomor_induk' => '198506152010012', + ]); + + $this->call([ + CategorySeeder::class, + BookSeeder::class, + AnnouncementSeeder::class, + RecommendationSeeder::class, + LoanSeeder::class, ]); } } diff --git a/resources/scss/_custom.scss b/resources/scss/_custom.scss index 2b46a15..6dccc8b 100644 --- a/resources/scss/_custom.scss +++ b/resources/scss/_custom.scss @@ -4,8 +4,7 @@ // UTILITIES (GENERATED FROM MAPS) // =================================== // Generator otomatis untuk variant warna (background light, soft, alert) -@each $color, -$value in $theme-colors { +@each $color, $value in $theme-colors { .bg-#{$color}-light { background-color: rgba($value, 0.25) !important; } @@ -63,14 +62,14 @@ html { .navbar-nav { .nav-link-landing { padding: 0.5rem 1rem; - color: #49769F; + color: #49769f; text-decoration: none; display: block; transition: color 0.3s ease; &:hover, &.active { - color: #0C5495; + color: #0c5495; font-weight: 500; transition: all 0.3s ease-in; } @@ -109,9 +108,11 @@ html { // Hero Section .landing-hero-section { - background: linear-gradient(135deg, - #0C5495 0%, - color.adjust(#0C5495, $lightness: 10%) 100%); + background: linear-gradient( + 135deg, + #0c5495 0%, + color.adjust(#0c5495, $lightness: 10%) 100% + ); padding: 60px 0; @media (min-width: 992px) { @@ -143,7 +144,6 @@ html { i { font-size: 5rem; } - } .card-img-top { @@ -186,9 +186,11 @@ html { // CTA Section .landing-cta-section { - background: linear-gradient(135deg, - #0C5495 0%, - color.adjust(#0C5495, $lightness: 10%) 100%); + background: linear-gradient( + 135deg, + #0c5495 0%, + color.adjust(#0c5495, $lightness: 10%) 100% + ); border-radius: 1rem; @media (min-width: 768px) { @@ -196,7 +198,6 @@ html { } } - // =================================== // BASE COMPONENTS // =================================== @@ -294,9 +295,10 @@ html { body { background-color: map-get($grays, "light"); + overflow-x: hidden; } -// Sidebar +// Sidebar .sidebar { width: 270px; position: fixed; @@ -308,6 +310,8 @@ body { .main-wrapper { transition: margin-left 0.3s ease; + overflow-x: hidden; + min-width: 0; } // Overlay gelap untuk mobile sidebar @@ -634,9 +638,11 @@ nav { border-radius: $border-radius-sm; &-container { - background: linear-gradient(135deg, - rgba(map-get($grays, "light"), 0.5) 0%, - rgba(map-get($grays, "light"), 0.8) 100%); + background: linear-gradient( + 135deg, + rgba(map-get($grays, "light"), 0.5) 0%, + rgba(map-get($grays, "light"), 0.8) 100% + ); border-radius: $border-radius-sm 0 0 $border-radius-sm; } } @@ -679,17 +685,13 @@ nav { --bs-btn-border-color: #{map-get($theme-colors, "primary")}; --bs-btn-hover-color: #{map-get($grays, "dark")}; --bs-btn-hover-bg: #{color.adjust( - map-get($theme-colors, "primary"), - $lightness: -5%) -} - -; ---bs-btn-hover-border-color: #{color.adjust( - map-get($theme-colors, "primary"), -$lightness: -7.5%) -} - -; + map-get($theme-colors, "primary"), + $lightness: -5% + )}; + --bs-btn-hover-border-color: #{color.adjust( + map-get($theme-colors, "primary"), + $lightness: -7.5% + )}; } // Text clamp untuk truncate multi line @@ -829,9 +831,11 @@ $lightness: -7.5%) // Background gradient hero section .hero-gradient { - background: linear-gradient(135deg, - map-get($theme-colors, "primary") 0%, - color.adjust(map-get($theme-colors, "primary"), $lightness: 10%) 100%); + background: linear-gradient( + 135deg, + map-get($theme-colors, "primary") 0%, + color.adjust(map-get($theme-colors, "primary"), $lightness: 10%) 100% + ); } // Card untuk pilih role (admin/user) @@ -851,9 +855,11 @@ $lightness: -7.5%) // Panel info di halaman auth .info-panel { - background: linear-gradient(135deg, - map-get($theme-colors, "primary") 0%, - color.adjust(map-get($theme-colors, "primary"), $lightness: 10%) 100%); + background: linear-gradient( + 135deg, + map-get($theme-colors, "primary") 0%, + color.adjust(map-get($theme-colors, "primary"), $lightness: 10%) 100% + ); } // Panel kiri auth (logo dan branding) @@ -906,7 +912,7 @@ $lightness: -7.5%) @media (max-width: 991.98px) { .auth-left-panel { background-image: none !important; - background-color: #0C5495; + background-color: #0c5495; position: absolute !important; min-height: 40vh; } @@ -936,7 +942,6 @@ $lightness: -7.5%) // Override styling DataTables .dataTables_wrapper { - .dataTables_length, .dataTables_filter, .dataTables_info, @@ -953,7 +958,8 @@ $lightness: -7.5%) &:focus { outline: none; border-color: map-get($theme-colors, "primary"); - box-shadow: 0 0 0 0.2rem rgba(map-get($theme-colors, "primary"), 0.25); + box-shadow: 0 0 0 0.2rem + rgba(map-get($theme-colors, "primary"), 0.25); } } @@ -968,7 +974,6 @@ $lightness: -7.5%) // Responsive DataTables untuk mobile @media screen and (max-width: 576px) { .dataTables_wrapper { - .dataTables_length, .dataTables_filter { text-align: center; @@ -1003,11 +1008,10 @@ $lightness: -7.5%) } // =================================== -// PROFILE PAGE +// PROFILE PAGE // =================================== - -// Avatar +// Avatar .profile-avatar-lg { width: 80px; height: 80px; diff --git a/resources/views/admin/buku/create.blade.php b/resources/views/admin/buku/create.blade.php index 28f6534..e90bd52 100644 --- a/resources/views/admin/buku/create.blade.php +++ b/resources/views/admin/buku/create.blade.php @@ -28,7 +28,7 @@ placeholder="Masukkan nama penulis" required>
-
+
-
+
-
+
+
+ + +
diff --git a/resources/views/admin/buku/edit.blade.php b/resources/views/admin/buku/edit.blade.php index 4d4cad3..26cb511 100644 --- a/resources/views/admin/buku/edit.blade.php +++ b/resources/views/admin/buku/edit.blade.php @@ -29,7 +29,7 @@ value="{{ old('penulis', $buku->penulis) }}" required>
-
+
-
+
+
+ + +
@php diff --git a/resources/views/admin/buku/index.blade.php b/resources/views/admin/buku/index.blade.php index 697367e..b166ad1 100644 --- a/resources/views/admin/buku/index.blade.php +++ b/resources/views/admin/buku/index.blade.php @@ -24,7 +24,7 @@ @@ -48,14 +48,17 @@ - @forelse($bukuOffline as $buku) - + @foreach($bukuOffline as $buku) + {{ $loop->iteration }} {{ $buku['judul'] }} {{ $buku['judul'] }} {{ $buku['kode_buku'] }} {{ $buku['penulis'] }} + + {{ $buku['stok'] ?? 0 }} + @if ($buku['status'] == 'Tersedia') Dipinjam data-kode_buku="{{ $buku['kode_buku'] }}" data-penulis="{{ $buku['penulis'] }}" data-kategori="{{ $buku->category->name ?? '-' }}" - data-tahun="{{ $buku['tahun'] }}" data-status="{{ $buku['status'] }}"> + data-tahun="{{ $buku['tahun'] }}" data-status="{{ $buku['status'] }}" data-stok="{{ $buku['stok'] ?? 0 }}"> Detail
- @empty - - Tidak ada data buku offline. - - - @endforelse + @endforeach
@@ -109,8 +107,8 @@ class="badge bg-warning-subtle text-warning-emphasis">Dipinjam - @forelse($bukuOnline as $buku) - + @foreach($bukuOnline as $buku) + {{ $loop->iteration }} {{ $buku['judul'] }} @@ -119,28 +117,24 @@ class="badge bg-warning-subtle text-warning-emphasis">Dipinjam {{ $buku['file_pdf'] ?? 'N/A' }} - - + +
- @empty - - Tidak ada data buku online. - - @endforelse + @endforeach @@ -161,12 +155,35 @@ class="badge bg-info-subtle text-info-emphasis">{{ $buku['file_pdf'] ?? 'N/A' }} + @forelse($bukuArsip as $buku) + + {{ $loop->iteration }} + {{ $buku['judul'] }} + {{ $buku->judul }} + {{ $buku->penulis }} + + @if(in_array('offline', $buku->tipe_akses ?? [])) + Offline + @else + Online + @endif + + + + + + @empty Belum ada buku yang diarsipkan. + @endforelse @@ -257,26 +274,27 @@ function updateTableNumbers() { // LOGIC MODAL DETAIL $('#detailBukuModal').on('show.bs.modal', function(event) { const button = $(event.relatedTarget); - const buku = button.data('buku'); - $('#modalCover').attr('src', "{{ asset('') }}" + buku.cover); - $('#modalJudulContent').text(buku.judul); - $('#modalPenulis').text('Penulis: ' + buku.penulis); - $('#modalKategori').text(buku.kategori); - $('#modalTahun').text(buku.tahun); - $('#modalStok').text(buku.stok ? buku.stok + ' Buku' : '-'); + $('#modalCover').attr('src', button.data('cover')); + $('#modalJudulContent').text(button.data('judul')); + $('#modalPenulis').text('Penulis: ' + button.data('penulis')); + $('#modalKategori').text(button.data('kategori')); + $('#modalTahun').text(button.data('tahun')); + $('#modalStok').text(button.data('stok') ? button.data('stok') + ' Buku' : '-'); - if (buku.kode_buku) { + const kodeBuku = button.data('kode_buku'); + if (kodeBuku) { $('#rowKodeBuku').show(); - $('#modalKode').text(buku.kode_buku); + $('#modalKode').text(kodeBuku); } else { $('#rowKodeBuku').hide(); } // Status Badge const statusBadge = $('#modalStatus'); + const status = button.data('status'); statusBadge.removeClass().addClass('badge'); - if (buku.status === 'Tersedia' || !buku.status) { + if (status === 'Tersedia' || status === 'Dapat Dibaca Online' || !status) { statusBadge.addClass('bg-success-subtle text-success-emphasis').text('Tersedia / Online'); } else { statusBadge.addClass('bg-warning-subtle text-warning-emphasis').text('Dipinjam'); @@ -287,12 +305,12 @@ function updateTableNumbers() { // LOGIC ARSIPKAN BUKU $(document).on('click', '.btn-arsipkan', function() { - const row = $(this).closest('tr'); - const judul = $(this).data('judul'); + const button = $(this); + const row = button.closest('tr'); + const id = button.data('id'); + const judul = button.data('judul'); const tipe = row.data('tipe'); - const rowData = row.prop('outerHTML'); - modernSwal.fire({ title: 'Arsipkan Buku?', text: `Buku "${judul}" akan dipindahkan ke tab Arsip.`, @@ -303,40 +321,53 @@ function updateTableNumbers() { confirmButtonColor: '#ffc107', }).then((result) => { if (result.isConfirmed) { - row.fadeOut(300, function() { - const coverHtml = row.find('td:eq(1)').html(); - const penulis = row.find('td:eq(3)').text(); - const badgeTipe = tipe === 'offline' ? - 'Offline' : - 'Online'; + $.ajax({ + url: "{{ route('admin.buku.arsip') }}", + type: "POST", + data: { + _token: "{{ csrf_token() }}", + id: id + }, + success: function(response) { + row.fadeOut(300, function() { + const coverHtml = row.find('td:eq(1)').html(); + const penulis = button.data('penulis'); + const badgeTipe = tipe === 'offline' ? + 'Offline' : + 'Online'; - const originalDataEncoded = encodeURIComponent(rowData); + const arsipRow = ` + + + ${coverHtml} + ${judul} + ${penulis} + ${badgeTipe} + + + + + `; - const arsipRow = ` - - - ${coverHtml} - ${judul} - ${penulis} - ${badgeTipe} - - - - - `; - - $('#tableArsip tbody').append(arsipRow); - $(this).remove(); - - updateTableNumbers(); - Toast.fire({ - icon: 'success', - title: 'Diarsipkan', - text: `Buku "${judul}" berhasil diarsipkan.` - }); + $('#tableArsip tbody').append(arsipRow); + if ($('.empty-row-arsip').length) $('.empty-row-arsip').hide(); + + row.remove(); + updateTableNumbers(); + + Toast.fire({ + icon: 'success', + title: 'Diarsipkan', + text: response.message + }); + }); + }, + error: function(xhr) { + modernSwal.fire('Error', 'Gagal mengarsipkan buku.', 'error'); + } }); } }); @@ -344,9 +375,11 @@ function updateTableNumbers() { // LOGIC PULIHKAN BUKU $(document).on('click', '.btn-pulihkan', function() { - const row = $(this).closest('tr'); - const judul = $(this).data('judul'); - const originalDataEncoded = row.data('origin'); + const button = $(this); + const row = button.closest('tr'); + const id = button.data('id'); + const judul = button.data('judul'); + const tipe = row.data('tipe'); modernSwal.fire({ title: 'Kembalikan Buku?', @@ -358,24 +391,37 @@ function updateTableNumbers() { confirmButtonColor: '#198754', }).then((result) => { if (result.isConfirmed) { - row.fadeOut(300, function() { - const originalRowHtml = decodeURIComponent(originalDataEncoded); - const $originalRow = $(originalRowHtml); - - const tipe = $originalRow.data('tipe'); - const targetTable = tipe === 'offline' ? '#tableOffline' : '#tableOnline'; - - $originalRow.removeAttr('style'); - - $(targetTable + ' tbody').append($originalRow); - $(this).remove(); - - updateTableNumbers(); - Toast.fire({ - icon: 'success', - title: 'Dipulihkan', - text: `Buku "${judul}" aktif kembali.` - }); + $.ajax({ + url: "{{ route('admin.buku.pulihkan') }}", + type: "POST", + data: { + _token: "{{ csrf_token() }}", + id: id + }, + success: function(response) { + row.fadeOut(300, function() { + const targetTable = tipe === 'offline' ? '#tableOffline' : '#tableOnline'; + + // Because we don't have the original row data stored locally in a perfect way, + // the easiest way is to reload or just fade out and tell user it's back. + // For better UX without reload, we could hide it and notify. + + row.remove(); + updateTableNumbers(); + + Toast.fire({ + icon: 'success', + title: 'Dipulihkan', + text: response.message + ' Silakan refresh untuk melihat di daftar aktif.' + }); + + // Optionally reload to show it in the right place with all data + setTimeout(() => location.reload(), 1500); + }); + }, + error: function(xhr) { + modernSwal.fire('Error', 'Gagal memulihkan buku.', 'error'); + } }); } }); diff --git a/resources/views/admin/denda/index.blade.php b/resources/views/admin/denda/index.blade.php index e964fc0..5cdfc13 100644 --- a/resources/views/admin/denda/index.blade.php +++ b/resources/views/admin/denda/index.blade.php @@ -107,21 +107,23 @@ class="btn btn-sm btn-success text-white" title="Tagih via WhatsApp"> - {{-- LOGIC TOMBOL AKTIFKAN / SANKSI --}} - @if ($item['is_banned']) - {{-- Jika sudah dibekukan (Otomatis/Manual), muncul tombol AKTIFKAN --}} - - @else - {{-- Jika belum dibekukan, muncul tombol SANKSI (Manual) --}} - + {{-- LOGIC TOMBOL AKTIFKAN / SANKSI (Hanya untuk Siswa) --}} + @if (!$item['is_guru']) + @if ($item['is_banned']) + {{-- Jika sudah dibekukan (Otomatis/Manual), muncul tombol AKTIFKAN --}} + + @else + {{-- Jika belum dibekukan, muncul tombol SANKSI (Manual) --}} + + @endif @endif @@ -146,8 +148,14 @@ class="btn btn-sm btn-success text-white" title="Tagih via WhatsApp"> @endpush \ No newline at end of file diff --git a/resources/views/admin/pengguna/edit.blade.php b/resources/views/admin/pengguna/edit.blade.php index 7d38467..c7562cc 100644 --- a/resources/views/admin/pengguna/edit.blade.php +++ b/resources/views/admin/pengguna/edit.blade.php @@ -33,7 +33,7 @@
- @php $oldNomorInduk = old('nomor_induk', $pengguna->role == 'siswa' ? $pengguna->nisn : $pengguna->nip); @endphp + @php $oldNomorInduk = old('nomor_induk', $pengguna->nomor_induk); @endphp @@ -49,7 +49,7 @@
- +
@@ -120,13 +120,13 @@ function toggleFields() { }); }); - @if(session('success')) + Toast.fire({ icon: 'success', title: 'Berhasil', - text: '{{ session("success") }}' + text: '' }); - @endif + @endpush \ No newline at end of file diff --git a/resources/views/admin/pengguna/index.blade.php b/resources/views/admin/pengguna/index.blade.php index 2a41074..a4c498b 100644 --- a/resources/views/admin/pengguna/index.blade.php +++ b/resources/views/admin/pengguna/index.blade.php @@ -28,7 +28,7 @@
- +
@@ -47,7 +47,7 @@
No{{ $user->name }}
{{ $user->email }}
-
{{ $user->no_hp ?? +
{{ $user->phone ?? '-' }}
@@ -60,7 +60,7 @@ @endif - {{ $user->nisn ?? ($user->nip ?? '-') }} + {{ $user->nomor_induk ?? '-' }} @if($user->role == 'siswa') @@ -78,7 +78,7 @@ class="btn btn-sm btn-warning" title="Edit Pengguna"> @@ -295,19 +295,17 @@ class="form-delete-whitelist" data-induk="{{ $item->nomor_induk }}"> }); @if(session('success')) - document.addEventListener('DOMContentLoaded', function() { - Toast.fire({ - icon: 'success', - title: 'Berhasil', - text: '{{ session("success") }}' - }); + Toast.fire({ + icon: 'success', + title: 'Berhasil', + text: '{{ session("success") }}' }); @endif - // GENERATE OTP & LINK RESET + // GENERATE OTP & LINK RESET $(document).on('click', '.btn-reset-password', function() { const nama = $(this).data('nama'); - const otpCode = "678901"; + const userId = $(this).data('id'); const linkReset = "{{ route('reset.password-request') }}"; modernSwal.fire({ @@ -322,27 +320,44 @@ class="form-delete-whitelist" data-induk="{{ $item->nomor_induk }}"> if (result.isConfirmed) { modernSwal.fire({ title: 'Generating...', - timer: 1000, - didOpen: () => Swal.showLoading() - }).then(() => { - modernSwal.fire({ - title: 'OTP Berhasil Dibuat!', - html: ` -
-

Kode OTP:

-

${otpCode}

-

Link Reset:

-
- + didOpen: () => Swal.showLoading(), + allowOutsideClick: false + }); + + $.ajax({ + url: "{{ route('reset.generate-otp') }}", + method: 'POST', + data: { + _token: '{{ csrf_token() }}', + user_id: userId + }, + success: function(response) { + if (response.status === 'success') { + modernSwal.fire({ + title: 'OTP Berhasil Dibuat!', + html: ` +
+

Kode OTP:

+

${response.otp}

+

Link Reset:

+
+ +
-
-
- Silakan salin Kode OTP & Link di atas lalu kirim ke WhatsApp user. -
- `, - icon: 'success', - confirmButtonText: 'Selesai' - }); +
+ Salin Kode OTP & Link lalu kirim ke WhatsApp pengguna. +
+ `, + icon: 'success', + confirmButtonText: 'Selesai' + }); + } else { + modernSwal.fire('Gagal', response.message || 'Gagal membuat OTP.', 'error'); + } + }, + error: function() { + modernSwal.fire('Error', 'Terjadi kesalahan saat membuat OTP.', 'error'); + } }); } }); diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 896ca13..e168b72 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -71,7 +71,7 @@ {{-- BANTUAN / LUPA PASSWORD --}}

Mengalami kendala login?

- Hubungi Petugas via WA diff --git a/resources/views/auth/reset-password-request.blade.php b/resources/views/auth/reset-password-request.blade.php index 3aa623a..aca95b3 100644 --- a/resources/views/auth/reset-password-request.blade.php +++ b/resources/views/auth/reset-password-request.blade.php @@ -3,10 +3,15 @@

Verifikasi Kode OTP

-

Masukkan 6 digit kode yang diberikan Admin via WhatsApp.

+

Masukkan email Anda dan 6 digit kode yang diberikan Admin via WhatsApp.

+
+ + +
+ {{-- Hidden email passed from OTP step --}} + + {{-- Password Utama --}}
@@ -67,60 +75,86 @@ diff --git a/resources/views/profile/index.blade.php b/resources/views/profile/index.blade.php index efbe616..6acbbf1 100644 --- a/resources/views/profile/index.blade.php +++ b/resources/views/profile/index.blade.php @@ -88,50 +88,36 @@ class="btn btn-light text-start py-3 d-flex align-items-center">
-
+
Foto Profil

{{ $user->name }}

- {{ - Str::title($user->role) }} + + {{ Str::title($user->role) }} +
-
-
Informasi Personal
-
-
- NIP / NUPTK -

{{ $user->nuptk ?? ($user->nomor_induk ?? 'N/A') }}

-
-
- Email -

{{ $user->email }}

-
-
- Nomor HP -

{{ $user->phone ?? 'N/A' }}

-
-
-
+
+
Informasi Personal
-
- NIP/NIK -

{{ $user->nip ?? $user->nisn ?? '-' }}

+
+ NIP / NIK / NUPTK +

{{ $user->nomor_induk ?? '-' }}

-
+
Email

{{ $user->email }}

-
+
Nomor HP

{{ $user->no_hp ?? '-' }}

@@ -195,39 +181,42 @@ class="list-group-item px-0 py-3 d-flex justify-content-between align-items-cent
-
+
Foto Profil

{{ $user->name }}

- {{ - Str::title($user->role) }} + + {{ Str::title($user->role) }} +
-
-
Informasi Personal
-
-
- NISN -

{{ $user->nomor_induk ?? 'N/A' }}

-
-
- Email -

{{ $user->email }}

-
-
- Nomor HP -

{{ $user->phone ?? 'N/A' }}

-
-
- Kelas -

{{ $user->kelas ?? 'N/A' }}

-
+
+ +
+ +
Informasi Personal
+
+
+ NISN +

{{ $user->nomor_induk ?? '-' }}

+
+
+ Email +

{{ $user->email }}

+
+
+ Nomor HP +

{{ $user->no_hp ?? '-' }}

+
+
+ Kelas +

{{ $user->kelas ?? '-' }}

diff --git a/routes/web.php b/routes/web.php index b9ea350..dc3a4fc 100644 --- a/routes/web.php +++ b/routes/web.php @@ -25,6 +25,7 @@ // Auth Controllers use App\Http\Controllers\Auth\AdminLoginController; +use App\Http\Controllers\Auth\ResetPasswordController; /* |-------------------------------------------------------------------------- @@ -87,6 +88,8 @@ Route::get('/buku/tambah', [AdminBookController::class, 'create'])->name('buku.create'); Route::post('/buku', [AdminBookController::class, 'store'])->name('buku.store'); Route::get('/buku/{id}/edit', [AdminBookController::class, 'edit'])->name('buku.edit'); + Route::post('/buku/arsip', [AdminBookController::class, 'arsip'])->name('buku.arsip'); + Route::post('/buku/pulihkan', [AdminBookController::class, 'pulihkan'])->name('buku.pulihkan'); Route::get('/pengguna', [AdminUserController::class, 'index'])->name('pengguna.index'); Route::get('/pengguna/tambah', [AdminUserController::class, 'create'])->name('pengguna.create'); @@ -115,7 +118,9 @@ Route::get('/peminjaman', [AdminPeminjamanController::class, 'index'])->name('peminjaman.index'); Route::get('/peminjaman/tambah', [AdminPeminjamanController::class, 'create'])->name('peminjaman.create'); - + Route::post('/peminjaman', [AdminPeminjamanController::class, 'store'])->name('peminjaman.store'); + Route::post('/peminjaman/kembali', [AdminPeminjamanController::class, 'kembalikan'])->name('peminjaman.kembali'); + Route::get('/peminjaman/export', [AdminPeminjamanController::class, 'export'])->name('peminjaman.export'); Route::get('/denda', [AdminPeminjamanController::class, 'dendaIndex'])->name('denda.index'); Route::post('/denda/sanksi', [AdminPeminjamanController::class, 'berikanSanksi'])->name('denda.sanksi'); @@ -134,4 +139,9 @@ return view('auth.reset-password-request'); })->name('reset.password-request'); +// OTP-based password reset API endpoints +Route::post('/reset-password/generate-otp', [ResetPasswordController::class, 'generateOtp'])->name('reset.generate-otp')->middleware('auth'); +Route::post('/reset-password/verify-otp', [ResetPasswordController::class, 'verifyOtp'])->name('reset.verify-otp'); +Route::post('/reset-password/update', [ResetPasswordController::class, 'updatePassword'])->name('reset.update-password'); + require __DIR__ . '/auth.php';