Feat: Finalize denda system, implemented manual sanction for teachers, fixed dashboard error, and updated UI modal

This commit is contained in:
zhadaarsita 2026-01-23 11:01:01 +07:00
parent 156a4e00a9
commit 779ef38952
21 changed files with 1336 additions and 829 deletions

View File

@ -29,14 +29,11 @@ public function retrieveById($identifier)
/**
* Rehash the user's password if required.
* Method ini wajib ada di Laravel 12. Kita biarkan kosong karena tidak relevan untuk data dummy.
*/
public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void
{
// Biarkan kosong
}
// Fungsi-fungsi di bawah ini tidak kita pakai, tapi harus ada.
public function retrieveByToken($identifier, $token) { return null; }
public function updateRememberToken(Authenticatable $user, $token) { }
public function retrieveByCredentials(array $credentials) { return null; }

View File

@ -9,36 +9,78 @@
class AdminPeminjamanController extends Controller
{
/**
* Menampilkan halaman utama manajemen peminjaman (Tabel).
*/
public function index(Request $request)
{
// Panggil data dari DummyDataService
$peminjamanAktif = DummyDataService::getAdminPeminjamanAktif();
// LOGIC DENDA & WA LINK (Update Request Client)
$peminjamanAktif = $peminjamanAktif->map(function ($item) {
// Hitung Denda Flat 1000/hari
$tenggat = Carbon::parse($item['tenggat_kembali']);
$now = Carbon::now();
$item['hari_terlambat'] = 0;
$item['total_denda'] = 0;
if ($now->greaterThan($tenggat)) {
$hariTelat = $tenggat->diffInDays($now);
$item['hari_terlambat'] = $hariTelat;
$item['total_denda'] = $hariTelat * 1000;
$item['denda_per_hari'] = 1000;
}
// Generate WA Link
$hp = $item['nomor_hp'];
if (substr($hp, 0, 1) == '0') {
$hp = '62' . substr($hp, 1);
}
$pesan = "Halo kak {$item['peminjam']}, buku anda sudah terlambat {$item['hari_terlambat']} hari dengan denda Rp " . number_format($item['total_denda'], 0, ',', '.') . ". Mohon segera dikembalikan ya.";
$item['wa_link'] = "https://wa.me/{$hp}?text=" . urlencode($pesan);
return $item;
});
$daftarPeminjam = $peminjamanAktif->pluck('peminjam')->unique();
return view('admin.peminjaman.index', [
'pageTitle' => 'Manajemen Peminjaman',
'pageTitle' => 'Manajemen Peminjaman & Denda',
'peminjamanAktif' => $peminjamanAktif,
'daftarPeminjam' => $daftarPeminjam,
]);
}
/**
* Menampilkan formulir untuk membuat peminjaman manual.
*/
public function create()
{
$allUsers = collect(DummyDataService::getAllSiswa());
$peminjamanAktif = DummyDataService::getAdminPeminjamanAktif();
// Kelompokkan berdasarkan 'role'
$groupedUsers = $allUsers
->whereIn('role', ['siswa', 'guru'])
->map(function ($user) use ($peminjamanAktif) {
// Hitung berapa buku yang sedang dipinjam
$jumlahPinjam = $peminjamanAktif->where('peminjam', $user['nama_lengkap'])->count();
// Cek Status
$user['jumlah_pinjam'] = $jumlahPinjam;
$user['kena_limit'] = $jumlahPinjam >= 2;
// Cek apakah user di-banned (Nonaktif Manual)
$user['is_banned'] = $user['is_banned'] ?? false;
$user['disabled'] = $user['kena_limit'] || $user['is_banned'];
if ($user['is_banned']) {
$user['status_text'] = "(Akun Dibekukan)";
} elseif ($user['kena_limit']) {
$user['status_text'] = "(Limit Penuh: 2/2)";
} else {
$user['status_text'] = "";
}
return $user;
})
->groupBy('role');
// Filter hanya buku offline
$daftarBuku = DummyDataService::getAllBooks()
->where('status', 'Tersedia')
->filter(function ($buku) {
@ -54,4 +96,96 @@ public function create()
'daftarBuku' => $daftarBuku,
]);
}
}
/**
* Menampilkan halaman KHUSUS Manajemen Denda (Siswa & Guru Telat).
*/
public function dendaIndex()
{
$allData = DummyDataService::getAdminPeminjamanAktif();
$allSiswaRaw = collect(DummyDataService::getAllSiswa());
$now = \Carbon\Carbon::now();
// LOGIC AUTO-BAN
$allSiswa = $allSiswaRaw->map(function ($siswa) use ($allData, $now) {
// Cek siswa punya pinjaman yang telat
$pinjamanUser = $allData->firstWhere('user_id', $siswa['id']);
$isTelat = false;
if ($pinjamanUser) {
$isTelat = $pinjamanUser['tenggat_kembali']->startOfDay()->lt($now->startOfDay());
}
// Jika Role SISWA dan TELAT -> Wajib AUTO BAN (Override true)
// ika Role GURU -> Ikut status asli database (Manual Ban)
if ($siswa['role'] === 'siswa' && $isTelat) {
$siswa['is_banned'] = true;
}
return $siswa;
});
$siswaTelat = $allData->filter(function ($item) use ($now, $allSiswa) {
$userData = $allSiswa->firstWhere('id', $item['user_id']);
if (!$userData)
return false;
$isTelat = $item['tenggat_kembali']->startOfDay()->lt($now->startOfDay());
$isBanned = $userData['is_banned'];
$isTargetRole = in_array($userData['role'], ['siswa', 'guru']);
return ($isTelat || $isBanned) && $isTargetRole;
})->map(function ($item) use ($now, $allSiswa) {
// Hitungan Denda
$tenggat = $item['tenggat_kembali']->startOfDay();
// Jika belum telat (tapi banned/manual), hari telat 0
$hariTelat = ($tenggat->lt($now->startOfDay()))
? $tenggat->diffInDays($now->startOfDay())
: 0;
$totalDenda = $hariTelat * $item['denda_per_hari'];
$item['hari_terlambat'] = $hariTelat;
$item['total_denda'] = $totalDenda;
// Ambil status is_banned TERBARU dari $allSiswa
$dataSiswa = $allSiswa->firstWhere('id', $item['user_id']);
$item['is_banned'] = $dataSiswa['is_banned'] ?? false;
$item['kelas'] = $dataSiswa['kelas'] ?? 'Guru';
// Link WA
$hp = $item['nomor_hp'];
if (substr($hp, 0, 1) == '0')
$hp = '62' . substr($hp, 1);
if ($hariTelat > 0) {
$pesan = "Halo {$item['peminjam']}, anda terlambat pengembalian buku. Total Denda: Rp " . number_format($totalDenda, 0, ',', '.');
} else {
$pesan = "Halo {$item['peminjam']}, akun anda sedang dinonaktifkan sementara. Mohon hubungi petugas.";
}
$item['wa_link'] = "https://wa.me/{$hp}?text=" . urlencode($pesan);
return $item;
});
$listKelas = $siswaTelat->pluck('kelas')->unique()->values();
return view('admin.denda.index', [
'pageTitle' => 'Manajemen Denda & Sanksi',
'siswaTelat' => $siswaTelat,
'listKelas' => $listKelas
]);
}
/**
* Dummy function untuk tombol Sanksi
*/
public function berikanSanksi(Request $request)
{
return response()->json(['status' => 'success']);
}
}

View File

@ -11,17 +11,31 @@ class DashboardController extends Controller
public function index()
{
$user = Auth::user();
$bukuPinjam = DummyDataService::getBukuPinjamOffline($user);
$isTelat = collect($bukuPinjam)->contains(function ($buku) {
return $buku['sisa_hari'] < 0;
});
if ($isTelat && $user->role === 'siswa') {
$user->is_banned = true;
}
$stats = DummyDataService::getDashboardStats();
$pengumuman = DummyDataService::getPengumuman();
$pemberitahuan = DummyDataService::getPemberitahuan();
$progressMembaca = DummyDataService::getProgressMembaca();
$statistikBulanan = DummyDataService::getStatistikBulanan();
$bukuPinjamOffline = DummyDataService::getBukuPinjamOffline($user);
$bukuPinjamOffline = $bukuPinjam;
$bacaBukuOnline = DummyDataService::getBacaBukuOnline($user);
$rekomendasiPembelajaran = DummyDataService::getRekomendasiPembelajaran();
$personalNotif = DummyDataService::getNotifikasiForUser($user);
// Cek apakah ada notifikasi denda aktif
$dendaAlert = collect($personalNotif)->where('type', 'denda_active');
// Menambahkan thumbnail YouTube ke setiap rekomendasi
$rekomendasiPembelajaran = DummyDataService::getRekomendasiPembelajaran()->map(function ($item) {
$rekomendasiPembelajaran = $rekomendasiPembelajaran->map(function ($item) {
$videoId = $this->extractYouTubeId($item['youtube_link']);
if ($videoId) {
$item['thumbnail'] = "https://img.youtube.com/vi/{$videoId}/hqdefault.jpg";
@ -31,30 +45,32 @@ public function index()
return $item;
});
$dendaAlert = collect($personalNotif)->where('type', 'denda_active');
$hour = date('H');
$greeting = "Selamat Pagi";
if ($hour >= 12 && $hour < 15) {
if ($hour >= 12 && $hour < 15)
$greeting = "Selamat Siang";
} elseif ($hour >= 15 && $hour < 18) {
elseif ($hour >= 15 && $hour < 18)
$greeting = "Selamat Sore";
} elseif ($hour >= 18) {
elseif ($hour >= 18)
$greeting = "Selamat Malam";
}
return view('dashboard', compact(
'user',
'stats',
'pengumuman',
'pemberitahuan',
'dendaAlert',
'progressMembaca',
'statistikBulanan',
'bukuPinjamOffline',
'bacaBukuOnline',
'greeting',
'rekomendasiPembelajaran'
));
))->with('notifikasi', $personalNotif);
}
/**
* Helper function untuk mengekstrak ID video dari URL YouTube.
*/
@ -63,4 +79,4 @@ private function extractYouTubeId(string $url): ?string
preg_match('/(v=|vi=|youtu.be\/|embed\/|\/v\/|\?v=|\&v=)(.+?)\b/i', $url, $matches);
return $matches[2] ?? null;
}
}
}

View File

@ -4,13 +4,23 @@
use App\Services\DummyDataService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class KatalogController extends Controller
{
public function index(Request $request)
{
$filters = $request->only(['search', 'kategori', 'tahun', 'penulis']);
$user = Auth::user();
$isBanned = false;
if ($user && $user->role === 'siswa') {
$bukuPinjam = DummyDataService::getBukuPinjamOffline($user);
$isTelat = collect($bukuPinjam)->contains(fn($b) => $b['sisa_hari'] < 0);
$isBannedManual = $user->is_banned ?? false;
$isBanned = $isTelat || $isBannedManual;
}
$filters = $request->only(['search', 'kategori', 'tahun', 'penulis']);
$semuaBuku = DummyDataService::getKatalogBuku($filters);
$filterOptions = DummyDataService::getFilterOptions();
@ -20,6 +30,7 @@ public function index(Request $request)
'input' => $filters,
'pageTitle' => 'Katalog Buku',
'mode' => 'umum',
'isBanned' => $isBanned,
]);
}
}
}

View File

