feat: Implement book stock and archive functionality, add password reset, and refine UI styling.

This commit is contained in:
cukiprit 2026-03-04 14:35:18 +07:00
parent f63628e48e
commit 528c120fc3
25 changed files with 733 additions and 359 deletions

View File

@ -8,6 +8,7 @@
use App\Models\User; use App\Models\User;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AdminPeminjamanController extends Controller class AdminPeminjamanController extends Controller
{ {
@ -24,7 +25,7 @@ public function index(Request $request)
$firstLoan = $userLoans->first(); $firstLoan = $userLoans->first();
return [ return [
'id_peminjaman' => 'PIN-ADM-' . sprintf('%03d', $userId), 'id_peminjaman' => 'PIN-ADM-'.sprintf('%03d', $userId),
'user_id' => $userId, 'user_id' => $userId,
'peminjam' => $user->nama_lengkap ?? 'Unknown', 'peminjam' => $user->nama_lengkap ?? 'Unknown',
'nomor_hp' => $user->phone ?? '-', 'nomor_hp' => $user->phone ?? '-',
@ -32,7 +33,7 @@ public function index(Request $request)
'tenggat_kembali' => $firstLoan->due_at, 'tenggat_kembali' => $firstLoan->due_at,
'status' => $firstLoan->status, 'status' => $firstLoan->status,
'role' => $user->role, 'role' => $user->role,
'books' => $userLoans->map(fn($l) => [ 'books' => $userLoans->map(fn ($l) => [
'id' => $l->book->id, 'id' => $l->book->id,
'judul' => $l->book->judul, 'judul' => $l->book->judul,
'cover' => $l->book->cover, 'cover' => $l->book->cover,
@ -63,11 +64,11 @@ public function create()
$user->disabled = $user->kena_limit || $user->is_banned; $user->disabled = $user->kena_limit || $user->is_banned;
if ($user->is_banned) { if ($user->is_banned) {
$user->status_text = "(Akun Dibekukan)"; $user->status_text = '(Akun Dibekukan)';
} elseif ($user->kena_limit) { } elseif ($user->kena_limit) {
$user->status_text = "(Limit Penuh: 2/2)"; $user->status_text = '(Limit Penuh: 2/2)';
} else { } else {
$user->status_text = ""; $user->status_text = '';
} }
return $user; return $user;
@ -75,6 +76,7 @@ public function create()
$daftarBuku = Book::where('status', 'Tersedia') $daftarBuku = Book::where('status', 'Tersedia')
->whereJsonContains('tipe_akses', 'offline') ->whereJsonContains('tipe_akses', 'offline')
->where('stok', '>', 0)
->get(); ->get();
return view('admin.peminjaman.create', [ return view('admin.peminjaman.create', [
@ -104,28 +106,92 @@ public function store(Request $request)
throw new \Exception("Buku '{$book->judul}' tidak tersedia."); 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 // Create loan record
Loan::create([ Loan::create([
'user_id' => $validated['peminjam_id'], 'user_id' => $validated['peminjam_id'],
'book_id' => $bookId, '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'], 'borrowed_at' => $validated['tanggal_pinjam'],
'due_at' => $validated['tanggal_kembali'], 'due_at' => $validated['tanggal_kembali'],
'status' => 'Dipinjam', 'status' => 'Dipinjam',
]); ]);
// Update book status // Update book status and decrement stock
$book->update(['status' => 'Dipinjam']); $book->update([
'status' => 'Dipinjam',
'stok' => $book->stok - 1,
]);
} }
\DB::commit(); \DB::commit();
return redirect()->route('admin.peminjaman.index')->with('success', 'Peminjaman berhasil dibuat.'); return redirect()->route('admin.peminjaman.index')->with('success', 'Peminjaman berhasil dibuat.');
} catch (\Exception $e) { } catch (\Exception $e) {
\DB::rollBack(); \DB::rollBack();
return back()->withErrors(['error' => $e->getMessage()])->withInput(); 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() public function dendaIndex()
{ {
$now = Carbon::now(); $now = Carbon::now();
@ -157,17 +223,19 @@ public function dendaIndex()
$hp = $user->phone ?? ''; $hp = $user->phone ?? '';
$waLink = '#'; $waLink = '#';
if ($hp) { 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) { 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."; $pesan = "Halo Bapak/Ibu {$user->nama_lengkap}, anda terlambat pengembalian buku selama {$hariTelat} hari. Mohon segera dikembalikan ke perpustakaan. Terima kasih.";
} else { } else {
$pesan = $hariTelat > 0 $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."; : "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 [ return [
@ -182,7 +250,7 @@ public function dendaIndex()
'wa_link' => $waLink, 'wa_link' => $waLink,
'is_banned' => $user->is_banned, 'is_banned' => $user->is_banned,
'tenggat_kembali' => $firstLoan->due_at, 'tenggat_kembali' => $firstLoan->due_at,
'books' => $userLoans->map(fn($l) => [ 'books' => $userLoans->map(fn ($l) => [
'id' => $l->book->id, 'id' => $l->book->id,
'judul' => $l->book->judul, 'judul' => $l->book->judul,
])->toArray(), ])->toArray(),
@ -194,7 +262,7 @@ public function dendaIndex()
return view('admin.denda.index', [ return view('admin.denda.index', [
'pageTitle' => 'Manajemen Denda & Sanksi', 'pageTitle' => 'Manajemen Denda & Sanksi',
'siswaTelat' => $siswaTelat, 'siswaTelat' => $siswaTelat,
'listKelas' => $listKelas 'listKelas' => $listKelas,
]); ]);
} }
@ -211,7 +279,7 @@ public function berikanSanksi(Request $request)
return response()->json([ return response()->json([
'status' => 'success', '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'], 'return_notes' => $item['notes'],
]); ]);
// Update book status // Update book status and increment stock
$loan->book->update(['status' => 'Tersedia']); $loan->book->update([
'status' => 'Tersedia',
'stok' => $loan->book->stok + 1,
]);
} }
} }
\DB::commit(); \DB::commit();
return response()->json(['status' => 'success', 'message' => 'Buku berhasil dikembalikan.']); return response()->json(['status' => 'success', 'message' => 'Buku berhasil dikembalikan.']);
} catch (\Exception $e) { } catch (\Exception $e) {
\DB::rollBack(); \DB::rollBack();
return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500); return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500);
} }
} }

View File

@ -12,29 +12,33 @@ class BookController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
$filters = $request->only(['search']); $filters = $request->only(['search']);
$query = Book::with('category'); $query = Book::with('category');
if ($request->filled('search')) { if ($request->filled('search')) {
$query->where('judul', 'like', '%' . $request->search . '%'); $query->where('judul', 'like', '%'.$request->search.'%');
} }
$semuaBuku = $query->latest()->get(); $semuaBuku = $query->latest()->get();
// Memisahkan buku menjadi dua koleksi: online dan offline secara independen
$bukuOnline = $semuaBuku->filter(function ($buku) { $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) { $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', [ return view('admin.buku.index', [
'pageTitle' => 'Manajemen Buku', 'pageTitle' => 'Manajemen Buku',
'bukuOnline' => $bukuOnline, 'bukuOnline' => $bukuOnline,
'bukuOffline' => $bukuOffline, 'bukuOffline' => $bukuOffline,
'input' => $filters 'bukuArsip' => $bukuArsip,
'input' => $filters,
]); ]);
} }
@ -45,7 +49,7 @@ public function create()
{ {
return view('admin.buku.create', [ return view('admin.buku.create', [
'pageTitle' => 'Tambah Buku Baru', 'pageTitle' => 'Tambah Buku Baru',
'categories' => Category::all() 'categories' => Category::all(),
]); ]);
} }
@ -54,9 +58,9 @@ public function edit($id)
$buku = Book::with('category')->findOrFail($id); $buku = Book::with('category')->findOrFail($id);
return view('admin.buku.edit', [ return view('admin.buku.edit', [
'pageTitle' => 'Edit Buku: ' . $buku->judul, 'pageTitle' => 'Edit Buku: '.$buku->judul,
'buku' => $buku, 'buku' => $buku,
'categories' => Category::all() 'categories' => Category::all(),
]); ]);
} }
@ -68,6 +72,7 @@ public function store(Request $request)
'category_id' => 'required|exists:categories,id', 'category_id' => 'required|exists:categories,id',
'tahun' => 'required|integer', 'tahun' => 'required|integer',
'kode_buku' => 'nullable|string', 'kode_buku' => 'nullable|string',
'stok' => 'required|integer|min:0',
'tipe_akses' => 'required|array', 'tipe_akses' => 'required|array',
'cover' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'cover' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
'file_pdf' => 'nullable|mimes:pdf|max:10240', 'file_pdf' => 'nullable|mimes:pdf|max:10240',
@ -75,7 +80,7 @@ public function store(Request $request)
if ($request->hasFile('cover')) { if ($request->hasFile('cover')) {
$path = $request->file('cover')->store('covers', 'public'); $path = $request->file('cover')->store('covers', 'public');
$validated['cover'] = 'storage/' . $path; $validated['cover'] = 'storage/'.$path;
} }
if ($request->hasFile('file_pdf')) { if ($request->hasFile('file_pdf')) {
@ -98,6 +103,7 @@ public function update(Request $request, $id)
'category_id' => 'required|exists:categories,id', 'category_id' => 'required|exists:categories,id',
'tahun' => 'required|integer', 'tahun' => 'required|integer',
'kode_buku' => 'nullable|string', 'kode_buku' => 'nullable|string',
'stok' => 'required|integer|min:0',
'tipe_akses' => 'required|array', 'tipe_akses' => 'required|array',
'cover' => 'nullable|image|mimes:jpeg,png,jpg|max:2048', 'cover' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
'file_pdf' => 'nullable|mimes:pdf|max:10240', 'file_pdf' => 'nullable|mimes:pdf|max:10240',
@ -105,7 +111,7 @@ public function update(Request $request, $id)
if ($request->hasFile('cover')) { if ($request->hasFile('cover')) {
$path = $request->file('cover')->store('covers', 'public'); $path = $request->file('cover')->store('covers', 'public');
$validated['cover'] = 'storage/' . $path; $validated['cover'] = 'storage/'.$path;
} }
if ($request->hasFile('file_pdf')) { if ($request->hasFile('file_pdf')) {
@ -125,4 +131,20 @@ public function destroy($id)
return redirect()->route('admin.buku.index')->with('success', 'Buku berhasil dihapus.'); return redirect()->route('admin.buku.index')->with('success', 'Buku berhasil dihapus.');
} }
}
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.']);
}
}