@ -10,6 +10,18 @@ class PeminjamanController extends Controller
{
public function index(Request $request)
{
$user = \Illuminate\Support\Facades\Auth::user();
$bukuPinjam = \App\Services\DummyDataService::getBukuPinjamOffline($user);
$isTelat = collect($bukuPinjam)->contains(function ($buku) {
return $buku['sisa_hari'] < 0;
});
$isBannedManual = $user->is_banned ?? false;
if (($isTelat || $isBannedManual) && $user->role === 'siswa') {
return redirect()->route('dashboard')->with('error', 'AKSES DITOLAK: Akun Anda sedang dibekukan karena ada buku terlambat!');
}
$filters = $request->only(['search', 'kategori', 'tahun', 'penulis']);
$filters['tipe_akses'] = 'offline';
$semuaBuku = DummyDataService::getKatalogBuku($filters);

View File

@ -4,15 +4,16 @@
use App\Services\DummyDataService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; // Tambahkan ini
class RiwayatController extends Controller
{
/**
* Menampilkan halaman riwayat peminjaman offline.
*/
public function offlineIndex()
{
$riwayatOffline = DummyDataService::getRiwayatOffline();
$user = \Illuminate\Support\Facades\Auth::user();
if (!$user) $user = (object) ['id' => 1];
$riwayatOffline = DummyDataService::getRiwayatOffline($user);
return view('riwayat.offline', [
'pageTitle' => 'Riwayat Peminjaman Offline',
@ -20,9 +21,6 @@ public function offlineIndex()
]);
}
/**
* Menampilkan halaman riwayat baca online.
*/
public function onlineIndex()
{
$riwayatOnline = DummyDataService::getRiwayatOnline();

View File

@ -3,6 +3,7 @@
namespace App\Providers;
use App\Services\DummyDataService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\View;
@ -26,18 +27,18 @@ public function boot(): void
URL::forceScheme('https');
}
// View Composer untuk semua view (*)
View::composer('*', function ($view) {
$notifikasi = collect([]);
$unreadNotificationsCount = 0;
// Hanya ambil notifikasi jika ada pengguna yang login
if (auth()->check()) {
$user = auth()->user();
if (Auth::check()) {
$user = Auth::user();
$notifikasi = collect(DummyDataService::getNotifikasiForUser($user));
$unreadNotificationsCount = $notifikasi->where('read', false)->count();
$unreadCount = $notifikasi->where('read', false)->count();
$view->with('notifikasi', $notifikasi);
$view->with('unreadNotificationsCount', $unreadCount);
} else {
$view->with('notifikasi', collect([]));
$view->with('unreadNotificationsCount', 0);
}
$view->with(compact('notifikasi', 'unreadNotificationsCount'));
});
}
}

View File

@ -22,6 +22,7 @@ public static function getAllSiswa(): array
'role' => 'siswa',
'kelas' => 'XII RPL',
'golongan' => 'A',
'is_banned' => false,
],
[
'id' => 2,
@ -41,6 +42,7 @@ public static function getAllSiswa(): array
'role' => 'siswa',
'kelas' => 'XII RPL A',
'golongan' => 'A',
'is_banned' => true,
],
[
@ -53,6 +55,7 @@ public static function getAllSiswa(): array
'role' => 'siswa',
'kelas' => 'XII RPL A',
'golongan' => 'A',
'is_banned' => false,
],
[
'id' => 5,
@ -61,6 +64,7 @@ public static function getAllSiswa(): array
'email' => 'rina.marlina@smkn1perpus.sch.id',
'password' => 'password',
'role' => 'guru',
'is_banned' => false,
],
];
}
@ -81,157 +85,39 @@ public static function getAktivitasMingguan(): array
*/
public static function getAdminPeminjamanAktif(): \Illuminate\Support\Collection
{
return collect([
[
'id_peminjaman' => 'PIN-202510-001',
'peminjam' => 'Silvi Rahmawati',
'nomor_hp' => '08123456789',
'tanggal_pinjam' => Carbon::now()->subDays(12),
'tenggat_kembali' => Carbon::now()->subDays(5), // Terlambat 5 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 5, 'judul' => 'Si Anak Pintar'],
['id' => 8, 'judul' => 'Ayah'],
]
],
[
'id_peminjaman' => 'PIN-202510-002',
'peminjam' => 'Andi Pratama',
'nomor_hp' => '081556677889',
'tanggal_pinjam' => Carbon::now()->subDays(4),
'tenggat_kembali' => Carbon::now()->addDays(3), // Sisa 3 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 7, 'judul' => 'The Last Spell Breather'],
]
],
[
'id_peminjaman' => 'PIN-202510-003',
'peminjam' => 'Siti Nurhaliza',
'nomor_hp' => '081998877665',
$allBooks = self::getAllBooks();
$allSiswa = collect(self::getAllSiswa());
$bukuDipinjam = $allBooks->whereIn('status', ['Dipinjam', 'Terlambat']);
$grouped = $bukuDipinjam->groupBy(function ($item) {
return is_array($item['user_id']) ? $item['user_id'][0] : $item['user_id'];
});
return $grouped->map(function ($books, $userId) use ($allSiswa) {
$siswa = $allSiswa->firstWhere('id', $userId);
$firstBook = $books->first();
$tglKembali = Carbon::now()->addDays($firstBook['sisa_hari']);
return [
'id_peminjaman' => 'PIN-ADM-' . sprintf('%03d', $userId),
'user_id' => $userId,
'role' => $siswa ? $siswa['role'] : 'siswa',
'is_banned' => $siswa ? ($siswa['is_banned'] ?? false) : false,
'peminjam' => $siswa ? $siswa['nama_lengkap'] : 'User Tidak Dikenal',
'nomor_hp' => $siswa ? ($siswa['nomor_hp'] ?? '-') : '-',
'tanggal_pinjam' => Carbon::now()->subDays(7),
'tenggat_kembali' => Carbon::now(), // Hari ini
'tenggat_kembali' => $tglKembali,
'denda_per_hari' => 1000,
'books' => [
['id' => 1, 'judul' => 'Modul Ajar IPAS'],
]
],
[
'id_peminjaman' => 'PIN-202510-004',
'peminjam' => 'Dewi Lestari',
'nomor_hp' => '082134567891',
'tanggal_pinjam' => Carbon::now()->subDays(15),
'tenggat_kembali' => Carbon::now()->subDays(8), // Terlambat 8 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 2, 'judul' => 'Modul Ajar Pendidikan Pancasila'],
['id' => 6, 'judul' => 'Matematika Dasar'],
]
],
[
'id_peminjaman' => 'PIN-202510-005',
'peminjam' => 'Eko Prasetyo',
'nomor_hp' => '085612345678',
'tanggal_pinjam' => Carbon::now()->subDays(2),
'tenggat_kembali' => Carbon::now()->addDays(5), // Sisa 5 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 9, 'judul' => 'Senja, Hujan, & Cerita yang Telah Usai'],
]
],
[
'id_peminjaman' => 'PIN-202510-006',
'peminjam' => 'Rina Marlina',
'nomor_hp' => '081987654321',
'tanggal_pinjam' => Carbon::now()->subDays(10),
'tenggat_kembali' => Carbon::now()->subDays(3), // Terlambat 3 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 4, 'judul' => 'Modul Pembelajaran Seni Budaya'],
]
],
[
'id_peminjaman' => 'PIN-202510-007',
'peminjam' => 'Budi Santoso',
'nomor_hp' => '082298765432',
'tanggal_pinjam' => Carbon::now()->subDays(6),
'tenggat_kembali' => Carbon::now()->addDays(1), // Sisa 1 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 3, 'judul' => 'Modul Belajar Sosiologi'],
['id' => 10, 'judul' => 'Hijrah itu Cinta'],
]
],
[
'id_peminjaman' => 'PIN-202510-008',
'peminjam' => 'Putri Amelia',
'nomor_hp' => '085743219876',
'tanggal_pinjam' => Carbon::now()->subDays(18),
'tenggat_kembali' => Carbon::now()->subDays(11), // Terlambat 11 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 1, 'judul' => 'Modul Ajar IPAS'],
]
],
[
'id_peminjaman' => 'PIN-202510-009',
'peminjam' => 'Ahmad Haziq',
'nomor_hp' => '081345678902',
'tanggal_pinjam' => Carbon::now()->subDays(3),
'tenggat_kembali' => Carbon::now()->addDays(4), // Sisa 4 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 7, 'judul' => 'The Last Spell Breather'],
['id' => 8, 'judul' => 'Ayah'],
]
],
[
'id_peminjaman' => 'PIN-202510-010',
'peminjam' => 'John Wick',
'nomor_hp' => '082187654309',
'tanggal_pinjam' => Carbon::now()->subDays(8),
'tenggat_kembali' => Carbon::now()->subDays(1), // Terlambat 1 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 5, 'judul' => 'Si Anak Pintar'],
]
],
[
'id_peminjaman' => 'PIN-202510-011',
'peminjam' => 'Silvi Rahmawati',
'nomor_hp' => '08123456789',
'tanggal_pinjam' => Carbon::now()->subDays(5),
'tenggat_kembali' => Carbon::now()->addDays(2), // Sisa 2 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 2, 'judul' => 'Modul Ajar Pendidikan Pancasila'],
['id' => 4, 'judul' => 'Modul Pembelajaran Seni Budaya'],
]
],
[
'id_peminjaman' => 'PIN-202510-012',
'peminjam' => 'Andi Pratama',
'nomor_hp' => '081556677889',
'tanggal_pinjam' => Carbon::now()->subDays(20),
'tenggat_kembali' => Carbon::now()->subDays(13), // Terlambat 13 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 6, 'judul' => 'Matematika Dasar'],
]
],
[
'id_peminjaman' => 'PIN-202510-013',
'peminjam' => 'Siti Nurhaliza',
'nomor_hp' => '081998877665',
'tanggal_pinjam' => Carbon::now()->subDays(1),
'tenggat_kembali' => Carbon::now()->addDays(6), // Sisa 6 hari
'denda_per_hari' => 1000,
'books' => [
['id' => 9, 'judul' => 'Senja, Hujan, & Cerita yang Telah Usai'],
['id' => 10, 'judul' => 'Hijrah itu Cinta'],
]
],
]);
'books' => $books->map(function ($b) {
return [
'id' => $b['id'],
'judul' => $b['judul'],
'cover' => $b['cover']
];
})->toArray()
];
})->values();
}
/**
@ -328,7 +214,7 @@ public static function getSiswaTeraktif(): array
['nama' => 'Rina Marlina', 'total_buku' => 20, 'kelas' => 'XII TKJ A'],
['nama' => 'Budi Santoso', 'total_buku' => 18, 'kelas' => 'X OTKP'],
['nama' => 'Putri Amelia', 'total_buku' => 16, 'kelas' => 'XI RPL C'],
['nama' => 'Ahmad Haziq', 'total_buku' => 12, 'kelas' => 'X TKJ B'],
['nama' => 'Ahmad Jono', 'total_buku' => 12, 'kelas' => 'X TKJ B'],
];
}
@ -483,158 +369,157 @@ public static function getStatistikBulanan(): array
* @return \Illuminate\Support\Collection
*/
public static function getAllBooks()
{
return collect([
[
'id' => 1,
'judul' => 'Modul Ajar IPAS',
'penulis' => 'Tim Kemdikbud Ristek',
'cover' => 'images/covers/ipas.jpg',
'kode_buku' => '510', // 500 (Sains)
'kategori' => 'Sains',
'tahun' => 2022,
'status' => 'Tersedia',
'is_new' => true,
'tipe_akses' => ['online', 'offline'],
'file_pdf' => 'ipas.pdf',
'progress' => 75,
'sisa_hari' => 14,
'user_id' => 1,
],
[
'id' => 2,
'judul' => 'Modul Ajar Pendidikan Pancasila',
'penulis' => 'Tim Guru Pancasila',
'cover' => 'images/covers/pancasila.jpg',
'kode_buku'=> '370', // 370 (Pendidikan, bagian dari 300-Sosial)
'kategori' => 'Pendidikan',
'tahun' => 2023,
'status' => 'Tersedia',
'is_new' => false,
'tipe_akses' => 'offline',
'sisa_hari' => 3,
'progress' => 100,
'user_id' => [3, 1],
],
[
'id' => 3,
'judul' => 'Modul Belajar Sosiologi',
'penulis' => 'Tim Cendekia',
'cover' => 'images/covers/sosiologi.jpg',
'kode_buku' => '340', // 300 (Ilmu Sosial)
'kategori' => 'Sosial',
'tahun' => 2021,
'status' => 'Dipinjam',
'is_new' => false,
'tipe_akses' => 'offline',
'sisa_hari' => 8,
'user_id' => 3,
],
[
'id' => 4,
'judul' => 'Modul Pembelajaran Seni Budaya',
'penulis' => 'Cahya Wulan, S.Pd.',
'cover' => 'images/covers/senbud.jpg',
'kode_buku' => '752', // 700 (Seni)
'kategori' => 'Seni',
'tahun' => 2022,
'status' => 'Dipinjam',
'is_new' => false,
'tipe_akses' => 'offline',
'sisa_hari' => 14,
'user_id' => [1, 3],
],
[
'id' => 5,
'judul' => 'Si Anak Pintar',
'penulis' => 'Tere Liye',
'cover' => 'images/covers/sianakpintar.jpg',
'kode_buku' => '843', // 800 (Fiksi/Sastra)
'kategori' => 'Fiksi',
'tahun' => 2018,
'status' => 'Dipinjam',
'is_new' => true,
'tipe_akses' => 'offline',
'sisa_hari' => 5,
'user_id' => 1,
],
[
'id' => 6,
'judul' => 'Matematika Dasar',
'penulis' => 'Prof. Dr. Matematikus',
'cover' => 'images/covers/mtk.jpg',
'kode_buku' => '374', // 370 (Pendidikan)
'kategori' => 'Pendidikan',
'tahun' => 2023,
'status' => 'Tersedia',
'is_new' => true,
'tipe_akses' => ['online', 'offline'],
'file_pdf' => 'mtk.pdf',
'sisa_hari' => 7,
'progress' => 40,
'user_id' => [1, 4, 5],
],
[
'id' => 7,
'judul' => 'The Last Spell Breather',
'penulis' => 'Julie Pike',
'cover' => 'images/covers/thelastspellbreather.jpg',
'kode_buku' => '834', // 800 (Fantasi/Sastra)
'kategori' => 'Fantasi',
'tahun' => 2024,
'status' => 'Tersedia',
'is_new' => true,
'tipe_akses' => 'offline',
'sisa_hari' => 4,
'progress' => 0,
'user_id' => [3, 1]
],
[
'id' => 8,
'judul' => 'Ayah',
'penulis' => 'Andrea Hirata',
'cover' => 'images/covers/ayah.png',
'kategori' => 'Novel',
'tahun' => 2015,
'status' => 'Tersedia',
'is_new' => true,
'tipe_akses' => 'online',
'file_pdf' => 'ayah.pdf',
'progress' => 0,
'user_id' => [1, 2, 3],
],
[
'id' => 9,
'judul' => 'Senja, Hujan, & Cerita yang Telah Usai',
'penulis' => 'Boy Candra',
'cover' => 'images/covers/senja.png',
'kode_buku' => '845', // 800 (Novel/Sastra)
'kategori' => 'Novel',
'tahun' => 2015,
'status' => 'Tersedia',
'is_new' => true,
'tipe_akses' => ['online', 'offline'],
'file_pdf' => 'senja.pdf',
'progress' => 0,
'sisa_hari' => 14,
'user_id' => [1, 3],
],
[
'id' => 10,
'judul' => 'Hijrah itu Cinta',
'penulis' => 'Abay Adhitya',
'cover' => 'images/covers/hijrah.png',
'kategori' => 'Religi',
'tahun' => 2018,
'status' => 'Tersedia',
'is_new' => true,
'tipe_akses' => 'online',
'file_pdf' => 'hijrah.pdf',
'progress' => 0,
'user_id' => [2, 3],
]
]);
}
{
return collect([
// --- BUKU MILIK USER 1 (SILVI) ---
[
'id' => 1,
'judul' => 'Modul Ajar IPAS',
'penulis' => 'Tim Kemdikbud Ristek',
'cover' => 'images/covers/ipas.jpg',
'kode_buku' => '510',
'kategori' => 'Sains',
'tahun' => 2022,
'status' => 'Dipinjam',
'is_new' => true,
'tipe_akses' => ['online', 'offline'],
'sisa_hari' => -5,
'user_id' => 1,
],
[
'id' => 2,
'judul' => 'Perahu Kertas',
'penulis' => 'Dewi Lestari',
'cover' => 'images/covers/ayah.png',
'kode_buku' => '844',
'kategori' => 'Fiksi',
'tahun' => 2012,
'status' => 'Dipinjam',
'is_new' => false,
'tipe_akses' => 'offline',
'sisa_hari' => 3,
'user_id' => 1,
],
// --- BUKU TERSEDIA (UNTUK CEK PEMINJAMAN BARU) ---
[
'id' => 3,
'judul' => 'Si Anak Pintar',
'penulis' => 'Tere Liye',
'cover' => 'images/covers/sianakpintar.jpg',
'kode_buku' => '843',
'kategori' => 'Fiksi',
'tahun' => 2018,
'status' => 'Tersedia',
'is_new' => true,
'tipe_akses' => 'offline',
'sisa_hari' => 0,
'user_id' => null,
],
[
'id' => 4,
'judul' => 'Laskar Pelangi',
'penulis' => 'Andrea Hirata',
'cover' => 'https://upload.wikimedia.org/wikipedia/id/8/8e/Laskar_pelangi_sampul.jpg',
'kode_buku' => 'NOV-001',
'kategori' => 'Novel',
'tahun' => 2005,
'status' => 'Tersedia',
'is_new' => false,
'tipe_akses' => 'offline',
'sisa_hari' => 0,
'user_id' => null,
],
[
'id' => 5,
'judul' => 'Filosofi Teras',
'penulis' => 'Henry Manampiring',
'cover' => 'https://upload.wikimedia.org/wikipedia/id/3/36/Filosofi_Teras.jpg',
'kode_buku' => 'PSI-002',
'kategori' => 'Psikologi',
'tahun' => 2018,
'status' => 'Tersedia',
'is_new' => true,
'tipe_akses' => 'offline',
'sisa_hari' => 0,
'user_id' => null,
],
[
'id' => 6,
'judul' => 'Atomic Habits',
'penulis' => 'James Clear',
'cover' => 'https://images-na.ssl-images-amazon.com/images/I/91bYsX41DVL.jpg',
'kode_buku' => 'SELF-003',
'kategori' => 'Refleksi Diri',
'tahun' => 2018,
'status' => 'Tersedia',
'is_new' => true,
'tipe_akses' => ['offline', 'online'],
'sisa_hari' => 0,
'user_id' => null,
],
[
'id' => 7,
'judul' => 'Bumi Manusia',
'penulis' => 'Pramoedya Ananta Toer',
'cover' => 'https://upload.wikimedia.org/wikipedia/id/4/44/Bumi_Manusia.jpg',
'kode_buku' => 'SAS-004',
'kategori' => 'Sastra',
'tahun' => 1980,
'status' => 'Tersedia',
'is_new' => false,
'tipe_akses' => 'offline',
'sisa_hari' => 0,
'user_id' => null,
],
[
'id' => 8,
'judul' => 'Laut Bercerita',
'penulis' => 'Leila S. Chudori',
'cover' => 'https://upload.wikimedia.org/wikipedia/id/6/6d/Laut_Bercerita.jpeg',
'kode_buku' => 'NOV-005',
'kategori' => 'Novel',
'tahun' => 2017,
'status' => 'Tersedia',
'is_new' => true,
'tipe_akses' => 'offline',
'sisa_hari' => 0,
'user_id' => null,
],
// --- BUKU MILIK USER 3 (SITI) - TELAT ---
[
'id' => 99,
'judul' => 'Seni Berbicara',
'penulis' => 'Larry King',
'cover' => 'https://via.placeholder.com/150',
'kode_buku' => 'COM-001',
'kategori' => 'Refleksi Diri',
'tahun' => 2019,
'status' => 'Dipinjam',
'is_new' => false,
'tipe_akses' => 'offline',
'sisa_hari' => -2,
'user_id' => 3,
],
// --- BUKU MILIK GURU (USER 5) - TESTING MANUAL BAN ---
[
'id' => 88,
'judul' => 'Strategi Pembelajaran Abad 21',
'penulis' => 'Prof. Dr. Pendidikan',
'cover' => 'https://via.placeholder.com/150',
'kode_buku' => 'GURU-001',
'kategori' => 'Pendidikan',
'tahun' => 2020,
'status' => 'Dipinjam',
'is_new' => false,
'tipe_akses' => 'offline',
'sisa_hari' => -3,
'user_id' => 5,
],
]);
}
/**
* Data untuk buku pinjam offline
@ -642,9 +527,16 @@ public static function getAllBooks()
public static function getBukuPinjamOffline($user): array
{
return self::getAllBooks()
->where('tipe_akses', 'offline')
->filter(function ($buku) {
// Handle tipe akses string atau array
if (is_array($buku['tipe_akses'])) {
return in_array('offline', $buku['tipe_akses']);
}
return $buku['tipe_akses'] === 'offline';
})
->filter(function ($buku) use ($user) {
if (!isset($buku['user_id'])) return false;
if (!isset($buku['user_id']) || $buku['user_id'] === null)
return false;
if (is_array($buku['user_id'])) {
return in_array($user->id, $buku['user_id']);
@ -669,7 +561,8 @@ public static function getBacaBukuOnline($user): array
return self::getAllBooks()
->where('tipe_akses', 'online')
->filter(function ($buku) use ($user) {
if (!isset($buku['user_id'])) return false;
if (!isset($buku['user_id']))
return false;
if (is_array($buku['user_id'])) {
return in_array($user->id, $buku['user_id']);
@ -738,52 +631,47 @@ public static function getFilterOptions(): array
* Data untuk riwayat peminjaman offline.
* Setiap item mewakili satu transaksi peminjaman.
*/
public static function getRiwayatOffline(): array
public static function getRiwayatOffline($user): array
{
return [
[
'id' => 1,
'id_peminjaman' => 'PIN-20240520-001',
'kode_buku' => '510',
'judul_utama' => 'Modul Ajar IPAS',
'tanggal_pinjam' => '20/05/2024',
'tanggal_kembali' => '27/05/2024',
'status' => 'Dikembalikan',
$allBooks = self::getAllBooks();
$myBooks = $allBooks->filter(function ($buku) use ($user) {
if (is_array($buku['user_id'])) {
return in_array($user->id, $buku['user_id']);
}
return $buku['user_id'] == $user->id;
})->whereIn('status', ['Dipinjam', 'Dikembalikan', 'Terlambat']);
// Mapping ke format tampilan View
return $myBooks->map(function ($buku, $index) {
// Logika Tanggal Dummy
$tglPinjam = Carbon::now()->subDays(7);
// Hitung tanggal kembali berdasarkan sisa hari di Master Data
$tglKembali = Carbon::now()->addDays($buku['sisa_hari']);
return [
'id' => $index + 1,
'id_peminjaman' => 'PIN-' . date('Ym') . '-' . sprintf('%03d', $buku['id']),
'kode_buku' => $buku['kode_buku'],
'judul_utama' => $buku['judul'],
'tanggal_pinjam' => $tglPinjam->format('d/m/Y'),
'tanggal_kembali' => $tglKembali->format('d/m/Y'),
'status' => $buku['status'],
'books' => [
[
'id' => 1,
'judul' => 'Modul Ajar IPAS',
'kode_buku' => '510',
'cover' => 'images/covers/ipas.jpg',
'deskripsi' => 'Buku ini berisi ajakan kepada anak-anak untuk semangat pergi ke sekolah dan menuntut ilmu.',
'kategori' => 'Pendidikan',
'tahun' => 2022,
'keterangan' => 'Buku dikembalikan dalam kondisi baik. Terdapat denda keterlambatan 2 hari: Rp 2.000,-'
'id' => $buku['id'],
'judul' => $buku['judul'],
'kode_buku' => $buku['kode_buku'],
'cover' => $buku['cover'],
'deskripsi' => 'Deskripsi buku ' . $buku['judul'],
'kategori' => $buku['kategori'],
'tahun' => $buku['tahun'],
'keterangan' => ($buku['sisa_hari'] < 0) ? 'Buku Terlambat' : null,
]
]
],
[
'id' => 2,
'id_peminjaman' => 'PIN-20240527-002',
'kode_buku' => '844',
'judul_utama' => 'Perahu Kertas',
'tanggal_pinjam' => '27/05/2024',
'tanggal_kembali' => '04/06/2024',
'status' => 'Dipinjam',
'books' => [
[
'id' => 8,
'judul' => 'Perahu Kertas',
'kode_buku' => '844',
'cover' => 'images/covers/ayah.png',
'deskripsi' => 'Cerita penggambaran pasang surut hubungan dua anak manusia, yaitu Kugy dan Keenan.',
'kategori' => 'Fiksi',
'tahun' => 2022,
'keterangan' => null,
],
]
],
];
];
})->values()->toArray();
}
/**
@ -821,79 +709,62 @@ public static function getRiwayatOnline(): array
public static function getNotifikasiForUser($user): array
{
$notifikasi = [];
$allBooks = self::getAllBooks();
$myBooks = $allBooks->filter(function ($buku) use ($user) {
if (!isset($buku['user_id']) || $buku['user_id'] === null) {
return false;
}
$bukuPinjaman = self::getBukuPinjamOffline($user);
if (is_array($buku['user_id'])) {
return in_array($user->id, $buku['user_id']);
}
return $buku['user_id'] == $user->id;
});
if (!empty($bukuPinjaman)) {
$bukuTerbaru = $bukuPinjaman[0];
$notifikasi[] = [
'icon' => 'bi-check2-circle',
'color' => 'success',
'title' => 'Buku "' . $bukuTerbaru['judul'] . '" berhasil dipinjam.',
'time' => '5 menit yang lalu',
'read' => false,
'type' => 'riwayat_peminjaman',
'link_id' => null,
];
}
foreach ($myBooks as $buku) {
// LOGIC TELAT
if ($buku['sisa_hari'] < 0 && $buku['status'] == 'Dipinjam') {
$hariTelat = abs($buku['sisa_hari']);
$denda = $hariTelat * 1000;
foreach ($bukuPinjaman as $buku) {
if ($buku['sisa_hari'] <= 3) {
$notifikasi[] = [
'icon' => 'bi-exclamation-triangle',
'icon' => 'bi-exclamation-octagon-fill',
'color' => 'danger',
'title' => 'Buku "' . $buku['judul'] . '" akan jatuh tempo!',
'time' => '1 jam yang lalu',
'title' => 'TERLAMBAT: ' . $buku['judul'],
'content' => "Telat {$hariTelat} hari. Denda: Rp " . number_format($denda, 0, ',', '.'),
'time' => 'Sekarang',
'read' => false,
'type' => 'riwayat_peminjaman',
'type' => 'denda_active',
'link_id' => null,
];
}
// LOGIC JATUH TEMPO
elseif ($buku['sisa_hari'] >= 0 && $buku['sisa_hari'] <= 3 && $buku['status'] == 'Dipinjam') {
$notifikasi[] = [
'icon' => 'bi-exclamation-triangle-fill',
'color' => 'warning',
'title' => 'Jatuh Tempo: ' . $buku['judul'],
'content' => "Sisa waktu tinggal " . $buku['sisa_hari'] . " hari lagi.",
'time' => 'Segera',
'read' => false,
'type' => 'warning_jatuh_tempo',
'link_id' => null,
];
}
}
if ($user->role === 'guru') {
$notifikasi[] = [
'icon' => 'bi-lightbulb-fill',
'color' => 'success',
'title' => 'Rekomendasi pembelajaran baru telah ditambahkan.',
'time' => 'Kemarin',
'read' => true,
'type' => 'rekomendasi',
'link_id' => 1,
];
}
$notifikasiUmum = [
[
'icon' => 'bi-book-half',
'color' => 'info',
'title' => 'Buku baru ditambahkan ke kategori Fiksi.',
'time' => '3 jam yang lalu',
'read' => false,
'type' => 'katalog_kategori',
'link_id' => 'Fiksi',
],
[
'icon' => 'bi-megaphone-fill',
'color' => 'warning',
'title' => 'Perpustakaan akan tutup lebih awal pada hari Jumat.',
'time' => '1 hari yang lalu',
'read' => true,
'type' => 'halaman_profil',
'link_id' => null,
],
[
'icon' => 'bi-calendar-event',
'color' => 'primary',
'title' => 'Acara "Bedah Buku" akan diadakan minggu depan.',
'time' => '2 hari yang lalu',
'read' => true,
'type' => 'halaman_profil',
'link_id' => null,
],
// Notif Default
$notifikasi[] = [
'icon' => 'bi-info-circle-fill',
'color' => 'primary',
'title' => 'Selamat Datang di DigiPus!',
'content' => 'Silakan jelajahi koleksi buku terbaru kami.',
'time' => 'Baru saja',
'read' => true,
'type' => 'info',
'link_id' => null,
];
return array_merge($notifikasi, $notifikasiUmum);
return $notifikasi;
}
}

8
package-lock.json generated
View File

@ -825,7 +825,6 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@ -2590,7 +2589,6 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@ -2677,8 +2675,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
"integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
@ -2735,7 +2732,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -2779,7 +2775,6 @@
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@ -2884,7 +2879,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},

View File

@ -6,7 +6,7 @@ if (formPeminjamanElement) {
const bukuAwal = JSON.parse(formPeminjamanElement.dataset.bukuAwal);
let bukuTerpilih = [bukuAwal.id];
const maxBuku = 3;
const maxBuku = 2;
document.addEventListener('DOMContentLoaded', function () {
updateCounter();

View File

@ -0,0 +1,248 @@
<x-app-layout>
@section('page-title', 'Manajemen Denda')
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h3 class="my-0 fw-bold">Manajemen Denda & Sanksi</h3>
<p class="text-muted mb-0">Pantau siswa terlambat dan berikan sanksi jika diperlukan.</p>
</div>
</div>
{{-- FILTER SECTION --}}
<div class="card border-0 shadow-sm mb-4">
<div class="card-body py-3">
<div class="row g-3 align-items-center">
<div class="col-md-auto">
<span class="fw-bold text-muted"><i class="bi bi-funnel-fill me-1"></i> Filter Data:</span>
</div>
<div class="col-md-3">
<select id="filterKelas" class="form-select form-select-sm">
<option value="">Semua Kelas</option>
@foreach ($listKelas as $kelas)
<option value="{{ $kelas }}">{{ $kelas }}</option>
@endforeach
</select>
</div>
<div class="col-md-3">
<select id="filterBesaran" class="form-select form-select-sm">
<option value="">Semua Nominal</option>
<option value="ringan">Denda Ringan (< Rp 10.000)</option>
<option value="berat">Denda Berat (> Rp 10.000)</option>
</select>
</div>
<div class="col-md-auto ms-auto">
<button id="resetFilter" class="btn btn-sm btn-light border text-muted">
<i class="bi bi-arrow-counterclockwise"></i> Reset
</button>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table id="dendaTable" class="table table-striped table-hover align-middle" style="width:100%">
<thead class="table-light">
<tr>
<th class="text-center" width="5%">NO</th>
<th width="25%">SISWA & KELAS</th>
<th width="25%">BUKU TERLAMBAT</th>
<th width="15%">STATUS</th>
<th width="15%">DENDA</th>
<th width="15%">AKSI</th>
</tr>
</thead>
<tbody>
@forelse ($siswaTelat as $item)
<tr>
<td class="text-center">{{ $loop->iteration }}</td>
<td>
<div class="fw-bold text-dark">{{ $item['peminjam'] }}</div>
<span
class="badge bg-light text-secondary border mt-1 mb-1">{{ $item['kelas'] }}</span>
<div class="small text-muted"><i
class="bi bi-telephone me-1"></i>{{ $item['nomor_hp'] }}</div>
</td>
<td>
<ul class="mb-0 ps-3 small text-muted">
@foreach ($item['books'] as $buku)
<li>{{ $buku['judul'] }}</li>
@endforeach
</ul>
</td>
<td data-order="{{ $item['tenggat_kembali']->timestamp }}">
<span
class="badge bg-danger-subtle text-danger border border-danger-subtle rounded-pill px-3">
Telat {{ $item['hari_terlambat'] }} Hari
</span>
<div class="small text-muted mt-1">
Tenggat: {{ $item['tenggat_kembali']->format('d/m/Y') }}
</div>
</td>
<td data-order="{{ $item['total_denda'] }}">
<div class="fw-bold text-danger">Rp
{{ number_format($item['total_denda'], 0, ',', '.') }}</div>
<small class="text-muted" style="font-size: 0.75rem;">Rp 1.000/hari</small>
</td>
<td>
<div class="d-flex gap-2">
{{-- Tombol WA --}}
<a href="{{ $item['wa_link'] }}" target="_blank"
class="btn btn-sm btn-success text-white" title="Tagih via WhatsApp">
<i class="bi bi-whatsapp"></i>
</a>
{{-- LOGIC TOMBOL AKTIFKAN / SANKSI --}}
@if ($item['is_banned'])
{{-- Jika sudah dibekukan (Otomatis/Manual), muncul tombol AKTIFKAN --}}
<button class="btn btn-sm btn-outline-success btn-aktifkan"
data-nama="{{ $item['peminjam'] }}" title="Aktifkan Kembali Akun">
<i class="bi bi-shield-check"></i> Aktifkan
</button>
@else
{{-- Jika belum dibekukan, muncul tombol SANKSI (Manual) --}}
<button class="btn btn-sm btn-outline-danger btn-sanksi"
data-nama="{{ $item['peminjam'] }}" title="Berikan Sanksi">
<i class="bi bi-slash-circle"></i> Sanksi
</button>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-5">
<div class="text-muted opacity-50">
<i class="bi bi-emoji-smile fs-1 mb-2 d-block"></i>
<p class="mb-0">Tidak ada siswa yang terlambat hari ini.</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
@push('scripts')
<script>
$(document).ready(function() {
var table = $('#dendaTable').DataTable({
order: [
[3, 'desc']
],
columnDefs: [{
"searchable": false,
"orderable": false,
"targets": 0
}],
language: {
search: "_INPUT_",
searchPlaceholder: "Cari nama siswa..."
},
dom: 'rtip'
});
table.on('order.dt search.dt', function() {
table.column(0, {
search: 'applied',
order: 'applied'
}).nodes().each(function(cell, i) {
cell.innerHTML = i + 1;
});
}).draw();
$.fn.dataTable.ext.search.push(
function(settings, data, dataIndex) {
var filterKelas = $('#filterKelas').val();
var filterBesaran = $('#filterBesaran').val();
var dataSiswa = data[1] || "";
var dataDendaRaw = data[4] || "0";
var dataDenda = parseInt(dataDendaRaw.replace(/[^0-9]/g, ''), 10);
if (filterKelas !== "" && !dataSiswa.includes(filterKelas)) return false;
if (filterBesaran === "ringan" && dataDenda >= 10000) return false;
if (filterBesaran === "berat" && dataDenda < 10000) return false;
return true;
}
);
$('#filterKelas, #filterBesaran').change(function() {
table.draw();
});
$('#resetFilter').click(function() {
$('#filterKelas').val('');
$('#filterBesaran').val('');
table.draw();
});
});
// --- LOGIC TOMBOL SANKSI ---
$(document).on('click', '.btn-sanksi', function() {
const nama = $(this).data('nama');
modernSwal.fire({
title: 'Nonaktifkan Guru?',
text: `Apakah Anda yakin ingin memberikan sanksi pembekuan akun kepada ${nama}?`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya, Bekukan',
confirmButtonColor: '#dc3545',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
modernSwal.fire({
title: 'Memproses...',
timer: 1000,
didOpen: () => Swal.showLoading()
})
.then(() => {
Toast.fire({
icon: 'success',
title: 'Sanksi Diterapkan',
text: `Akun ${nama} berhasil dibekukan.`
});
});
}
});
});
// Logic Tombol Aktifkan (Unban)
$(document).on('click', '.btn-aktifkan', function() {
const nama = $(this).data('nama');
modernSwal.fire({
title: 'Aktifkan Akun?',
text: `Pastikan ${nama} sudah melunasi denda. Akun akan diaktifkan kembali.`,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Aktifkan',
confirmButtonColor: '#198754',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
// Simulasi Loading & Sukses
modernSwal.fire({
title: 'Memproses...',
timer: 1000,
didOpen: () => Swal.showLoading()
})
.then(() => {
Toast.fire({
icon: 'success',
title: 'Akun Diaktifkan',
text: `Status sanksi pada ${nama} telah dicabut.`
});
$(this).closest('td').html('<span class="badge bg-success">Aktif</span>');
});
}
});
});
</script>
@endpush
</x-app-layout>

View File

@ -10,7 +10,7 @@
<form action="#" method="POST" id="formPeminjaman" autocomplete="off"> @csrf
<div class="row g-4">
<div class="col-lg-7">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
@ -18,20 +18,24 @@
<h5 class="fw-bold mb-3">Data Peminjaman</h5>
<div class="mb-3">
<label for="peminjam_id" class="form-label">Pilih Peminjam (Siswa/Guru)</label>
<select class="form-control" id="peminjam_id" name="peminjam_id"
placeholder="Cari nama atau email...">
<option value="">Pilih...</option>
@foreach ($groupedUsers as $role => $users)
<optgroup label="{{ ucfirst($role) }}">
@foreach ($users as $user)
<option value="{{ $user['id'] }}">{{ $user['nama_lengkap'] }}</option>
{{-- Logic Disable User --}}
<option value="{{ $user['id'] }}"
{{ $user['disabled'] ? 'disabled' : '' }}>
{{ $user['nama_lengkap'] }} {{ $user['status_text'] }}
</option>
@endforeach
</optgroup>
@endforeach
</select>
</div>
<div class="row g-3">
<div class="col-md-6">
<label for="tanggal_pinjam" class="form-label">Tanggal Pinjam</label>
@ -40,8 +44,8 @@
</div>
<div class="col-md-6">
<label for="tanggal_kembali" class="form-label">Tenggat Kembali</label>
<input type="text" class="form-control" id="tanggal_kembali"
name="tanggal_kembali" placeholder="Pilih tenggat kembali">
<input type="text" class="form-control" id="tanggal_kembali" name="tanggal_kembali"
placeholder="Pilih tenggat kembali">
</div>
</div>
@ -51,13 +55,13 @@
<h5 class="fw-bold m-0">Buku Terpilih</h5>
<span class="badge bg-primary-soft" id="counterBuku">0 Buku</span>
</div>
<div id="daftarBukuPinjam" class="mb-3">
<div id="emptyStateBuku" class="text-center text-muted py-4">
<i class="bi bi-collection fs-1" style="opacity: 0.25;"></i>
<p class="mb-0 mt-2">Belum ada buku yang dipilih.</p>
</div>
</div>
</div>
<div id="hiddenInputs"></div>
@ -66,7 +70,7 @@
<i class="bi bi-save me-1"></i> Simpan Peminjaman
</button>
</div>
</div>
</div>
</div>
@ -96,19 +100,16 @@
<div class="d-flex align-items-center book-item-list"
onclick="toggleBookSelection(this, {{ $buku['id'] }})"
style="cursor: pointer;">
<img src="{{ asset($buku['cover']) }}" alt="Cover"
class="rounded me-3"
<img src="{{ asset($buku['cover']) }}" alt="Cover" class="rounded me-3"
style="width: 50px; height: 70px; object-fit: cover;">
<div class="flex-grow-1">
<h6 class="fw-bold mb-1 line-clamp-2">{{ $buku['judul'] }}</h6>
<p class="text-muted small mb-1">{{ $buku['penulis'] }}</p>
<span
class="badge bg-info-soft">{{ $buku['kategori'] }}</span>
<span class="badge bg-info-soft">{{ $buku['kategori'] }}</span>
</div>
<div class="form-check ms-3">
<input class="form-check-input book-checkbox" type="checkbox"
value="{{ $buku['id'] }}"
id="book-check-{{ $buku['id'] }}"
value="{{ $buku['id'] }}" id="book-check-{{ $buku['id'] }}"
style="pointer-events: none;">
</div>
</div>
@ -125,10 +126,7 @@ class="badge bg-info-soft">{{ $buku['kategori'] }}</span>
@push('scripts')
<script>
document.addEventListener("DOMContentLoaded", function() {
// Inisialisasi Library & Simpan Instansinya
const tomSelect = new TomSelect("#peminjam_id", {
const tomSelect = new TomSelect("#peminjam_id", {
create: false,
sortField: {
field: "text",
@ -136,12 +134,12 @@ class="badge bg-info-soft">{{ $buku['kategori'] }}</span>
}
});
const tglPinjam = flatpickr("#tanggal_pinjam", {
dateFormat: "Y-m-d",
const tglPinjam = flatpickr("#tanggal_pinjam", {
dateFormat: "Y-m-d",
altInput: true,
altFormat: "d/m/Y",
altFormat: "d/m/Y",
defaultDate: "today",
locale: "id",
locale: "id",
onChange: function(selectedDates, dateStr) {
if (selectedDates.length > 0) {
tglKembali.set("minDate", new Date(selectedDates[0]).fp_incr(1));
@ -149,19 +147,18 @@ class="badge bg-info-soft">{{ $buku['kategori'] }}</span>
}
});
const tglKembali = flatpickr("#tanggal_kembali", {
const tglKembali = flatpickr("#tanggal_kembali", {
dateFormat: "Y-m-d",
altInput: true,
altFormat: "d/m/Y",
defaultDate: new Date().fp_incr(7),
altFormat: "d/m/Y",
defaultDate: new Date().fp_incr(7),
locale: "id",
minDate: new Date().fp_incr(1)
});
// Logika Pemilihan Buku
const MAX_BOOKS = 3;
const MAX_BOOKS = 2;
let selectedBookIds = new Set();
const allBooks = new Map();
document.querySelectorAll('.book-option').forEach(el => {
@ -238,26 +235,27 @@ function renderSelectedBooks() {
if (selectedBookIds.has(stringId)) {
selectedBookIds.delete(stringId);
checkbox.checked = false;
itemElement.style.background = 'transparent';
itemElement.style.background = 'transparent';
} else {
if (selectedBookIds.size >= MAX_BOOKS) {
alert(`Maaf, Anda hanya dapat memilih maksimal ${MAX_BOOKS} buku.`);
return;
return;
}
selectedBookIds.add(stringId);
checkbox.checked = true;
itemElement.style.background = 'rgba(var(--bs-primary-rgb), 0.05)';
itemElement.style.background = 'rgba(var(--bs-primary-rgb), 0.05)';
}
renderSelectedBooks();
}
// Global function untuk remove
// Global function untuk remove
window.removeBook = function(id) {
const stringId = String(id);
selectedBookIds.delete(stringId);
const itemElement = document.querySelector(`.book-option[data-book-id="${id}"] .book-item-list`);
const itemElement = document.querySelector(
`.book-option[data-book-id="${id}"] .book-item-list`);
if (itemElement) {
itemElement.querySelector('.book-checkbox').checked = false;
itemElement.style.background = 'transparent';
@ -265,27 +263,25 @@ function renderSelectedBooks() {
renderSelectedBooks();
}
const form = document.getElementById('formPeminjaman');
form.reset();
// Logika Reset Saat Reload Halaman
const form = document.getElementById('formPeminjaman');
form.reset();
tomSelect.clear();
tglPinjam.setDate("today", false);
tglKembali.setDate(new Date().fp_incr(7), false);
selectedBookIds.clear();
document.querySelectorAll('.book-option').forEach(el => {
tomSelect.clear();
tglPinjam.setDate("today", false);
tglKembali.setDate(new Date().fp_incr(7), false);
selectedBookIds.clear();
document.querySelectorAll('.book-option').forEach(el => {
el.querySelector('.book-checkbox').checked = false;
el.querySelector('.book-item-list').style.background = 'transparent';
el.querySelector('.book-item-list').style.background = 'transparent';
});
renderSelectedBooks();
renderSelectedBooks();
});
</script>
@endpush
</x-app-layout>
</x-app-layout>

View File

@ -14,7 +14,8 @@
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table id="peminjamanTable" class="table table-striped table-hover nowrap dt-responsive" style="width:100%">
<table id="peminjamanTable" class="table table-striped table-hover nowrap dt-responsive"
style="width:100%">
<thead class="table-light">
<tr>
<th>NO</th>
@ -32,6 +33,7 @@
$now = \Carbon\Carbon::now();
$counter = 1;
@endphp
@forelse ($peminjamanAktif as $transaksi)
@php
$tenggat = $transaksi['tenggat_kembali'];
@ -39,177 +41,178 @@
$isHariIni = $now->startOfDay()->isSameDay($tenggat->startOfDay());
$selisihHari = $now->startOfDay()->diffInDays($tenggat->startOfDay(), false);
$statusKeterlambatan = '';
$dendaKeterlambatan = 0;
$statusClass = '';
if ($isTerlambat) {
$hariTerlambat = $tenggat->startOfDay()->diffInDays($now->startOfDay());
$statusKeterlambatan = "Terlambat $hariTerlambat hari";
$dendaKeterlambatan = $hariTerlambat * $transaksi['denda_per_hari'];
$statusClass = 'badge rounded-pill bg-danger-subtle text-danger-emphasis';
$statusText =
'Terlambat ' . $tenggat->startOfDay()->diffInDays($now->startOfDay()) . ' hari';
} elseif ($isHariIni) {
$statusKeterlambatan = 'Hari ini';
$statusClass = 'badge rounded-pill bg-warning-subtle text-warning-emphasis';
$statusText = 'Jatuh Tempo Hari Ini';
} else {
$sisaHari = abs($selisihHari);
$statusKeterlambatan = "Sisa $sisaHari hari";
$statusClass = 'badge rounded-pill bg-success-subtle text-success-emphasis';
$statusText = 'Sisa ' . abs($selisihHari) . ' hari';
}
@endphp
@foreach ($transaksi['books'] as $index => $buku)
<tr>
<td>{{ $counter++ }}</td>
<td>
<span
class="badge bg-primary-light text-primary fw-semibold">{{ $transaksi['id_peminjaman'] }}</span>
</td>
<td>{{ $transaksi['peminjam'] }}</td>
<td>{{ $buku['judul'] }}</td>
<td>{{ $transaksi['tanggal_pinjam']->format('d/m/Y') }}</td>
<td>{{ $transaksi['tenggat_kembali']->format('d/m/Y') }}</td>
<td>
<span class="badge {{ $statusClass }}">{{ $statusKeterlambatan }}</span>
</td>
<td>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal"
data-bs-target="#modalPengembalian-{{ $transaksi['id_peminjaman'] }}-{{ $buku['id'] }}">
Proses
</button>
</td>
</tr>
<tr>
<td>{{ $counter++ }}</td>
<td>
<span
class="badge bg-primary-subtle text-primary fw-bold">{{ $transaksi['id_peminjaman'] }}</span>
</td>
<td>
<div class="fw-bold">{{ $transaksi['peminjam'] }}</div>
<div class="small text-muted">{{ $transaksi['nomor_hp'] }}</div>
</td>
<td>
<ul class="mb-0 ps-3 small">
@foreach ($transaksi['books'] as $buku)
<li>{{ $buku['judul'] }}</li>
@endforeach
</ul>
</td>
<td>{{ $transaksi['tanggal_pinjam']->format('d/m/Y') }}</td>
<td>{{ $transaksi['tenggat_kembali']->format('d/m/Y') }}</td>
<td>
<span class="{{ $statusClass }}">{{ $statusText }}</span>
</td>
<td>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal"
data-bs-target="#modalPengembalian-{{ $transaksi['id_peminjaman'] }}">
Proses Pengembalian
</button>
</td>
</tr>
<div class="modal fade"
id="modalPengembalian-{{ $transaksi['id_peminjaman'] }}-{{ $buku['id'] }}"
tabindex="-1" aria-labelledby="modalLabel-{{ $buku['id'] }}" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-3">
<div class="modal-header p-4">
<h1 class="modal-title fs-5" id="modalLabel-{{ $buku['id'] }}">
Proses Pengembalian Buku</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
{{-- MODAL PENGEMBALIAN --}}
<div class="modal fade" id="modalPengembalian-{{ $transaksi['id_peminjaman'] }}"
tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-3">
<div class="modal-header p-4">
<h5 class="modal-title fw-bold">Proses Pengembalian
({{ count($transaksi['books']) }} Buku)
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<div class="alert alert-light border mb-4">
<div class="d-flex justify-content-between">
<span>Peminjam:
<strong>{{ $transaksi['peminjam'] }}</strong></span>
<span>ID: <strong>{{ $transaksi['id_peminjaman'] }}</strong></span>
</div>
</div>
<div class="modal-body p-4">
<div class="row g-4">
<div class="col-md-6">
<h6 class="fw-bold">Informasi Peminjaman</h6>
<div class="mb-3">
<label class="form-label small text-muted">Nama
Peminjam</label>
<input type="text" class="form-control"
value="{{ $transaksi['peminjam'] }}" readonly>
</div>
<div class="mb-3">
<label class="form-label small text-muted">Judul
Buku</label>
<input type="text" class="form-control"
value="{{ $buku['judul'] }}" readonly>
</div>
<hr>
<h6 class="fw-bold">1. Kondisi Buku</h6>
<div class="form-check">
<input class="form-check-input radio-kondisi" type="radio"
name="kondisi_buku_{{ $buku['id'] }}"
id="kondisi_baik_{{ $buku['id'] }}" value="baik"
checked data-denda="0">
<label class="form-check-label"
for="kondisi_baik_{{ $buku['id'] }}">
Kondisi Baik
</label>
</div>
<div class="form-check">
<input class="form-check-input radio-kondisi" type="radio"
name="kondisi_buku_{{ $buku['id'] }}"
id="kondisi_rusak_{{ $buku['id'] }}" value="rusak"
data-denda="0">
<label class="form-check-label"
for="kondisi_rusak_{{ $buku['id'] }}">
Buku Rusak
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input radio-kondisi" type="radio"
name="kondisi_buku_{{ $buku['id'] }}"
id="kondisi_hilang_{{ $buku['id'] }}" value="hilang"
data-denda="0">
<label class="form-check-label"
for="kondisi_hilang_{{ $buku['id'] }}">
Buku Hilang
</label>
</div>
<div class="mb-3 d-none denda-rusak-input-wrapper">
<label class="form-label text-danger">Denda
Kerusakan/Kehilangan (Rp)</label>
<input type="number" class="form-control denda-rusak-input"
value="0" placeholder="Masukkan nominal denda">
</div>
</div>
<div class="col-md-6">
<h6 class="fw-bold">2. Status Keterlambatan</h6>
<div class="mb-3">
<label class="form-label small text-muted">Status</label>
<input type="text" class="form-control"
value="{{ $statusKeterlambatan }}" readonly>
</div>
<div class="mb-3">
<label class="form-label small text-muted">Denda
Keterlambatan</label>
<input type="text"
class="form-control denda-keterlambatan-display"
value="Rp {{ number_format($dendaKeterlambatan, 0, ',', '.') }}"
readonly
data-denda-keterlambatan="{{ $dendaKeterlambatan }}">
</div>
<hr>
<h6 class="fw-bold">3. Rangkuman & Aksi</h6>
<div class="mb-3">
<label for="catatan_petugas_{{ $buku['id'] }}"
class="form-label small text-muted">Catatan Petugas
(Opsional)
</label>
<textarea class="form-control" id="catatan_petugas_{{ $buku['id'] }}" rows="2"></textarea>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox"
role="switch" id="notif_wa_{{ $buku['id'] }}"
checked>
<label class="form-check-label"
for="notif_wa_{{ $buku['id'] }}">
Kirim notifikasi ke WA ({{ $transaksi['nomor_hp'] }})
</label>
</div>
<div class="p-3 bg-primary-light rounded-3 text-primary">
<h5 class="fw-bold mb-0 text-center">Total Denda Diterima
</h5>
<h3 class="fw-bold mb-0 text-center total-denda-display">
Rp
{{ number_format($dendaKeterlambatan, 0, ',', '.') }}
</h3>
<h6 class="fw-bold mb-3">Daftar Buku yang Dikembalikan:</h6>
@foreach ($transaksi['books'] as $buku)
<div class="card mb-3 border bg-light">
<div class="card-body p-3">
<h6 class="fw-bold text-primary mb-2">{{ $buku['judul'] }}</h6>
<div class="row">
<div class="col-md-6">
<label class="form-label small text-muted mb-1">Kondisi
Buku</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input radio-kondisi"
type="radio"
name="kondisi_{{ $buku['id'] }}_{{ $transaksi['id_peminjaman'] }}"
value="baik" checked>
<label
class="form-check-label small">Baik</label>
</div>
<div class="form-check">
<input class="form-check-input radio-kondisi"
type="radio"
name="kondisi_{{ $buku['id'] }}_{{ $transaksi['id_peminjaman'] }}"
value="rusak">
<label
class="form-check-label small">Rusak/Hilang</label>
</div>
</div>
</div>
<div class="col-md-6 denda-rusak-input-wrapper d-none">
<label class="form-label small text-muted mb-1">Denda
Kerusakan</label>
<input type="number"
class="form-control form-control-sm denda-rusak-input"
placeholder="0">
</div>
<div class="col-12">
<label class="form-label small text-muted mb-1">
<i class="bi bi-pencil-square me-1"></i>Catatan
Petugas
</label>
<textarea class="form-control form-control-sm" rows="2"
placeholder="Contoh: Ada coretan di halaman 10, Cover sedikit terlipat, dll..."></textarea>
</div>
</div>
</div>
</div>
@endforeach
<hr>
<div class="d-flex justify-content-between align-items-center mb-3">
<span>Denda Keterlambatan ({{ $statusText }})</span>
@php
$dendaTelat = 0;
if ($isTerlambat) {
$hari = $tenggat->startOfDay()->diffInDays($now->startOfDay());
$dendaTelat = $hari * 1000;
}
@endphp
{{-- Data Attribute untuk JS --}}
<strong class="text-danger denda-keterlambatan-display"
data-denda-keterlambatan="{{ $dendaTelat }}">
Rp {{ number_format($dendaTelat, 0, ',', '.') }}
</strong>
</div>
<div class="modal-footer p-4">
<button type="button" class="btn btn-outline-primary"
data-bs-dismiss="modal">Batal</button>
<button type="button" class="btn btn-primary">Konfirmasi
Pengembalian</button>
<div
class="d-flex justify-content-between align-items-center mb-3 bg-danger bg-opacity-10 p-2 rounded">
<span class="fw-bold text-danger">TOTAL YANG HARUS DIBAYAR</span>
<strong class="text-danger fs-5 total-denda-display">
Rp {{ number_format($dendaTelat, 0, ',', '.') }}
</strong>
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" role="switch" checked
id="waStrukToggle-{{ $transaksi['id_peminjaman'] }}">
<label class="form-check-label small"
for="waStrukToggle-{{ $transaksi['id_peminjaman'] }}">
Kirim <strong>Bukti Pengembalian</strong> ke WhatsApp siswa
</label>
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input email-toggle" type="checkbox"
role="switch" checked
id="emailStrukToggle-{{ $transaksi['id_peminjaman'] }}">
<label class="form-check-label small"
for="emailStrukToggle-{{ $transaksi['id_peminjaman'] }}">
Kirim Bukti Pengembalian</strong> ke Email
</label>
</div>
</div>
<div class="modal-footer p-3 bg-light">
<button type="button" class="btn btn-outline-secondary"
data-bs-dismiss="modal">Batal</button>
<button type="button" class="btn btn-primary btn-konfirmasi-kembali"
data-nama-peminjam="{{ $transaksi['peminjam'] }}"
data-nomor-hp="{{ $transaksi['nomor_hp'] }}">
Konfirmasi & Selesai
</button>
</div>
</div>
</div>
@endforeach
</div>
@empty
<tr>
<td colspan="8" class="text-center py-5">
<div class="empty-state">
<i class="bi bi-journal-x"></i>
<p>Tidak ada data peminjaman yang sedang aktif.</p>
</div>
</td>
<td colspan="8" class="text-center py-5">Tidak ada data.</td>
</tr>
@endforelse
</tbody>
@ -220,44 +223,53 @@ class="form-label small text-muted">Catatan Petugas
@push('scripts')
<script>
// Inisialisasi DataTables
$(document).ready(function() {
$('#peminjamanTable').DataTable({
pageLength: 10,
order: [
[0, 'asc']
],
});
});
// Logika Modal
document.addEventListener('DOMContentLoaded', function() {
function formatRupiah(angka) {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(angka).replace(/\s/g, '');
}).format(angka).replace(/\s/g, '');
}
function hitungTotalDenda(modal) {
const dendaKeterlambatanEl = modal.querySelector('.denda-keterlambatan-display');
const dendaRusakEl = modal.querySelector('.denda-rusak-input');
const dendaRusakInputs = modal.querySelectorAll('.denda-rusak-input');
const totalDendaEl = modal.querySelector('.total-denda-display');
const dendaKeterlambatan = parseInt(dendaKeterlambatanEl.dataset.dendaKeterlambatan) || 0;
const dendaRusak = parseInt(dendaRusakEl.value) || 0;
const totalDenda = dendaKeterlambatan + dendaRusak;
let dendaKeterlambatan = parseInt(dendaKeterlambatanEl.dataset.dendaKeterlambatan) || 0;
let totalDendaRusak = 0;
dendaRusakInputs.forEach(input => {
if (!input.closest('.denda-rusak-input-wrapper').classList.contains('d-none')) {
totalDendaRusak += parseInt(input.value) || 0;
}
});
const totalDenda = dendaKeterlambatan + totalDendaRusak;
totalDendaEl.textContent = formatRupiah(totalDenda);
}
// Logic Radio Button Kondisi Buku
const radioKondisi = document.querySelectorAll('.radio-kondisi');
radioKondisi.forEach(radio => {
radio.addEventListener('change', function() {
const modal = this.closest('.modal-content');
const dendaRusakWrapper = modal.querySelector('.denda-rusak-input-wrapper');
const dendaRusakInput = modal.querySelector('.denda-rusak-input');
if (this.value === 'rusak' || this.value === 'hilang') {
const cardBody = this.closest('.card-body');
const dendaRusakWrapper = cardBody.querySelector('.denda-rusak-input-wrapper');
const dendaRusakInput = cardBody.querySelector('.denda-rusak-input');
const modal = this.closest('.modal');
if (this.value === 'rusak') {
dendaRusakWrapper.classList.remove('d-none');
} else {
dendaRusakWrapper.classList.add('d-none');
@ -267,15 +279,71 @@ function hitungTotalDenda(modal) {
});
});
// Logic Input Denda Rusak
const dendaRusakInputs = document.querySelectorAll('.denda-rusak-input');
dendaRusakInputs.forEach(input => {
input.addEventListener('input', function() {
const modal = this.closest('.modal-content');
const modal = this.closest('.modal');
hitungTotalDenda(modal);
});
});
});
$(document).on('click', '.btn-konfirmasi-kembali', function() {
const nama = $(this).data('nama-peminjam');
const modalEl = $(this).closest('.modal');
const modalInstance = bootstrap.Modal.getInstance(modalEl[0]);
const isEmailChecked = modalEl.find('.email-toggle').is(':checked');
modalInstance.hide();
modernSwal.fire({
title: 'Selesaikan Transaksi?',
text: `Buku dari ${nama} akan ditandai sudah kembali.`,
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Selesaikan',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
// Loading Simpan DB
modernSwal.fire({
title: 'Menyimpan Data...',
timer: 800,
timerProgressBar: true,
didOpen: () => Swal.showLoading()
}).then(() => {
// Loading Kirim Email (Jika dicentang)
if (isEmailChecked) {
const dummyEmail = nama.replace(/\s+/g, '.').toLowerCase() +
'@sekolah.sch.id';
modernSwal.fire({
title: 'Mengirim Email...',
html: `Mengirim nota ke: <b>${dummyEmail}</b>`,
timer: 2000,
timerProgressBar: true,
didOpen: () => Swal.showLoading()
}).then(() => {
finishTransaction(true);
});
} else {
finishTransaction(false);
}
});
} else {
modalInstance.show();
}
});
function finishTransaction(withEmail) {
Toast.fire({
icon: 'success',
title: 'Berhasil',
text: withEmail ? 'Buku kembali & Email terkirim.' : 'Buku berhasil dikembalikan.'
});
setTimeout(() => location.reload(), 1500);
}
});
</script>
@endpush
</x-app-layout>

View File

@ -6,6 +6,28 @@
Pinjam Buku Baru
</button> --}}
{{-- CEK STATUS BANNED --}}
@if (Auth::user()->is_banned)
<div class="alert alert-danger border-0 shadow-sm d-flex align-items-center mb-4" role="alert">
<div class="display-1 text-danger me-4">
<i class="bi bi-shield-lock-fill"></i>
</div>
<div>
<h4 class="alert-heading fw-bold">AKUN ANDA DIBEKUKAN SEMENTARA!</h4>
<p class="mb-1">
Sistem mendeteksi adanya <strong>keterlambatan pengembalian buku</strong> atau <strong>tunggakan
denda</strong> yang belum diselesaikan.
</p>
<hr>
<p class="mb-0 small">
<i class="bi bi-info-circle me-1"></i>
Fitur peminjaman buku dinonaktifkan. Silakan segera kembalikan buku ke perpustakaan atau hubungi
petugas untuk mengaktifkan kembali akun Anda.
</p>
</div>
</div>
@endif
<div class="card border-0 shadow-sm rounded-4 my-4 p-4 position-relative" style="background-color: #CEDEFF;">
<div class="row">
<div class="col-12 col-md-8 p-4 p-md-5" style="z-index: 2;">
@ -192,7 +214,9 @@ class="badge bg-{{ $item['type'] }}-soft text-{{ $item['type'] }} rounded-pill p
@forelse($bukuPinjamOffline as $buku)
<div class="col-xl-4 col-md-6" x-show="expanded || {{ $loop->index }} < 3" x-transition>
<x-book-card :buku="$buku">
<div class="d-flex align-items-center text-danger bg-white bg-opacity-75 rounded-2 px-2 py-1"> <i class="bi bi-clock-fill me-2 shadow-md"></i>
<div
class="d-flex align-items-center text-danger bg-white bg-opacity-75 rounded-2 px-2 py-1">
<i class="bi bi-clock-fill me-2 shadow-md"></i>
<span class="fw-bold small">Sisa: {{ $buku['sisa_hari'] }} hari</span>
</div>
</x-book-card>

View File

@ -68,48 +68,44 @@ class="badge fw-normal {{ $buku['status'] == 'Tersedia' ? 'bg-success-subtle tex
</div>
<div class="d-flex flex-column gap-2 mt-auto pt-2 border-top">
@if ($mode === 'offline')
@if ($buku['status'] == 'Dipinjam')
<button class="btn btn-sm btn-secondary w-100" disabled>
<i class="bi bi-x-circle me-1"></i> Tidak Tersedia
@php
$userBanned = $isBanned ?? false;
$isOfflineAccess =
$buku['tipe_akses'] === 'offline' ||
(is_array($buku['tipe_akses']) && in_array('offline', $buku['tipe_akses']));
$isOnlineAccess =
$buku['tipe_akses'] === 'online' ||
(is_array($buku['tipe_akses']) && in_array('online', $buku['tipe_akses']));
$stokHabis = $buku['status'] == 'Dipinjam';
@endphp
{{-- TOMBOL PINJAM (OFFLINE) --}}
@if ($isOfflineAccess && $mode !== 'online')
@if ($stokHabis)
@if (!$isOnlineAccess)
<button class="btn btn-sm btn-light border w-100 text-muted" disabled><i
class="bi bi-x-circle me-1"></i> Tidak Tersedia</button>
@endif
@elseif($userBanned)
{{-- LOGIC : Jika Banned, tombol Pinjam MATI --}}
<button class="btn btn-sm btn-secondary w-100" disabled
title="Akun Anda bermasalah">
<i class="bi bi-lock-fill me-1"></i> Akun Dikunci
</button>
@else
<a href="{{ route('peminjaman.ringkasan', $buku['id']) }}"
class="btn btn-sm btn-outline-primary w-100">
<i class="bi bi-arrow-left-right me-1"></i> Pinjam
<i class="bi bi-arrow-left-right me-1"></i> Pinjam
</a>
@endif
@elseif($mode === 'online')
@endif
{{-- TOMBOL BACA (ONLINE) - Selalu Nyala --}}
@if ($isOnlineAccess && $mode !== 'offline')
<a href="{{ route('baca.ringkasan', $buku['id']) }}"
class="btn btn-sm btn-primary w-100">
<i class="bi bi-search me-1"></i> Baca
<i class="bi bi-book-half me-1"></i> Baca
</a>
@else
{{-- Mode 'umum' --}}
@if ($buku['status'] == 'Dipinjam')
<button class="btn btn-sm btn-secondary w-100" disabled>
<i class="bi bi-x-circle me-1"></i> Tidak Tersedia
</button>
@else
@if (is_array($buku['tipe_akses']))
<a href="{{ route('peminjaman.ringkasan', $buku['id']) }}"
class="btn btn-sm btn-outline-primary flex-fill"><i
class="bi bi-arrow-left-right me-1"></i> Pinjam</a>
<a href="{{ route('baca.ringkasan', $buku['id']) }}"
class="btn btn-sm btn-primary flex-fill"><i class="bi bi-book-half me-1"></i>
Baca</a>
@elseif ($buku['tipe_akses'] === 'offline')
<a href="{{ route('peminjaman.ringkasan', $buku['id']) }}"
class="btn btn-sm btn-outline-primary w-100">
<i class="bi bi-arrow-left-right me-1"></i> Pinjam
</a>
@elseif ($buku['tipe_akses'] === 'online')
<a href="{{ route('baca.ringkasan', $buku['id']) }}"
class="btn btn-sm btn-primary w-100">
<i class="bi bi-book-half me-1"></i> Baca
</a>
@endif
@endif
@endif
</div>
</div>

View File

@ -32,6 +32,13 @@
<div class="main-wrapper flex-grow-1">
@include('layouts.navigation')
<main class="container-fluid py-4 px-4">
@if (session('error'))
<div class="alert alert-danger alert-dismissible fade show m-3" role="alert">
<i class="bi bi-exclamation-octagon-fill me-2"></i>
<strong>DITOLAK!</strong> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
{{ $slot }}
</main>
<footer class="footer text-center py-3">
@ -60,6 +67,8 @@
if (isset($item['type'])) {
if ($item['type'] === 'riwayat_peminjaman') {
$url = route('riwayat.offline');
} elseif ($item['type'] === 'denda_active') {
$url = route('riwayat.offline');
} elseif ($item['type'] === 'rekomendasi' && isset($item['link_id'])) {
$url = route('rekomendasi.show', $item['link_id']);
} elseif ($item['type'] === 'katalog_kategori' && isset($item['link_id'])) {
@ -103,6 +112,8 @@ class="notification-item d-flex my-1 rounded-3 text-body text-decoration-none @i
<!-- Tom Select -->
<script src="https://cdn.jsdelivr.net/npm/tom-select/dist/js/tom-select.complete.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const sidebar = document.getElementById('sidebar');
@ -150,6 +161,34 @@ function toggleDesktopSidebar() {
});
});
});
const modernSwal = Swal.mixin({
customClass: {
popup: 'rounded-4 border-0 shadow-lg',
title: 'fw-bold text-dark fs-4',
html: 'text-muted',
confirmButton: 'btn btn-primary rounded-pill px-4 py-2 fw-bold ms-2',
cancelButton: 'btn btn-light text-muted rounded-pill px-4 py-2 fw-bold'
},
buttonsStyling: false,
reverseButtons: true,
padding: '2rem',
});
// Preset untuk Notifikasi Sukses
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
customClass: {
popup: 'rounded-3 shadow-sm border-0 bg-white'
},
didOpen: (toast) => {
toast.addEventListener('mouseenter', Swal.stopTimer)
toast.addEventListener('mouseleave', Swal.resumeTimer)
}
});
</script>
@stack('scripts')

View File

@ -44,6 +44,8 @@ class="badge bg-primary-subtle text-primary-emphasis rounded-pill">{{ $unreadNot
if (isset($item['type'])) {
if ($item['type'] === 'riwayat_peminjaman') {
$url = route('riwayat.offline');
} elseif ($item['type'] === 'denda_active') {
$url = route('riwayat.offline');
} elseif ($item['type'] === 'rekomendasi' && isset($item['link_id'])) {
$url = route('rekomendasi.show', $item['link_id']);
} elseif ($item['type'] === 'katalog_kategori' && isset($item['link_id'])) {

View File

@ -39,6 +39,10 @@ class="bi bi-book-fill"></i><span class="nav-text ms-2">Manajemen Buku</span></a
class="nav-link {{ request()->routeIs('admin.peminjaman.*') ? 'active' : '' }}"> <i
class="bi bi-arrow-left-right"></i><span class="nav-text ms-2">Manajemen Pinjaman</span> </a>
</li>
<li class="nav-item"> <a href="{{ route('admin.denda.index') }}"
class="nav-link {{ request()->routeIs('admin.denda.*') ? 'active' : '' }}"> <i
class="bi-exclamation-octagon"></i><span class="nav-text ms-2">Manajemen Denda</span> </a>
</li>
<li class="nav-item"><a href="{{ route('admin.pengumuman.index') }}"
class="nav-link {{ request()->routeIs('admin.pengumuman.*') ? 'active' : '' }}"><i
class="bi bi-megaphone-fill"></i><span class="nav-text ms-2">Pengumuman</span></a>

View File

@ -18,7 +18,8 @@
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h5 class="fw-bold m-0">Informasi Peminjam</h5>
<a href="{{ route('profile.edit') }}" class="btn btn-sm btn-outline-secondary ">Edit Profil</a>
<a href="{{ route('profile.edit') }}" class="btn btn-sm btn-outline-secondary ">Edit
Profil</a>
</div>
<div class="card-body p-4">
<div class="row g-3">
@ -95,8 +96,6 @@ class="rounded me-3 form-book-cover">
value="{{ \Carbon\Carbon::now()->addDays(7)->format('d F Y') }}">
</div>
</div>
</div>
{{-- Peraturan Peminjaman --}}
@ -105,7 +104,7 @@ class="rounded me-3 form-book-cover">
<div class="alert alert-info border-0 bg-info-subtle">
<ol class="mb-0 ps-3">
<li>Perpustakaan buka pada jam operasional sekolah.</li>
<li>Setiap siswa hanya dapat meminjam sebanyak 2-3 buku dalam satu waktu.</li>
<li>Setiap siswa hanya dapat meminjam sebanyak 1-2 buku dalam satu waktu.</li>
<li>Buku yang dipinjam harus dikembalikan dalam jangka waktu yang telah ditentukan.
</li>
<li>Jika buku tidak dikembalikan tepat waktu, akan dikenakan denda atau sanksi
@ -147,8 +146,8 @@ class="rounded me-3 form-book-cover">
<div class="modal-body">
<div class="alert alert-info border-0 bg-info-subtle mb-4 d-flex align-items-center">
<i class="bi bi-info-circle-fill me-2"></i>
<span>Anda dapat memilih maksimal <strong><span id="sisaSlot">2</span> buku</strong> lagi.
Total maksimal 3 buku.</span>
<span>Anda dapat memilih maksimal <strong><span id="sisaSlot">1</span> buku</strong> lagi.
Total maksimal 2 buku.</span>
</div>
<div class="mb-3">
@ -206,20 +205,20 @@ class="bi bi-star-fill me-1"></i>Buku Utama</span>
@endforeach
</div>
<!-- Counter Buku Terpilih -->
<div class="mt-4 p-3 bg-light rounded shadow-sm">
<div class="d-flex justify-content-between align-items-center">
<span class="fw-semibold">
<i class="bi bi-collection me-2 text-primary"></i>Buku Terpilih:
<span id="counterBuku" class="text-dark">1</span>/3
</span>
<ul>
<li id="selectedBooks" class="text-muted small"></li>
</ul>
</div>
</div>
</div>
<!-- Counter Buku Terpilih -->
<div class="p-3 bg-light">
<div class="d-flex justify-content-between align-items-center">
<span class="fw-semibold">
<i class="bi bi-collection me-2 text-primary"></i>Buku Terpilih:
<span id="counterBuku" class="text-dark">1</span>/2
</span>
<ul>
<li id="selectedBooks" class="text-muted small"></li>
</ul>
</div>
</div>
<!-- Footer -->
<div class="modal-footer bg-light">
@ -252,6 +251,23 @@ class="bi bi-star-fill me-1"></i>Buku Utama</span>
<div id="ringkasanBuku" class="mt-2"></div>
</div>
</div>
<div class="alert alert-warning border-0 border-start border-4 border-warning shadow-sm mb-4"
role="alert">
<div class="d-flex">
<div class="fs-1 me-3 text-warning">
<i class="bi bi-exclamation-circle-fill"></i>
</div>
<div>
<h6 class="fw-bold text-dark mb-1">PENTING: Aturan Peminjaman!</h6>
<p class="mb-0 text-muted small">
Sesuai peraturan perpustakaan, durasi peminjaman buku maksimal adalah
<strong class="text-dark bg-warning-subtle px-2 py-1 rounded">2 HARI</strong>.
<br>
Mohon kembalikan tepat waktu untuk menghindari denda (Rp 1.000/hari).
</p>
</div>
</div>
</div>
<div class="modal-footer border-0 justify-content-center">
<button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Batal</button>
@ -268,30 +284,41 @@ class="bi bi-star-fill me-1"></i>Buku Utama</span>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const tglPinjam = flatpickr("#tanggalPinjam", {
// (Default Hari Ini + 2 Hari)
// Simpan instance-nya ke variabel
const fpKembali = flatpickr("#tanggalKembali", {
dateFormat: "d F Y",
altInput: true,
altFormat: "d F Y",
defaultDate: "{{ \Carbon\Carbon::now()->format('Y-m-d') }}",
defaultDate: new Date().fp_incr(2),
locale: "id",
minDate: "today",
onChange: function(selectedDates, dateStr) {
if (tglKembali.selectedDates[0] <= selectedDates[0]) {
const newDate = new Date(selectedDates[0]);
newDate.setDate(newDate.getDate() + 1);
tglKembali.setDate(newDate);
}
tglKembali.set("minDate", new Date(selectedDates[0]).fp_incr(1));
}
minDate: new Date().fp_incr(1),
maxDate: new Date().fp_incr(2)
});
const tglKembali = flatpickr("#tanggalKembali", {
// Inisialisasi Tanggal Pinjam
flatpickr("#tanggalPinjam", {
dateFormat: "d F Y",
altInput: true,
altFormat: "d F Y",
defaultDate: "{{ \Carbon\Carbon::now()->addDays(7)->format('Y-m-d') }}",
defaultDate: "today",
locale: "id",
minDate: new Date().fp_incr(1)
minDate: "today",
// LOGIC : Saat Tanggal Pinjam Berubah
onChange: function(selectedDates, dateStr) {
if (selectedDates.length > 0) {
const tglMulai = selectedDates[0];
const maxDateBaru = new Date(tglMulai).fp_incr(2);
const minDateBaru = new Date(tglMulai).fp_incr(1);
fpKembali.set("minDate", minDateBaru);
fpKembali.set("maxDate", maxDateBaru);
fpKembali.setDate(maxDateBaru);
}
}
});
});
</script>

View File

@ -21,58 +21,81 @@
</thead>
<tbody>
@php $counter = 1; @endphp
@forelse ($riwayatOffline as $transaksi)
@foreach ($transaksi['books'] as $buku)
<tr>
<td>{{ $counter++ }}</td>
<td>{{ $transaksi['id_peminjaman'] }}</td>
<td>{{ $buku['kode_buku'] }}</td>
<td>{{ $buku['judul'] }}</td>
<td>{{ $transaksi['tanggal_pinjam'] }}</td>
<td>{{ $transaksi['tanggal_kembali'] }}</td>
<td>
@if ($transaksi['status'] == 'Dikembalikan')
<span
class="badge rounded-pill bg-success-subtle text-success-emphasis">{{ $transaksi['status'] }}</span>
@else
<span
class="badge rounded-pill bg-warning-subtle text-warning-emphasis">{{ $transaksi['status'] }}</span>
@endif
</td>
<td>
<button class="btn btn-primary btn-sm text-white" data-bs-toggle="modal"
data-bs-target="#detailModal" data-transaksi-id="{{ $transaksi['id'] }}"
data-buku-id="{{ $buku['id'] }}">
Detail
</button>
</td>
</tr>
@if (count($riwayatOffline) > 0)
@foreach ($riwayatOffline as $transaksi)
@foreach ($transaksi['books'] as $buku)
@php
try {
$tglKembali = \Carbon\Carbon::createFromFormat(
'd/m/Y',
$transaksi['tanggal_kembali'],
)->startOfDay();
} catch (\Exception $e) {
$tglKembali = \Carbon\Carbon::parse(
$transaksi['tanggal_kembali'],
)->startOfDay();
}
$today = now()->startOfDay();
$isTelat = $today->gt($tglKembali) && $transaksi['status'] == 'Dipinjam';
$hariTelat = 0;
$denda = 0;
if ($isTelat) {
$hariTelat = $tglKembali->diffInDays($today);
$denda = $hariTelat * 1000;
}
@endphp
<tr>
<td>{{ $counter++ }}</td>
<td>{{ $transaksi['id_peminjaman'] }}</td>
<td>{{ $buku['kode_buku'] }}</td>
<td>{{ $buku['judul'] }}</td>
<td>{{ $transaksi['tanggal_pinjam'] }}</td>
<td>{{ $transaksi['tanggal_kembali'] }}</td>
<td>
@if ($transaksi['status'] == 'Dikembalikan')
<span
class="badge rounded-pill bg-success-subtle text-success-emphasis">Dikembalikan</span>
@elseif($isTelat)
<span class="badge rounded-pill bg-danger text-white">Terlambat</span>
@else
<span
class="badge rounded-pill bg-warning-subtle text-warning-emphasis">Dipinjam</span>
@endif
</td>
<td>
<button class="btn btn-primary btn-sm text-white" data-bs-toggle="modal"
data-bs-target="#detailModal" data-transaksi-id="{{ $transaksi['id'] }}"
data-buku-id="{{ $buku['id'] }}"
data-is-telat="{{ $isTelat ? 'true' : 'false' }}"
data-hari-telat="{{ $hariTelat }}"
data-denda="{{ number_format($denda, 0, ',', '.') }}">
Detail
</button>
</td>
</tr>
@endforeach
@endforeach
@empty
<tr>
<td colspan="7" class="text-center">Tidak ada riwayat peminjaman.</td> </tr>
@endforelse
@endif
</tbody>
</table>
</div>
</div>
</div>
{{-- MODAL DETAIL RIWAYAT --}}
<div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
{{-- MODAL DETAIL --}}
<div class="modal fade" id="detailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold" id="detailModalLabel"><i
class="bi bi-book-half me-2 text-primary"></i>Detail Riwayat</h5>
<h5 class="modal-title fw-bold"><i class="bi bi-book-half me-2 text-primary"></i>Detail Riwayat</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<div id="modal-content-placeholder" class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div id="modal-content-placeholder" class="text-center">Loading...</div>
</div>
</div>
</div>
@ -90,9 +113,20 @@ class="bi bi-book-half me-2 text-primary"></i>Detail Riwayat</h5>
order: [
[0, 'asc']
],
language: {
emptyTable: `
<div class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-2 opacity-50"></i>
Belum ada riwayat peminjaman.
</div>
`,
zeroRecords: "Tidak ada data yang cocok"
},
columnDefs: [{
responsivePriority: 1,
targets: 2
targets: 3
},
{
responsivePriority: 2,
@ -106,75 +140,107 @@ class="bi bi-book-half me-2 text-primary"></i>Detail Riwayat</h5>
});
});
// Modal Detail
// Modal Logic
const detailModal = document.getElementById('detailModal');
detailModal.addEventListener('show.bs.modal', event => {
const button = event.relatedTarget;
const transaksiId = button.getAttribute('data-transaksi-id');
const bukuId = button.getAttribute('data-buku-id');
const modalBody = detailModal.querySelector('#modal-content-placeholder');
const modalTitle = detailModal.querySelector('#detailModalLabel');
const isTelat = button.getAttribute('data-is-telat') === 'true';
const hariTelat = button.getAttribute('data-hari-telat');
const denda = button.getAttribute('data-denda');
const modalBody = detailModal.querySelector('#modal-content-placeholder');
const transaksiItem = riwayatOfflineData.find(item => item.id == transaksiId);
if (!transaksiItem) return;
const buku = transaksiItem.books.find(b => b.id == bukuId);
if (!buku) return;
modalTitle.textContent = `Detail Buku`;
const keteranganHtml = buku.keterangan ?
`
<hr class="my-3">
<div class="alert alert-warning border-0 bg-warning-subtle mb-0">
<h6 class="fw-bold mb-1 text-warning-emphasis"><i class="bi bi-info-circle-fill me-2"></i>Catatan Petugas</h6>
<p class="mb-0">${buku.keterangan}</p>
const dendaHtml = isTelat ? `
<div class="card border-0 border-start border-4 border-danger bg-danger-subtle mb-3 shadow-sm">
<div class="card-body p-3 d-flex align-items-center">
<div class="flex-shrink-0 me-3">
<i class="bi bi-exclamation-octagon-fill text-danger fs-1"></i>
</div>
<div class="text-start">
<h6 class="fw-bold text-danger mb-1">Terlambat Pengembalian!</h6>
<p class="mb-0 small text-danger-emphasis">
Anda terlambat <span class="badge bg-danger text-white">${hariTelat} Hari</span>.
Denda yang harus dibayar:
<span class="fw-bold fs-6 d-block mt-1">Rp ${denda}</span>
</p>
</div>
</div>
` :
'';
</div>` : '';
const keteranganHtml = buku.keterangan ? `
<div class="card border-0 border-start border-4 border-warning bg-warning-subtle mb-4 shadow-sm">
<div class="card-body p-3 d-flex align-items-start">
<div class="flex-shrink-0 me-3">
<i class="bi bi-journal-bookmark-fill text-warning-emphasis fs-3"></i>
</div>
<div class="text-start">
<h6 class="fw-bold text-warning-emphasis mb-1">Catatan Petugas</h6>
<p class="mb-0 small text-dark opacity-75 fst-italic">
"${buku.keterangan}"
</p>
</div>
</div>
</div>` : '';
const contentHTML = `
// RENDER HTML
modalBody.innerHTML = `
<div class="text-center mb-4">
<img src="{{ asset('${buku.cover}') }}" alt="Cover ${buku.judul}" class="img-fluid rounded shadow-sm" style="max-width: 150px;">
<img src="{{ asset('') }}${buku.cover}" class="img-fluid rounded shadow-sm" style="max-width: 140px;">
</div>
<h4 class="fw-bold text-center">${buku.judul}</h4>
<p class="text-muted text-center">${buku.deskripsi}</p>
<h5 class="fw-bold text-center mb-1">${buku.judul}</h5>
<p class="text-muted text-center small mb-4">${buku.deskripsi}</p>
<hr class="my-4">
<table class="table table-borderless table-sm">
<tbody>
<tr>
<td class="fw-bold text-start" style="width: 35%;">ID Peminjaman</td>
<td style="width: 5%;">:</td>
<td class="text-start">${transaksiItem.id_peminjaman}</td>
</tr>
<tr>
<td class="fw-bold text-start">Kode Buku</td>
<td>:</td>
<td class="text-start">${buku.kode_buku}</td>
</tr>
<tr>
<td class="fw-bold text-start">Kategori Buku</td>
<td>:</td>
<td class="text-start"><span class="badge bg-primary-subtle text-primary-emphasis rounded-pill px-3 py-2">${buku.kategori}</span></td>
</tr>
<tr>
<td class="fw-bold text-start">Tahun Terbit</td>
<td>:</td>
<td class="text-start">${buku.tahun}</td>
</tr>
</tbody>
</table>
${dendaHtml}
${keteranganHtml}
${keteranganHtml} `;
modalBody.innerHTML = contentHTML;
<div class="card bg-light border-0 rounded-3 p-3">
<table class="table table-borderless table-sm mb-0">
<tbody>
<tr>
<td class="fw-bold text-start" style="width: 35%;">ID Peminjaman</td>
<td style="width: 1%;">:</td>
<td class="text-start">${transaksiItem.id_peminjaman}</td>
</tr>
<tr>
<td class="fw-bold text-start">Kode Buku</td>
<td>:</td>
<td class="text-start fw-medium">${buku.kode_buku}</td>
</tr>
<tr>
<td class="fw-bold text-start align-middle">Kategori</td>
<td class="align-middle">:</td>
<td class="text-start">
<span class="badge bg-primary-subtle text-primary-emphasis rounded-pill px-3 py-2">
${buku.kategori}
</span>
</td>
</tr>
<tr>
<td class="fw-bold text-start">Tahun Terbit</td>
<td>:</td>
<td class="text-start">${buku.tahun}</td>
</tr>
<tr>
<td class="fw-bold text-start">Tgl Pinjam</td>
<td>:</td>
<td class="text-start">${transaksiItem.tanggal_pinjam}</td>
</tr>
<tr>
<td class="fw-bold text-start">Tenggat</td>
<td>:</td>
<td class="text-start">${transaksiItem.tanggal_kembali}</td>
</tr>
</tbody>
</table>
</div>`;
});
</script>
@endpush
</x-app-layout>
</x-app-layout>

View File

@ -100,6 +100,9 @@
Route::get('/peminjaman', [AdminPeminjamanController::class, 'index'])->name('peminjaman.index');
Route::get('/peminjaman/tambah', [AdminPeminjamanController::class, 'create'])->name('peminjaman.create');
Route::get('/denda', [AdminPeminjamanController::class, 'dendaIndex'])->name('denda.index');
Route::post('/denda/sanksi', [AdminPeminjamanController::class, 'berikanSanksi'])->name('denda.sanksi');
});
// --- RUTE LOGIN KHUSUS ADMIN ---