View File

@ -52,6 +52,7 @@ public function store(Request $request)
'phone' => 'nullable|string|max:20', 'phone' => 'nullable|string|max:20',
'role' => 'required|in:siswa,guru,penjaga perpus', 'role' => 'required|in:siswa,guru,penjaga perpus',
'kelas' => 'nullable|string|max:50', 'kelas' => 'nullable|string|max:50',
'golongan' => 'nullable|string|max:50',
'password' => 'required|string|min:8|confirmed', 'password' => 'required|string|min:8|confirmed',
]); ]);
@ -74,6 +75,7 @@ public function update(Request $request, $id)
'phone' => 'nullable|string|max:20', 'phone' => 'nullable|string|max:20',
'role' => 'required|in:siswa,guru,penjaga perpus', 'role' => 'required|in:siswa,guru,penjaga perpus',
'kelas' => 'nullable|string|max:50', 'kelas' => 'nullable|string|max:50',
'golongan' => 'nullable|string|max:50',
'password' => 'nullable|string|min:8|confirmed', 'password' => 'nullable|string|min:8|confirmed',
]); ]);

View File

@ -3,8 +3,8 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\MasterInduk; use App\Models\MasterInduk;
use App\Models\User;
use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -19,6 +19,7 @@ class RegisteredUserController extends Controller
public function create(Request $request): View public function create(Request $request): View
{ {
$role = $request->query('role', 'siswa'); $role = $request->query('role', 'siswa');
return view('auth.register', ['role' => $role]); return view('auth.register', ['role' => $role]);
} }
@ -26,7 +27,6 @@ public function store(Request $request): RedirectResponse
{ {
$role = $request->input('role'); $role = $request->input('role');
$rules = [ $rules = [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
@ -43,27 +43,24 @@ public function store(Request $request): RedirectResponse
$request->validate($rules); $request->validate($rules);
$nomorInduk = ($role === 'siswa') ? $request->nisn : $request->nip; $nomorInduk = ($role === 'siswa') ? $request->nisn : $request->nip;
$isWhitelisted = MasterInduk::where('nomor_induk', $nomorInduk) $isWhitelisted = MasterInduk::where('nomor_induk', $nomorInduk)
->where('role', $role) ->where('role', $role)
->exists(); ->exists();
if (!$isWhitelisted) { if (! $isWhitelisted) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
($role === 'siswa' ? 'nisn' : 'nip') => ['Nomor induk tidak terdaftar di Data Sekolah. Hubungi petugas.'], ($role === 'siswa' ? 'nisn' : 'nip') => ['Nomor induk tidak terdaftar di Data Sekolah. Hubungi petugas.'],
]); ]);
} }
$user = User::create([ $user = User::create([
'name' => $request->name, 'name' => $request->name,
'email' => $request->email, 'email' => $request->email,
'password' => Hash::make($request->password), 'password' => Hash::make($request->password),
'role' => $role, 'role' => $role,
'nisn' => ($role === 'siswa') ? $nomorInduk : null, 'nomor_induk' => $nomorInduk,
'nip' => ($role === 'guru') ? $nomorInduk : null,
]); ]);
event(new Registered($user)); event(new Registered($user));

View File

@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
class ResetPasswordController extends Controller
{
/**
* Generate an OTP for a specific user and store it in cache.
* Called by admin from the user management page.
*/
public function generateOtp(Request $request)
{
$request->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.',
]);
}
}

View File

@ -18,7 +18,7 @@ class ProfileController extends Controller
/** /**
* Tampilkan halaman profil user. * Tampilkan halaman profil user.
*/ */
public function index(Request $request): View public function index(Request $request): \Illuminate\View\View|\Illuminate\Http\RedirectResponse
{ {
$user = Auth::user(); $user = Auth::user();
if (!$user) { if (!$user) {
@ -62,7 +62,22 @@ public function index(Request $request): View
// Analytics for Guru (simplified for profile view) // Analytics for Guru (simplified for profile view)
$viewData['laporan'] = [ $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.' 'insight' => 'Siswa aktif meminjam buku kategori Sains.'
]; ];
} else { } else {

View File

@ -8,7 +8,7 @@ class Book extends Model
{ {
protected $fillable = [ protected $fillable = [
'judul', 'penulis', 'cover', 'kode_buku', '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 = [ protected $casts = [

View File

@ -37,7 +37,7 @@ public static function getAllSiswa(): array
'nisn' => '9988776655', 'nisn' => '9988776655',
'nama_lengkap' => 'Siti Nurhaliza', 'nama_lengkap' => 'Siti Nurhaliza',
'email' => 'siti.nurhaliza@smkn1perpus.sch.id', 'email' => 'siti.nurhaliza@smkn1perpus.sch.id',
'nomor_hp' => '081998877665', 'nomor_hp' => '0895618643811',
'password' => 'password', 'password' => 'password',
'role' => 'siswa', 'role' => 'siswa',
'kelas' => 'XII RPL A', 'kelas' => 'XII RPL A',

View File

@ -11,12 +11,8 @@
*/ */
public function up(): void public function up(): void
{ {
Schema::create('master_induks', function (Blueprint $table) { Schema::table('books', function (Blueprint $table) {
$table->id(); $table->integer('stok')->default(1)->after('status');
$table->string('nomor_induk')->unique();
$table->string('nama_pemilik');
$table->string('role');
$table->timestamps();
}); });
} }
@ -25,6 +21,8 @@ public function up(): void
*/ */
public function down(): void public function down(): void
{ {
Schema::dropIfExists('master_induks'); Schema::table('books', function (Blueprint $table) {
//
});
} }
}; };

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('books', function (Blueprint $table) {
$table->boolean('is_arsip')->default(false)->after('stok');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('books', function (Blueprint $table) {
//
});
}
};

View File

@ -2,11 +2,10 @@
namespace Database\Seeders; namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Book; use App\Models\Book;
use App\Models\Category; use App\Models\Category;
use App\Services\DummyDataService; use App\Services\DummyDataService;
use Illuminate\Support\Str; use Illuminate\Database\Seeder;
class BookSeeder extends Seeder class BookSeeder extends Seeder
{ {
@ -20,6 +19,8 @@ public function run(): void
foreach ($books as $data) { foreach ($books as $data) {
$category = Category::where('name', $data['kategori'])->first(); $category = Category::where('name', $data['kategori'])->first();
$stok = isset($data['stok']) ? $data['stok'] : (($data['status'] === 'Tersedia') ? 1 : 0);
Book::updateOrCreate( Book::updateOrCreate(
['kode_buku' => $data['kode_buku']], ['kode_buku' => $data['kode_buku']],
[ [
@ -30,6 +31,7 @@ public function run(): void
'category_id' => $category ? $category->id : null, 'category_id' => $category ? $category->id : null,
'tahun' => $data['tahun'], 'tahun' => $data['tahun'],
'status' => $data['status'], 'status' => $data['status'],
'stok' => $stok,
'is_new' => $data['is_new'], 'is_new' => $data['is_new'],
'tipe_akses' => is_array($data['tipe_akses']) ? $data['tipe_akses'] : [$data['tipe_akses']], 'tipe_akses' => is_array($data['tipe_akses']) ? $data['tipe_akses'] : [$data['tipe_akses']],
] ]

View File

@ -2,11 +2,11 @@
namespace Database\Seeders; namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\MasterInduk; 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\DB;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
@ -29,7 +29,7 @@ public function run()
MasterInduk::create($w); MasterInduk::create($w);
} }
// ISI USER ASLI // ISI USER ASLI
// ID 1: Silvi (Siswa) // ID 1: Silvi (Siswa)
User::create([ User::create([
@ -38,10 +38,10 @@ public function run()
'email' => 'silvi.rahmawati@smkn1perpus.sch.id', 'email' => 'silvi.rahmawati@smkn1perpus.sch.id',
'password' => Hash::make('password'), 'password' => Hash::make('password'),
'role' => 'siswa', 'role' => 'siswa',
'nisn' => '1234567890', 'nomor_induk' => '1234567890',
'no_hp' => '08123456789', 'phone' => '08123456789',
'kelas' => 'XII RPL', 'kelas' => 'XII RPL',
'golongan' => 'A' 'golongan' => 'A',
]); ]);
// ID 2: Budi (Admin/Penjaga) // ID 2: Budi (Admin/Penjaga)
@ -51,7 +51,7 @@ public function run()
'email' => 'budi.santoso@smkn1perpus.sch.id', 'email' => 'budi.santoso@smkn1perpus.sch.id',
'password' => Hash::make('password'), 'password' => Hash::make('password'),
'role' => 'penjaga perpus', 'role' => 'penjaga perpus',
'nip' => '197812312005011', 'nomor_induk' => '197812312005011',
]); ]);
// ID 3: Siti (Siswa) // ID 3: Siti (Siswa)
@ -61,10 +61,10 @@ public function run()
'email' => 'siti.nurhaliza@smkn1perpus.sch.id', 'email' => 'siti.nurhaliza@smkn1perpus.sch.id',
'password' => Hash::make('password'), 'password' => Hash::make('password'),
'role' => 'siswa', 'role' => 'siswa',
'nisn' => '9988776655', 'nomor_induk' => '9988776655',
'no_hp' => '081998877665', 'phone' => '0895618643811',
'kelas' => 'XII RPL', 'kelas' => 'XII RPL',
'golongan' => 'B' 'golongan' => 'B',
]); ]);
// ID 4: Andi (Siswa) // ID 4: Andi (Siswa)
@ -74,10 +74,10 @@ public function run()
'email' => 'andi.pratama@smkn1perpus.sch.id', 'email' => 'andi.pratama@smkn1perpus.sch.id',
'password' => Hash::make('password'), 'password' => Hash::make('password'),
'role' => 'siswa', 'role' => 'siswa',
'nisn' => '5566778899', 'nomor_induk' => '5566778899',
'no_hp' => '081556677889', 'phone' => '081556677889',
'kelas' => 'XII RPL', 'kelas' => 'XII RPL',
'golongan' => 'C' 'golongan' => 'C',
]); ]);
// ID 5: Rina (Guru) // ID 5: Rina (Guru)
@ -87,7 +87,15 @@ public function run()
'email' => 'rina.marlina@smkn1perpus.sch.id', 'email' => 'rina.marlina@smkn1perpus.sch.id',
'password' => Hash::make('password'), 'password' => Hash::make('password'),
'role' => 'guru', 'role' => 'guru',
'nip' => '198506152010012', 'nomor_induk' => '198506152010012',
]);
$this->call([
CategorySeeder::class,
BookSeeder::class,
AnnouncementSeeder::class,
RecommendationSeeder::class,
LoanSeeder::class,
]); ]);
} }
} }

View File

@ -4,8 +4,7 @@
// UTILITIES (GENERATED FROM MAPS) // UTILITIES (GENERATED FROM MAPS)
// =================================== // ===================================
// Generator otomatis untuk variant warna (background light, soft, alert) // Generator otomatis untuk variant warna (background light, soft, alert)
@each $color, @each $color, $value in $theme-colors {
$value in $theme-colors {
.bg-#{$color}-light { .bg-#{$color}-light {
background-color: rgba($value, 0.25) !important; background-color: rgba($value, 0.25) !important;
} }
@ -63,14 +62,14 @@ html {
.navbar-nav { .navbar-nav {
.nav-link-landing { .nav-link-landing {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
color: #49769F; color: #49769f;
text-decoration: none; text-decoration: none;
display: block; display: block;
transition: color 0.3s ease; transition: color 0.3s ease;
&:hover, &:hover,
&.active { &.active {
color: #0C5495; color: #0c5495;
font-weight: 500; font-weight: 500;
transition: all 0.3s ease-in; transition: all 0.3s ease-in;
} }
@ -109,9 +108,11 @@ html {
// Hero Section // Hero Section
.landing-hero-section { .landing-hero-section {
background: linear-gradient(135deg, background: linear-gradient(
#0C5495 0%, 135deg,
color.adjust(#0C5495, $lightness: 10%) 100%); #0c5495 0%,
color.adjust(#0c5495, $lightness: 10%) 100%
);
padding: 60px 0; padding: 60px 0;
@media (min-width: 992px) { @media (min-width: 992px) {
@ -143,7 +144,6 @@ html {
i { i {
font-size: 5rem; font-size: 5rem;
} }
} }
.card-img-top { .card-img-top {
@ -186,9 +186,11 @@ html {
// CTA Section // CTA Section
.landing-cta-section { .landing-cta-section {
background: linear-gradient(135deg, background: linear-gradient(
#0C5495 0%, 135deg,
color.adjust(#0C5495, $lightness: 10%) 100%); #0c5495 0%,
color.adjust(#0c5495, $lightness: 10%) 100%
);
border-radius: 1rem; border-radius: 1rem;
@media (min-width: 768px) { @media (min-width: 768px) {
@ -196,7 +198,6 @@ html {
} }
} }
// =================================== // ===================================
// BASE COMPONENTS // BASE COMPONENTS
// =================================== // ===================================
@ -294,9 +295,10 @@ html {
body { body {
background-color: map-get($grays, "light"); background-color: map-get($grays, "light");
overflow-x: hidden;
} }
// Sidebar // Sidebar
.sidebar { .sidebar {
width: 270px; width: 270px;
position: fixed; position: fixed;
@ -308,6 +310,8 @@ body {
.main-wrapper { .main-wrapper {
transition: margin-left 0.3s ease; transition: margin-left 0.3s ease;
overflow-x: hidden;
min-width: 0;
} }
// Overlay gelap untuk mobile sidebar // Overlay gelap untuk mobile sidebar
@ -634,9 +638,11 @@ nav {
border-radius: $border-radius-sm; border-radius: $border-radius-sm;
&-container { &-container {
background: linear-gradient(135deg, background: linear-gradient(
rgba(map-get($grays, "light"), 0.5) 0%, 135deg,
rgba(map-get($grays, "light"), 0.8) 100%); 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; 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-border-color: #{map-get($theme-colors, "primary")};
--bs-btn-hover-color: #{map-get($grays, "dark")}; --bs-btn-hover-color: #{map-get($grays, "dark")};
--bs-btn-hover-bg: #{color.adjust( --bs-btn-hover-bg: #{color.adjust(
map-get($theme-colors, "primary"), map-get($theme-colors, "primary"),
$lightness: -5%) $lightness: -5%
} )};
--bs-btn-hover-border-color: #{color.adjust(
; map-get($theme-colors, "primary"),
--bs-btn-hover-border-color: #{color.adjust( $lightness: -7.5%
map-get($theme-colors, "primary"), )};
$lightness: -7.5%)
}
;
} }
// Text clamp untuk truncate multi line // Text clamp untuk truncate multi line
@ -829,9 +831,11 @@ $lightness: -7.5%)
// Background gradient hero section // Background gradient hero section
.hero-gradient { .hero-gradient {
background: linear-gradient(135deg, background: linear-gradient(
map-get($theme-colors, "primary") 0%, 135deg,
color.adjust(map-get($theme-colors, "primary"), $lightness: 10%) 100%); map-get($theme-colors, "primary") 0%,
color.adjust(map-get($theme-colors, "primary"), $lightness: 10%) 100%
);
} }
// Card untuk pilih role (admin/user) // Card untuk pilih role (admin/user)
@ -851,9 +855,11 @@ $lightness: -7.5%)
// Panel info di halaman auth // Panel info di halaman auth
.info-panel { .info-panel {
background: linear-gradient(135deg, background: linear-gradient(
map-get($theme-colors, "primary") 0%, 135deg,
color.adjust(map-get($theme-colors, "primary"), $lightness: 10%) 100%); map-get($theme-colors, "primary") 0%,
color.adjust(map-get($theme-colors, "primary"), $lightness: 10%) 100%
);
} }
// Panel kiri auth (logo dan branding) // Panel kiri auth (logo dan branding)
@ -906,7 +912,7 @@ $lightness: -7.5%)
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
.auth-left-panel { .auth-left-panel {
background-image: none !important; background-image: none !important;
background-color: #0C5495; background-color: #0c5495;
position: absolute !important; position: absolute !important;
min-height: 40vh; min-height: 40vh;
} }
@ -936,7 +942,6 @@ $lightness: -7.5%)
// Override styling DataTables // Override styling DataTables
.dataTables_wrapper { .dataTables_wrapper {
.dataTables_length, .dataTables_length,
.dataTables_filter, .dataTables_filter,
.dataTables_info, .dataTables_info,
@ -953,7 +958,8 @@ $lightness: -7.5%)
&:focus { &:focus {
outline: none; outline: none;
border-color: map-get($theme-colors, "primary"); 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 // Responsive DataTables untuk mobile
@media screen and (max-width: 576px) { @media screen and (max-width: 576px) {
.dataTables_wrapper { .dataTables_wrapper {
.dataTables_length, .dataTables_length,
.dataTables_filter { .dataTables_filter {
text-align: center; text-align: center;
@ -1003,11 +1008,10 @@ $lightness: -7.5%)
} }
// =================================== // ===================================
// PROFILE PAGE // PROFILE PAGE
// =================================== // ===================================
// Avatar
// Avatar
.profile-avatar-lg { .profile-avatar-lg {
width: 80px; width: 80px;
height: 80px; height: 80px;

View File

@ -28,7 +28,7 @@
placeholder="Masukkan nama penulis" required> placeholder="Masukkan nama penulis" required>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4 mb-3"> <div class="col-md-3 mb-3">
<label for="category_id" class="form-label">Kategori</label> <label for="category_id" class="form-label">Kategori</label>
<select name="category_id" class="form-select" id="category_id" required> <select name="category_id" class="form-select" id="category_id" required>
<option value="" disabled selected>Pilih Kategori</option> <option value="" disabled selected>Pilih Kategori</option>
@ -37,16 +37,21 @@
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="col-md-4 mb-3"> <div class="col-md-3 mb-3">
<label for="tahun" class="form-label">Tahun Terbit</label> <label for="tahun" class="form-label">Tahun Terbit</label>
<input type="number" name="tahun" class="form-control" id="tahun" <input type="number" name="tahun" class="form-control" id="tahun"
placeholder="Contoh: 2024" min="0" required> placeholder="Contoh: 2024" min="0" required>
</div> </div>
<div class="col-md-4 mb-3"> <div class="col-md-3 mb-3">
<label for="kode_buku" class="form-label">Kode Buku</label> <label for="kode_buku" class="form-label">Kode Buku</label>
<input type="text" name="kode_buku" class="form-control" id="kode_buku" <input type="text" name="kode_buku" class="form-control" id="kode_buku"
placeholder="Contoh: 330"> placeholder="Contoh: 330">
</div> </div>
<div class="col-md-3 mb-3">
<label for="stok" class="form-label">Stok</label>
<input type="number" name="stok" class="form-control" id="stok"
value="1" min="0" required>
</div>
</div> </div>
</div> </div>

View File

@ -29,7 +29,7 @@
value="{{ old('penulis', $buku->penulis) }}" required> value="{{ old('penulis', $buku->penulis) }}" required>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-4 mb-3">
<label for="category_id" class="form-label">Kategori</label> <label for="category_id" class="form-label">Kategori</label>
<select name="category_id" class="form-select" id="category_id" required> <select name="category_id" class="form-select" id="category_id" required>
@foreach($categories as $category) @foreach($categories as $category)
@ -39,11 +39,16 @@
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-4 mb-3">
<label for="tahun" class="form-label">Tahun Terbit</label> <label for="tahun" class="form-label">Tahun Terbit</label>
<input type="number" name="tahun" class="form-control" id="tahun" <input type="number" name="tahun" class="form-control" id="tahun"
value="{{ old('tahun', $buku->tahun) }}" required> value="{{ old('tahun', $buku->tahun) }}" required>
</div> </div>
<div class="col-md-4 mb-3">
<label for="stok" class="form-label">Stok</label>
<input type="number" name="stok" class="form-control" id="stok"
value="{{ old('stok', $buku->stok ?? 1) }}" min="0" required>
</div>
</div> </div>
@php @php

View File

@ -24,7 +24,7 @@
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link text-warning" id="arsip-tab" data-bs-toggle="tab" <button class="nav-link text-warning" id="arsip-tab" data-bs-toggle="tab"
data-bs-target="#arsip-tab-pane" type="button" role="tab"> data-bs-target="#arsip-tab-pane" type="button" role="tab">
<i class="bi bi-archive-fill me-1"></i>Diarsipkan (<span id="countArsip">0</span>) <i class="bi bi-archive-fill me-1"></i>Diarsipkan (<span id="countArsip">{{ $bukuArsip->count() }}</span>)
</button> </button>
</li> </li>
</ul> </ul>
@ -48,14 +48,17 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse($bukuOffline as $buku) @foreach($bukuOffline as $buku)
<tr> <tr data-tipe="offline" data-id="{{ $buku->id }}">
<td>{{ $loop->iteration }}</td> <td>{{ $loop->iteration }}</td>
<td><img src="{{ asset($buku['cover']) }}" alt="{{ $buku['judul'] }}" <td><img src="{{ asset($buku['cover']) }}" alt="{{ $buku['judul'] }}"
width="50" class="rounded"></td> width="50" class="rounded"></td>
<td>{{ $buku['judul'] }}</td> <td>{{ $buku['judul'] }}</td>
<td>{{ $buku['kode_buku'] }}</td> <td>{{ $buku['kode_buku'] }}</td>
<td>{{ $buku['penulis'] }}</td> <td>{{ $buku['penulis'] }}</td>
<td>
<span class="badge bg-info-subtle text-info-emphasis">{{ $buku['stok'] ?? 0 }}</span>
</td>
<td> <td>
@if ($buku['status'] == 'Tersedia') @if ($buku['status'] == 'Tersedia')
<span <span
@ -73,22 +76,17 @@ class="badge bg-warning-subtle text-warning-emphasis">Dipinjam</span>
data-kode_buku="{{ $buku['kode_buku'] }}" data-kode_buku="{{ $buku['kode_buku'] }}"
data-penulis="{{ $buku['penulis'] }}" data-penulis="{{ $buku['penulis'] }}"
data-kategori="{{ $buku->category->name ?? '-' }}" 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 }}">
<i class="bi bi-eye-fill"></i> Detail <i class="bi bi-eye-fill"></i> Detail
</button> </button>
<button class="btn btn-sm btn-outline-warning btn-arsipkan" <button class="btn btn-sm btn-outline-warning btn-arsipkan"
data-judul="{{ $buku['judul'] }}" title="Arsipkan Buku"> data-id="{{ $buku->id }}" data-judul="{{ $buku->judul }}" data-penulis="{{ $buku->penulis }}" title="Arsipkan Buku">
<i class="bi bi-archive-fill"></i> Arsip <i class="bi bi-archive-fill"></i> Arsip
</button> </button>
</div> </div>
</td> </td>
</tr> </tr>
@empty @endforeach
<tr class="empty-row">
<td colspan="8" class="text-center py-4 text-muted">Tidak ada data buku offline.
</td>
</tr>
@endforelse
</tbody> </tbody>
</table> </table>
</div> </div>
@ -109,8 +107,8 @@ class="badge bg-warning-subtle text-warning-emphasis">Dipinjam</span>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse($bukuOnline as $buku) @foreach($bukuOnline as $buku)
<tr> <tr data-tipe="online" data-id="{{ $buku->id }}">
<td>{{ $loop->iteration }}</td> <td>{{ $loop->iteration }}</td>
<td><img src="{{ asset($buku['cover']) }}" alt="{{ $buku['judul'] }}" <td><img src="{{ asset($buku['cover']) }}" alt="{{ $buku['judul'] }}"
width="50" class="rounded"></td> width="50" class="rounded"></td>
@ -119,28 +117,24 @@ class="badge bg-warning-subtle text-warning-emphasis">Dipinjam</span>
<td><span <td><span
class="badge bg-info-subtle text-info-emphasis">{{ $buku['file_pdf'] ?? 'N/A' }}</span> class="badge bg-info-subtle text-info-emphasis">{{ $buku['file_pdf'] ?? 'N/A' }}</span>
</td> </td>
<td> <td>
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" <button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal"
data-bs-target="#detailBukuModal" data-id="{{ $buku['id'] }}" data-bs-target="#detailBukuModal" data-id="{{ $buku['id'] }}"
data-cover="{{ asset($buku['cover']) }}" data-cover="{{ asset($buku['cover']) }}"
data-judul="{{ $buku['judul'] }}" data-judul="{{ $buku['judul'] }}"
data-penulis="{{ $buku['penulis'] }}" data-penulis="{{ $buku['penulis'] }}"
data-kategori="{{ $buku->category->name ?? '-' }}" data-kategori="{{ $buku->category->name ?? '-' }}"
data-tahun="{{ $buku['tahun'] }}" data-status="Dapat Dibaca Online"> data-tahun="{{ $buku['tahun'] }}" data-status="Dapat Dibaca Online" data-stok="{{ $buku['stok'] ?? 0 }}">
<i class="bi bi-eye-fill"></i> Detail <i class="bi bi-eye-fill"></i> Detail
</button> </button>
<button class="btn btn-sm btn-outline-warning btn-arsipkan" <button class="btn btn-sm btn-outline-warning btn-arsipkan"
data-judul="{{ $buku['judul'] }}" title="Arsipkan Buku"> data-id="{{ $buku->id }}" data-judul="{{ $buku->judul }}" data-penulis="{{ $buku->penulis }}" title="Arsipkan Buku">
<i class="bi bi-archive-fill"></i> Arsip <i class="bi bi-archive-fill"></i> Arsip
</button> </button>
</div> </div>
</td> </td>
</tr> </tr>
@empty @endforeach
<tr class="empty-row">
<td colspan="6" class="text-center py-4 text-muted">Tidak ada data buku online.</td>
</tr>
@endforelse
</tbody> </tbody>
</table> </table>
</div> </div>
@ -161,12 +155,35 @@ class="badge bg-info-subtle text-info-emphasis">{{ $buku['file_pdf'] ?? 'N/A' }}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse($bukuArsip as $buku)
<tr data-id="{{ $buku->id }}" data-tipe="{{ in_array('offline', $buku->tipe_akses ?? []) ? 'offline' : 'online' }}">
<td class="row-number">{{ $loop->iteration }}</td>
<td><img src="{{ asset($buku['cover']) }}" alt="{{ $buku['judul'] }}"
width="50" class="rounded"></td>
<td class="fw-bold text-muted">{{ $buku->judul }}</td>
<td class="text-muted">{{ $buku->penulis }}</td>
<td>
@if(in_array('offline', $buku->tipe_akses ?? []))
<span class="badge bg-secondary"><i class="bi bi-book me-1"></i>Offline</span>
@else
<span class="badge bg-info"><i class="bi bi-globe me-1"></i>Online</span>
@endif
</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-success btn-pulihkan"
data-id="{{ $buku->id }}" data-judul="{{ $buku->judul }}" title="Kembalikan ke Daftar Aktif">
<i class="bi bi-arrow-counterclockwise me-1"></i>Kembalikan
</button>
</td>
</tr>
@empty
<tr class="empty-row-arsip"> <tr class="empty-row-arsip">
<td colspan="6" class="text-center py-5 text-muted"> <td colspan="6" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-2 text-secondary opacity-50"></i> <i class="bi bi-inbox fs-1 d-block mb-2 text-secondary opacity-50"></i>
Belum ada buku yang diarsipkan. Belum ada buku yang diarsipkan.
</td> </td>
</tr> </tr>
@endforelse
</tbody> </tbody>
</table> </table>
</div> </div>
@ -257,26 +274,27 @@ function updateTableNumbers() {
// LOGIC MODAL DETAIL // LOGIC MODAL DETAIL
$('#detailBukuModal').on('show.bs.modal', function(event) { $('#detailBukuModal').on('show.bs.modal', function(event) {
const button = $(event.relatedTarget); const button = $(event.relatedTarget);
const buku = button.data('buku');
$('#modalCover').attr('src', "{{ asset('') }}" + buku.cover); $('#modalCover').attr('src', button.data('cover'));
$('#modalJudulContent').text(buku.judul); $('#modalJudulContent').text(button.data('judul'));
$('#modalPenulis').text('Penulis: ' + buku.penulis); $('#modalPenulis').text('Penulis: ' + button.data('penulis'));
$('#modalKategori').text(buku.kategori); $('#modalKategori').text(button.data('kategori'));
$('#modalTahun').text(buku.tahun); $('#modalTahun').text(button.data('tahun'));
$('#modalStok').text(buku.stok ? buku.stok + ' Buku' : '-'); $('#modalStok').text(button.data('stok') ? button.data('stok') + ' Buku' : '-');
if (buku.kode_buku) { const kodeBuku = button.data('kode_buku');
if (kodeBuku) {
$('#rowKodeBuku').show(); $('#rowKodeBuku').show();
$('#modalKode').text(buku.kode_buku); $('#modalKode').text(kodeBuku);
} else { } else {
$('#rowKodeBuku').hide(); $('#rowKodeBuku').hide();
} }
// Status Badge // Status Badge
const statusBadge = $('#modalStatus'); const statusBadge = $('#modalStatus');
const status = button.data('status');
statusBadge.removeClass().addClass('badge'); 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'); statusBadge.addClass('bg-success-subtle text-success-emphasis').text('Tersedia / Online');
} else { } else {
statusBadge.addClass('bg-warning-subtle text-warning-emphasis').text('Dipinjam'); statusBadge.addClass('bg-warning-subtle text-warning-emphasis').text('Dipinjam');
@ -287,12 +305,12 @@ function updateTableNumbers() {
// LOGIC ARSIPKAN BUKU // LOGIC ARSIPKAN BUKU
$(document).on('click', '.btn-arsipkan', function() { $(document).on('click', '.btn-arsipkan', function() {
const row = $(this).closest('tr'); const button = $(this);
const judul = $(this).data('judul'); const row = button.closest('tr');
const id = button.data('id');
const judul = button.data('judul');
const tipe = row.data('tipe'); const tipe = row.data('tipe');
const rowData = row.prop('outerHTML');
modernSwal.fire({ modernSwal.fire({
title: 'Arsipkan Buku?', title: 'Arsipkan Buku?',
text: `Buku "${judul}" akan dipindahkan ke tab Arsip.`, text: `Buku "${judul}" akan dipindahkan ke tab Arsip.`,
@ -303,40 +321,53 @@ function updateTableNumbers() {
confirmButtonColor: '#ffc107', confirmButtonColor: '#ffc107',
}).then((result) => { }).then((result) => {
if (result.isConfirmed) { if (result.isConfirmed) {
row.fadeOut(300, function() { $.ajax({
const coverHtml = row.find('td:eq(1)').html(); url: "{{ route('admin.buku.arsip') }}",
const penulis = row.find('td:eq(3)').text(); type: "POST",
const badgeTipe = tipe === 'offline' ? data: {
'<span class="badge bg-secondary"><i class="bi bi-book me-1"></i>Offline</span>' : _token: "{{ csrf_token() }}",
'<span class="badge bg-info"><i class="bi bi-globe me-1"></i>Online</span>'; 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' ?
'<span class="badge bg-secondary"><i class="bi bi-book me-1"></i>Offline</span>' :
'<span class="badge bg-info"><i class="bi bi-globe me-1"></i>Online</span>';
const originalDataEncoded = encodeURIComponent(rowData); const arsipRow = `
<tr data-id="${id}" data-tipe="${tipe}">
<td class="row-number"></td>
<td>${coverHtml}</td>
<td class="fw-bold text-muted">${judul}</td>
<td class="text-muted">${penulis}</td>
<td>${badgeTipe}</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-success btn-pulihkan"
data-id="${id}" data-judul="${judul}" title="Kembalikan ke Daftar Aktif">
<i class="bi bi-arrow-counterclockwise me-1"></i>Kembalikan
</button>
</td>
</tr>
`;
const arsipRow = ` $('#tableArsip tbody').append(arsipRow);
<tr data-origin="${originalDataEncoded}"> if ($('.empty-row-arsip').length) $('.empty-row-arsip').hide();
<td class="row-number"></td>
<td>${coverHtml}</td> row.remove();
<td class="fw-bold text-muted">${judul}</td> updateTableNumbers();
<td class="text-muted">${penulis}</td>
<td>${badgeTipe}</td> Toast.fire({
<td class="text-center"> icon: 'success',
<button class="btn btn-sm btn-outline-success btn-pulihkan" title: 'Diarsipkan',
data-judul="${judul}" title="Kembalikan ke Daftar Aktif"> text: response.message
<i class="bi bi-arrow-counterclockwise me-1"></i>Kembalikan });
</button> });
</td> },
</tr> error: function(xhr) {
`; modernSwal.fire('Error', 'Gagal mengarsipkan buku.', 'error');
}
$('#tableArsip tbody').append(arsipRow);
$(this).remove();
updateTableNumbers();
Toast.fire({
icon: 'success',
title: 'Diarsipkan',
text: `Buku "${judul}" berhasil diarsipkan.`
});
}); });
} }
}); });
@ -344,9 +375,11 @@ function updateTableNumbers() {
// LOGIC PULIHKAN BUKU // LOGIC PULIHKAN BUKU
$(document).on('click', '.btn-pulihkan', function() { $(document).on('click', '.btn-pulihkan', function() {
const row = $(this).closest('tr'); const button = $(this);
const judul = $(this).data('judul'); const row = button.closest('tr');
const originalDataEncoded = row.data('origin'); const id = button.data('id');
const judul = button.data('judul');
const tipe = row.data('tipe');
modernSwal.fire({ modernSwal.fire({
title: 'Kembalikan Buku?', title: 'Kembalikan Buku?',
@ -358,24 +391,37 @@ function updateTableNumbers() {
confirmButtonColor: '#198754', confirmButtonColor: '#198754',
}).then((result) => { }).then((result) => {
if (result.isConfirmed) { if (result.isConfirmed) {
row.fadeOut(300, function() { $.ajax({
const originalRowHtml = decodeURIComponent(originalDataEncoded); url: "{{ route('admin.buku.pulihkan') }}",
const $originalRow = $(originalRowHtml); type: "POST",
data: {
const tipe = $originalRow.data('tipe'); _token: "{{ csrf_token() }}",
const targetTable = tipe === 'offline' ? '#tableOffline' : '#tableOnline'; id: id
},
$originalRow.removeAttr('style'); success: function(response) {
row.fadeOut(300, function() {
$(targetTable + ' tbody').append($originalRow); const targetTable = tipe === 'offline' ? '#tableOffline' : '#tableOnline';
$(this).remove();
// Because we don't have the original row data stored locally in a perfect way,
updateTableNumbers(); // the easiest way is to reload or just fade out and tell user it's back.
Toast.fire({ // For better UX without reload, we could hide it and notify.
icon: 'success',
title: 'Dipulihkan', row.remove();
text: `Buku "${judul}" aktif kembali.` 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');
}
}); });
} }
}); });

View File

@ -107,21 +107,23 @@ class="btn btn-sm btn-success text-white" title="Tagih via WhatsApp">
<i class="bi bi-whatsapp"></i> <i class="bi bi-whatsapp"></i>
</a> </a>
{{-- LOGIC TOMBOL AKTIFKAN / SANKSI --}} {{-- LOGIC TOMBOL AKTIFKAN / SANKSI (Hanya untuk Siswa) --}}
@if ($item['is_banned']) @if (!$item['is_guru'])
{{-- Jika sudah dibekukan (Otomatis/Manual), muncul tombol AKTIFKAN --}} @if ($item['is_banned'])
<button class="btn btn-sm btn-outline-success btn-aktifkan" {{-- Jika sudah dibekukan (Otomatis/Manual), muncul tombol AKTIFKAN --}}
data-user-id="{{ $item['user_id'] }}" <button class="btn btn-sm btn-outline-success btn-aktifkan"
data-nama="{{ $item['peminjam'] }}" title="Aktifkan Kembali Akun"> data-user-id="{{ $item['user_id'] }}"
<i class="bi bi-shield-check"></i> Aktifkan data-nama="{{ $item['peminjam'] }}" title="Aktifkan Kembali Akun">
</button> <i class="bi bi-shield-check"></i> Aktifkan
@else </button>
{{-- Jika belum dibekukan, muncul tombol SANKSI (Manual) --}} @else
<button class="btn btn-sm btn-outline-danger btn-sanksi" {{-- Jika belum dibekukan, muncul tombol SANKSI (Manual) --}}
data-user-id="{{ $item['user_id'] }}" <button class="btn btn-sm btn-outline-danger btn-sanksi"
data-nama="{{ $item['peminjam'] }}" title="Berikan Sanksi"> data-user-id="{{ $item['user_id'] }}"
<i class="bi bi-slash-circle"></i> Sanksi data-nama="{{ $item['peminjam'] }}" title="Berikan Sanksi">
</button> <i class="bi bi-slash-circle"></i> Sanksi
</button>
@endif
@endif @endif
</div> </div>
</td> </td>
@ -146,8 +148,14 @@ class="btn btn-sm btn-success text-white" title="Tagih via WhatsApp">
<script> <script>
$(document).ready(function() { $(document).ready(function() {
var table = $('#dendaTable').DataTable({ var table = $('#dendaTable').DataTable({
order: [ order: [[3, 'desc']],
[3, 'desc'] columns: [
{ title: "NO", searchable: false, orderable: false },
{ title: "NAMA" },
{ title: "BUKU TERLAMBAT" },
{ title: "STATUS" },
{ title: "DENDA" },
{ title: "AKSI", searchable: false, orderable: false }
], ],
columnDefs: [{ columnDefs: [{
@ -265,7 +273,7 @@ function(settings, data, dataIndex) {
}); });
$.ajax({ $.ajax({
url: "{{ route('admin.denda.sanksi ') }}", url: "{{ route('admin.denda.sanksi') }}",
method: 'POST', method: 'POST',
data: { data: {
_token: '{{ csrf_token() }}', _token: '{{ csrf_token() }}',

View File

@ -7,11 +7,14 @@
<p class="text-muted mb-0">Daftar ini hanya menampilkan buku yang masih berstatus "Dipinjam".</p> <p class="text-muted mb-0">Daftar ini hanya menampilkan buku yang masih berstatus "Dipinjam".</p>
</div> </div>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<form action="#" method="GET" class="d-flex m-0 p-0 bg-white border rounded p-1" onsubmit="alert('Fitur download Excel sedang disiapkan tim Backend'); return false;"> <form action="{{ route('admin.peminjaman.export') }}" method="GET" class="d-flex m-0 p-0 bg-white border rounded p-1">
<input type="month" name="bulan_laporan" class="form-control form-control-sm border-0 me-1" required> <div class="input-group input-group-sm">
<button type="submit" class="btn btn-sm btn-success text-nowrap"> <span class="input-group-text bg-transparent border-0 text-muted small">Filter Bulan:</span>
<i class="bi bi-file-earmark-excel-fill me-1"></i>Excel <input type="date" name="bulan_laporan" class="form-control border-0" title="Pilih tanggal untuk filter bulan (Opsional)">
</button> <button type="submit" class="btn btn-success text-nowrap">
<i class="bi bi-file-earmark-excel-fill me-1"></i>Excel
</button>
</div>
</form> </form>
<a href="{{ route('admin.peminjaman.create') }}" class="btn btn-primary text-nowrap"> <a href="{{ route('admin.peminjaman.create') }}" class="btn btn-primary text-nowrap">
@ -166,9 +169,8 @@
$isGuru = ($transaksi['role'] ?? '') === 'guru'; $isGuru = ($transaksi['role'] ?? '') === 'guru';
$dendaTelat = 0; $dendaTelat = 0;
if ($isTerlambat && !$isGuru) { if ($isTerlambat && !$isGuru) {
if ($isTerlambat && !$isGuru) { $hari = $tenggat->startOfDay()->diffInDays($now->startOfDay());
$hari = $tenggat->startOfDay()->diffInDays($now->startOfDay()); $dendaTelat = $hari * 1000;
$dendaTelat = $hari * 1000;
} }
@endphp @endphp
{{-- Data Attribute untuk JS --}} {{-- Data Attribute untuk JS --}}
@ -233,8 +235,16 @@
$(document).ready(function() { $(document).ready(function() {
$('#peminjamanTable').DataTable({ $('#peminjamanTable').DataTable({
pageLength: 10, pageLength: 10,
order: [ order: [[0, 'asc']],
[0, 'asc'] columns: [
{ title: "NO" },
{ title: "ID PEMINJAMAN" },
{ title: "PEMINJAM (JABATAN/KELAS)" },
{ title: "JUDUL BUKU" },
{ title: "TGL. PINJAM" },
{ title: "TENGGAT KEMBALI" },
{ title: "STATUS" },
{ title: "AKSI" }
] ]
}); });
}); });
@ -382,7 +392,7 @@ function hitungTotalDenda(modal) {
function finishTransaction(returnsData, userId, waLink) { function finishTransaction(returnsData, userId, waLink) {
$.ajax({ $.ajax({
url: "{{ route('admin.peminjaman.kembali ') }}", url: "{{ route('admin.peminjaman.kembali') }}",
method: 'POST', method: 'POST',
data: { data: {
_token: '{{ csrf_token() }}', _token: '{{ csrf_token() }}',

View File

@ -59,8 +59,8 @@
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="no_hp" class="form-label">No. Handphone</label> <label for="no_hp" class="form-label">No. Handphone</label>
<input type="text" class="form-control" id="no_hp" name="no_hp" <input type="text" class="form-control" id="no_hp" name="phone"
value="{{ old('no_hp') }}" placeholder="Contoh: 08123456789"> value="{{ old('phone') }}" placeholder="Contoh: 08123456789">
</div> </div>
</div> </div>
@ -171,13 +171,13 @@ function toggleFields() {
}); });
}); });
@if(session('success')) <?php if(session('success')): ?>
Toast.fire({ Toast.fire({
icon: 'success', icon: 'success',
title: 'Berhasil', title: 'Berhasil',
text: '{{ session("success") }}' text: '<?php echo session("success"); ?>'
}); });
@endif <?php endif; ?>
</script> </script>
@endpush @endpush
</x-app-layout> </x-app-layout>

View File

@ -33,7 +33,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
@php $oldNomorInduk = old('nomor_induk', $pengguna->role == 'siswa' ? $pengguna->nisn : $pengguna->nip); @endphp @php $oldNomorInduk = old('nomor_induk', $pengguna->nomor_induk); @endphp
<label for="nomor_induk" class="form-label" id="label_nomor_induk">NISN / NIP</label> <label for="nomor_induk" class="form-label" id="label_nomor_induk">NISN / NIP</label>
<input type="number" class="form-control @error('nomor_induk') is-invalid @enderror" <input type="number" class="form-control @error('nomor_induk') is-invalid @enderror"
id="nomor_induk" name="nomor_induk" value="{{ $oldNomorInduk }}" placeholder="Masukkan Nomor Induk"> id="nomor_induk" name="nomor_induk" value="{{ $oldNomorInduk }}" placeholder="Masukkan Nomor Induk">
@ -49,7 +49,7 @@
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="no_hp" class="form-label">No. Handphone</label> <label for="no_hp" class="form-label">No. Handphone</label>
<input type="text" class="form-control" id="no_hp" name="no_hp" value="{{ old('no_hp', $pengguna->no_hp) }}"> <input type="text" class="form-control" id="phone" name="phone" value="{{ old('phone', $pengguna->phone) }}">
</div> </div>
</div> </div>
@ -120,13 +120,13 @@ function toggleFields() {
}); });
}); });
@if(session('success')) <?php if(session('success')): ?>
Toast.fire({ Toast.fire({
icon: 'success', icon: 'success',
title: 'Berhasil', title: 'Berhasil',
text: '{{ session("success") }}' text: '<?php echo session("success"); ?>'
}); });
@endif <?php endif; ?>
</script> </script>
@endpush @endpush
</x-app-layout> </x-app-layout>

View File

@ -28,7 +28,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-bordered align-middle" width="100%" cellspacing="0"> <table class="table table-bordered align-middle" cellspacing="0" style="min-width: 900px;">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th>No</th> <th>No</th>
@ -47,7 +47,7 @@
<td class="fw-bold">{{ $user->name }}</td> <td class="fw-bold">{{ $user->name }}</td>
<td> <td>
<div>{{ $user->email }}</div> <div>{{ $user->email }}</div>
<div class="small text-muted"><i class="bi bi-telephone me-1"></i>{{ $user->no_hp ?? <div class="small text-muted"><i class="bi bi-telephone me-1"></i>{{ $user->phone ??
'-' }}</div> '-' }}</div>
</td> </td>
<td> <td>
@ -60,7 +60,7 @@
@endif @endif
</td> </td>
<td class="font-monospace text-primary"> <td class="font-monospace text-primary">
{{ $user->nisn ?? ($user->nip ?? '-') }} {{ $user->nomor_induk ?? '-' }}
</td> </td>
<td> <td>
@if($user->role == 'siswa') @if($user->role == 'siswa')
@ -78,7 +78,7 @@ class="btn btn-sm btn-warning" title="Edit Pengguna">
</a> </a>
<button type="button" class="btn btn-sm btn-secondary btn-reset-password" <button type="button" class="btn btn-sm btn-secondary btn-reset-password"
data-nama="{{ $user->name }}" title="Reset Password (OTP)"> data-id="{{ $user->id }}" data-nama="{{ $user->name }}" title="Reset Password (OTP)">
<i class="bi bi-key-fill"></i> <i class="bi bi-key-fill"></i>
</button> </button>
@ -295,19 +295,17 @@ class="form-delete-whitelist" data-induk="{{ $item->nomor_induk }}">
}); });
@if(session('success')) @if(session('success'))
document.addEventListener('DOMContentLoaded', function() { Toast.fire({
Toast.fire({ icon: 'success',
icon: 'success', title: 'Berhasil',
title: 'Berhasil', text: '{{ session("success") }}'
text: '{{ session("success") }}'
});
}); });
@endif @endif
// GENERATE OTP & LINK RESET // GENERATE OTP & LINK RESET
$(document).on('click', '.btn-reset-password', function() { $(document).on('click', '.btn-reset-password', function() {
const nama = $(this).data('nama'); const nama = $(this).data('nama');
const otpCode = "678901"; const userId = $(this).data('id');
const linkReset = "{{ route('reset.password-request') }}"; const linkReset = "{{ route('reset.password-request') }}";
modernSwal.fire({ modernSwal.fire({
@ -322,27 +320,44 @@ class="form-delete-whitelist" data-induk="{{ $item->nomor_induk }}">
if (result.isConfirmed) { if (result.isConfirmed) {
modernSwal.fire({ modernSwal.fire({
title: 'Generating...', title: 'Generating...',
timer: 1000, didOpen: () => Swal.showLoading(),
didOpen: () => Swal.showLoading() allowOutsideClick: false
}).then(() => { });
modernSwal.fire({
title: 'OTP Berhasil Dibuat!', $.ajax({
html: ` url: "{{ route('reset.generate-otp') }}",
<div class="text-start bg-light p-3 rounded border"> method: 'POST',
<p class="mb-1 small text-muted">Kode OTP:</p> data: {
<h3 class="text-primary fw-bold letter-spacing-1 mb-3">${otpCode}</h3> _token: '{{ csrf_token() }}',
<p class="mb-1 small text-muted">Link Reset:</p> user_id: userId
<div class="input-group"> },
<input type="text" class="form-control form-control-sm" value="${linkReset}" readonly> success: function(response) {
if (response.status === 'success') {
modernSwal.fire({
title: 'OTP Berhasil Dibuat!',
html: `
<div class="text-start bg-light p-3 rounded border">
<p class="mb-1 small text-muted">Kode OTP:</p>
<h3 class="text-primary fw-bold letter-spacing-1 mb-3">${response.otp}</h3>
<p class="mb-1 small text-muted">Link Reset:</p>
<div class="input-group">
<input type="text" class="form-control form-control-sm" value="${linkReset}" readonly>
</div>
</div> </div>
</div> <div class="mt-3 small text-muted">
<div class="mt-3 small text-muted"> Salin Kode OTP & Link lalu kirim ke WhatsApp pengguna.
Silakan salin Kode OTP & Link di atas lalu kirim ke WhatsApp user. </div>
</div> `,
`, icon: 'success',
icon: 'success', confirmButtonText: 'Selesai'
confirmButtonText: 'Selesai' });
}); } else {
modernSwal.fire('Gagal', response.message || 'Gagal membuat OTP.', 'error');
}
},
error: function() {
modernSwal.fire('Error', 'Terjadi kesalahan saat membuat OTP.', 'error');
}
}); });
} }
}); });

View File

@ -71,7 +71,7 @@
{{-- BANTUAN / LUPA PASSWORD --}} {{-- BANTUAN / LUPA PASSWORD --}}
<div class="bg-light p-3 rounded-3 text-center border border-dashed"> <div class="bg-light p-3 rounded-3 text-center border border-dashed">
<p class="text-muted small mb-1">Mengalami kendala login?</p> <p class="text-muted small mb-1">Mengalami kendala login?</p>
<a href="https://wa.me/6281234567890?text=Halo%20Admin,%20saya%20lupa%20kata%20sandi%20akun%20saya." <a href="https://wa.me/62895618643811?text=Halo%20Admin,%20saya%20lupa%20kata%20sandi%20akun%20saya."
target="_blank" class="text-decoration-none fw-bold text-success d-inline-flex align-items-center"> target="_blank" class="text-decoration-none fw-bold text-success d-inline-flex align-items-center">
<i class="bi bi-whatsapp me-1"></i> Hubungi Petugas via WA <i class="bi bi-whatsapp me-1"></i> Hubungi Petugas via WA
</a> </a>

View File

@ -3,10 +3,15 @@
<div id="step-otp"> <div id="step-otp">
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<h4 class="fw-bold text-dark">Verifikasi Kode OTP</h4> <h4 class="fw-bold text-dark">Verifikasi Kode OTP</h4>
<p class="text-muted small">Masukkan 6 digit kode yang diberikan Admin via WhatsApp.</p> <p class="text-muted small">Masukkan email Anda dan 6 digit kode yang diberikan Admin via WhatsApp.</p>
</div> </div>
<form id="formVerifyOtp" onsubmit="verifikasiOTP(event)"> <form id="formVerifyOtp" onsubmit="verifikasiOTP(event)">
<div class="mb-3">
<label class="form-label fw-bold">Email Anda</label>
<input class="form-control" type="email" id="inputEmail"
placeholder="email@contoh.com" required autocomplete="email">
</div>
<div class="mb-4"> <div class="mb-4">
<label class="form-label fw-bold">Kode OTP</label> <label class="form-label fw-bold">Kode OTP</label>
<input class="form-control text-center fs-3 letter-spacing-2 py-2" type="text" id="inputOtp" <input class="form-control text-center fs-3 letter-spacing-2 py-2" type="text" id="inputOtp"
@ -35,6 +40,9 @@
</div> </div>
<form id="formResetPassword" onsubmit="simpanPassword(event)"> <form id="formResetPassword" onsubmit="simpanPassword(event)">
{{-- Hidden email passed from OTP step --}}
<input type="hidden" id="verifiedEmail">
{{-- Password Utama --}} {{-- Password Utama --}}
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Kata Sandi Baru</label> <label class="form-label">Kata Sandi Baru</label>
@ -67,60 +75,86 @@
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script> <script>
// Fungsi Toggle Lihat Password
function lihatPassword(targetId, btn) { function lihatPassword(targetId, btn) {
const input = document.getElementById(targetId); const input = document.getElementById(targetId);
const icon = btn.querySelector('i'); const icon = btn.querySelector('i');
if (input.type === 'password') { if (input.type === 'password') {
input.type = 'text'; input.type = 'text';
icon.classList.remove('bi-eye-slash-fill'); icon.classList.replace('bi-eye-slash-fill', 'bi-eye-fill');
icon.classList.add('bi-eye-fill');
} else { } else {
input.type = 'password'; input.type = 'password';
icon.classList.remove('bi-eye-fill'); icon.classList.replace('bi-eye-fill', 'bi-eye-slash-fill');
icon.classList.add('bi-eye-slash-fill');
} }
} }
// Fungsi Verifikasi OTP // Fungsi Verifikasi OTP (real backend call)
function verifikasiOTP(e) { function verifikasiOTP(e) {
e.preventDefault(); e.preventDefault();
const inputOtp = document.getElementById('inputOtp').value; const email = document.getElementById('inputEmail').value;
const otp = document.getElementById('inputOtp').value;
if(inputOtp !== '678901') { Swal.fire({ title: 'Memverifikasi...', didOpen: () => Swal.showLoading(), allowOutsideClick: false });
Swal.fire({ icon: 'error', title: 'Gagal', text: 'Kode OTP salah! Coba cek lagi WA Admin.' });
return;
}
Swal.fire({ title: 'Memverifikasi...', timer: 800, didOpen: () => Swal.showLoading() }).then(() => { fetch('{{ route("reset.verify-otp") }}', {
document.getElementById('step-otp').classList.add('d-none'); method: 'POST',
document.getElementById('step-password').classList.remove('d-none'); headers: {
document.getElementById('password').focus(); 'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 3000 }); },
Toast.fire({ icon: 'success', title: 'Kode OTP Valid' }); body: JSON.stringify({ email, otp })
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
// Pass verified email to the password step
document.getElementById('verifiedEmail').value = data.email;
document.getElementById('step-otp').classList.add('d-none');
document.getElementById('step-password').classList.remove('d-none');
document.getElementById('password').focus();
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 3000 });
Toast.fire({ icon: 'success', title: 'Kode OTP Valid' });
} else {
Swal.fire({ icon: 'error', title: 'Gagal', text: data.message || 'Kode OTP salah.' });
}
})
.catch(() => {
Swal.fire({ icon: 'error', title: 'Error', text: 'Terjadi kesalahan. Silakan coba lagi.' });
}); });
} }
// Fungsi Simpan Password // Fungsi Simpan Password (real backend call)
function simpanPassword(e) { function simpanPassword(e) {
e.preventDefault(); e.preventDefault();
const email = document.getElementById('verifiedEmail').value;
const pass = document.getElementById('password').value; const pass = document.getElementById('password').value;
const conf = document.getElementById('password_confirmation').value; const conf = document.getElementById('password_confirmation').value;
if(pass.length < 8) { if (pass.length < 8) { Swal.fire({ icon: 'warning', text: 'Kata sandi minimal 8 karakter!' }); return; }
Swal.fire({ icon: 'warning', text: 'Kata sandi minimal 8 karakter!' }); return; if (pass !== conf) { Swal.fire({ icon: 'warning', text: 'Konfirmasi kata sandi tidak cocok!' }); return; }
}
if(pass !== conf) {
Swal.fire({ icon: 'warning', text: 'Konfirmasi kata sandi tidak cocok!' }); return;
}
Swal.fire({ title: 'Menyimpan...', timer: 1500, didOpen: () => Swal.showLoading() }).then(() => { Swal.fire({ title: 'Menyimpan...', didOpen: () => Swal.showLoading(), allowOutsideClick: false });
Swal.fire({
icon: 'success', title: 'Berhasil!', text: 'Kata sandi Anda telah diperbarui. Silakan login.', fetch('{{ route("reset.update-password") }}', {
confirmButtonText: 'Ke Halaman Login', allowOutsideClick: false method: 'POST',
}).then(() => { window.location.href = "/login"; }); headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ email, password: pass, password_confirmation: conf })
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
Swal.fire({
icon: 'success', title: 'Berhasil!', text: 'Kata sandi Anda telah diperbarui. Silakan login.',
confirmButtonText: 'Ke Halaman Login', allowOutsideClick: false
}).then(() => { window.location.href = '/login'; });
} else {
Swal.fire({ icon: 'error', title: 'Gagal', text: data.message || 'Gagal menyimpan password.' });
}
})
.catch(() => {
Swal.fire({ icon: 'error', title: 'Error', text: 'Terjadi kesalahan. Silakan coba lagi.' });
}); });
} }
</script> </script>

View File

@ -88,50 +88,36 @@ class="btn btn-light text-start py-3 d-flex align-items-center">
<div class="col-lg-8 d-flex flex-column"> <div class="col-lg-8 d-flex flex-column">
<div class="card border-0 mb-3 mb-md-4"> <div class="card border-0 mb-3 mb-md-4">
<div class="card-body p-3 p-md-4"> <div class="card-body p-3 p-md-4">
<div class="d-flex flex-column flex-sm-row align-items-center text-center text-sm-start"> <div class="d-flex flex-column flex-sm-row align-items-center text-center text-sm-start mb-4">
<img src="https://ui-avatars.com/api/?name={{ urlencode($user->name) }}&background=198754&color=fff&size=80&rounded=true" <img src="https://ui-avatars.com/api/?name={{ urlencode($user->name) }}&background=198754&color=fff&size=80&rounded=true"
alt="Foto Profil" class="rounded-circle profile-avatar-lg mb-3 mb-sm-0"> alt="Foto Profil" class="rounded-circle profile-avatar-lg mb-3 mb-sm-0">
<div class="ms-sm-4 mb-3 mb-sm-0"> <div class="ms-sm-4 mb-3 mb-sm-0">
<h4 class="fw-bold mb-1">{{ $user->name }}</h4> <h4 class="fw-bold mb-1">{{ $user->name }}</h4>
<span class="badge rounded-pill bg-success-subtle text-success-emphasis">{{ <span class="badge rounded-pill bg-success-subtle text-success-emphasis">
Str::title($user->role) }}</span> {{ Str::title($user->role) }}
</span>
</div> </div>
<div class="ms-sm-auto"> <div class="ms-sm-auto">
<a href="{{ route('profile.edit') }}" <a href="{{ route('profile.edit') }}"
class="btn btn-outline-primary rounded-pill ms-sm-auto w-100 w-sm-auto"> class="btn btn-outline-primary rounded-pill w-100 w-sm-auto">
<i class="bi bi-pencil-square me-2"></i>Edit Profil <i class="bi bi-pencil-square me-2"></i>Edit Profil
</a> </a>
</div> </div>
<hr class="my-3 my-md-4">
<h5 class="fw-bold mb-3 px-4">Informasi Personal</h5>
<div class="row g-3 px-4 pb-4">
<div class="col-sm-6">
<small class="text-muted d-block mb-1">NIP / NUPTK</small>
<p class="fw-semibold mb-0">{{ $user->nuptk ?? ($user->nomor_induk ?? 'N/A') }}</p>
</div>
<div class="col-sm-6">
<small class="text-muted d-block mb-1">Email</small>
<p class="fw-semibold mb-0 text-break">{{ $user->email }}</p>
</div>
<div class="col-sm-6">
<small class="text-muted d-block mb-1">Nomor HP</small>
<p class="fw-semibold mb-0">{{ $user->phone ?? 'N/A' }}</p>
</div>
</div>
</div> </div>
<hr class="my-3 my-md-4"> <hr class="my-4">
<h5 class="fw-bold mb-3">Informasi Personal</h5> <h5 class="fw-bold mb-3">Informasi Personal</h5>
<div class="row g-3"> <div class="row g-3">
<div class="col-sm-6"> <div class="col-sm-6 col-md-4">
<small class="text-muted d-block mb-1">NIP/NIK</small> <small class="text-muted d-block mb-1">NIP / NIK / NUPTK</small>
<p class="fw-semibold mb-0">{{ $user->nip ?? $user->nisn ?? '-' }}</p> <p class="fw-semibold mb-0">{{ $user->nomor_induk ?? '-' }}</p>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6 col-md-4">
<small class="text-muted d-block mb-1">Email</small> <small class="text-muted d-block mb-1">Email</small>
<p class="fw-semibold mb-0 text-break">{{ $user->email }}</p> <p class="fw-semibold mb-0 text-break">{{ $user->email }}</p>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6 col-md-4">
<small class="text-muted d-block mb-1">Nomor HP</small> <small class="text-muted d-block mb-1">Nomor HP</small>
<p class="fw-semibold mb-0">{{ $user->no_hp ?? '-' }}</p> <p class="fw-semibold mb-0">{{ $user->no_hp ?? '-' }}</p>
</div> </div>
@ -195,39 +181,42 @@ class="list-group-item px-0 py-3 d-flex justify-content-between align-items-cent
<div class="col-lg-8 d-flex flex-column"> <div class="col-lg-8 d-flex flex-column">
<div class="card border-0 mb-3 mb-md-4"> <div class="card border-0 mb-3 mb-md-4">
<div class="card-body p-3 p-md-4"> <div class="card-body p-3 p-md-4">
<div class="d-flex flex-column flex-sm-row align-items-center text-center text-sm-start"> <div class="d-flex flex-column flex-sm-row align-items-center text-center text-sm-start mb-4">
<img src="https://ui-avatars.com/api/?name={{ urlencode($user->name) }}&background=435ebe&color=fff&size=80&rounded=true" <img src="https://ui-avatars.com/api/?name={{ urlencode($user->name) }}&background=435ebe&color=fff&size=80&rounded=true"
alt="Foto Profil" class="rounded-circle profile-avatar-lg mb-3 mb-sm-0"> alt="Foto Profil" class="rounded-circle profile-avatar-lg mb-3 mb-sm-0">
<div class="ms-sm-4 mb-3 mb-sm-0"> <div class="ms-sm-4 mb-3 mb-sm-0">
<h4 class="fw-bold mb-1">{{ $user->name }}</h4> <h4 class="fw-bold mb-1">{{ $user->name }}</h4>
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis">{{ <span class="badge rounded-pill bg-primary-subtle text-primary-emphasis">
Str::title($user->role) }}</span> {{ Str::title($user->role) }}
</span>
</div> </div>
<div class="ms-sm-auto"> <div class="ms-sm-auto">
<a href="{{ route('profile.edit') }}" <a href="{{ route('profile.edit') }}"
class="btn btn-outline-primary rounded-pill ms-md-auto"> class="btn btn-outline-primary rounded-pill w-100 w-sm-auto">
<i class="bi bi-pencil-square me-2"></i>Edit Profil <i class="bi bi-pencil-square me-2"></i>Edit Profil
</a> </a>
</div> </div>
<hr class="my-3 my-md-4"> </div>
<h5 class="fw-bold mb-3">Informasi Personal</h5>
<div class="row g-3"> <hr class="my-4">
<div class="col-sm-6">
<small class="text-muted d-block mb-1">NISN</small> <h5 class="fw-bold mb-3">Informasi Personal</h5>
<p class="fw-semibold mb-0">{{ $user->nomor_induk ?? 'N/A' }}</p> <div class="row g-3">
</div> <div class="col-sm-6 col-md-3">
<div class="col-sm-6"> <small class="text-muted d-block mb-1">NISN</small>
<small class="text-muted d-block mb-1">Email</small> <p class="fw-semibold mb-0">{{ $user->nomor_induk ?? '-' }}</p>
<p class="fw-semibold mb-0 text-break">{{ $user->email }}</p> </div>
</div> <div class="col-sm-6 col-md-3">
<div class="col-sm-6"> <small class="text-muted d-block mb-1">Email</small>
<small class="text-muted d-block mb-1">Nomor HP</small> <p class="fw-semibold mb-0 text-break">{{ $user->email }}</p>
<p class="fw-semibold mb-0">{{ $user->phone ?? 'N/A' }}</p> </div>
</div> <div class="col-sm-6 col-md-3">
<div class="col-sm-6"> <small class="text-muted d-block mb-1">Nomor HP</small>
<small class="text-muted d-block mb-1">Kelas</small> <p class="fw-semibold mb-0">{{ $user->no_hp ?? '-' }}</p>
<p class="fw-semibold mb-0">{{ $user->kelas ?? 'N/A' }}</p> </div>
</div> <div class="col-sm-6 col-md-3">
<small class="text-muted d-block mb-1">Kelas</small>
<p class="fw-semibold mb-0">{{ $user->kelas ?? '-' }}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -25,6 +25,7 @@
// Auth Controllers // Auth Controllers
use App\Http\Controllers\Auth\AdminLoginController; 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::get('/buku/tambah', [AdminBookController::class, 'create'])->name('buku.create');
Route::post('/buku', [AdminBookController::class, 'store'])->name('buku.store'); Route::post('/buku', [AdminBookController::class, 'store'])->name('buku.store');
Route::get('/buku/{id}/edit', [AdminBookController::class, 'edit'])->name('buku.edit'); 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', [AdminUserController::class, 'index'])->name('pengguna.index');
Route::get('/pengguna/tambah', [AdminUserController::class, 'create'])->name('pengguna.create'); 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', [AdminPeminjamanController::class, 'index'])->name('peminjaman.index');
Route::get('/peminjaman/tambah', [AdminPeminjamanController::class, 'create'])->name('peminjaman.create'); 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::get('/denda', [AdminPeminjamanController::class, 'dendaIndex'])->name('denda.index');
Route::post('/denda/sanksi', [AdminPeminjamanController::class, 'berikanSanksi'])->name('denda.sanksi'); Route::post('/denda/sanksi', [AdminPeminjamanController::class, 'berikanSanksi'])->name('denda.sanksi');
@ -134,4 +139,9 @@
return view('auth.reset-password-request'); return view('auth.reset-password-request');
})->name('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'; require __DIR__ . '/auth.php';