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. * 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 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 retrieveByToken($identifier, $token) { return null; }
public function updateRememberToken(Authenticatable $user, $token) { } public function updateRememberToken(Authenticatable $user, $token) { }
public function retrieveByCredentials(array $credentials) { return null; } public function retrieveByCredentials(array $credentials) { return null; }

View File

@ -9,36 +9,78 @@
class AdminPeminjamanController extends Controller class AdminPeminjamanController extends Controller
{ {
/**
* Menampilkan halaman utama manajemen peminjaman (Tabel).
*/
public function index(Request $request) public function index(Request $request)
{ {
// Panggil data dari DummyDataService
$peminjamanAktif = DummyDataService::getAdminPeminjamanAktif(); $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(); $daftarPeminjam = $peminjamanAktif->pluck('peminjam')->unique();
return view('admin.peminjaman.index', [ return view('admin.peminjaman.index', [
'pageTitle' => 'Manajemen Peminjaman', 'pageTitle' => 'Manajemen Peminjaman & Denda',
'peminjamanAktif' => $peminjamanAktif, 'peminjamanAktif' => $peminjamanAktif,
'daftarPeminjam' => $daftarPeminjam, 'daftarPeminjam' => $daftarPeminjam,
]); ]);
} }
/**
* Menampilkan formulir untuk membuat peminjaman manual.
*/
public function create() public function create()
{ {
$allUsers = collect(DummyDataService::getAllSiswa()); $allUsers = collect(DummyDataService::getAllSiswa());
$peminjamanAktif = DummyDataService::getAdminPeminjamanAktif();
// Kelompokkan berdasarkan 'role'
$groupedUsers = $allUsers $groupedUsers = $allUsers
->whereIn('role', ['siswa', 'guru']) ->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'); ->groupBy('role');
// Filter hanya buku offline
$daftarBuku = DummyDataService::getAllBooks() $daftarBuku = DummyDataService::getAllBooks()
->where('status', 'Tersedia') ->where('status', 'Tersedia')
->filter(function ($buku) { ->filter(function ($buku) {
@ -54,4 +96,96 @@ public function create()
'daftarBuku' => $daftarBuku, '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() public function index()
{ {
$user = Auth::user(); $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(); $stats = DummyDataService::getDashboardStats();
$pengumuman = DummyDataService::getPengumuman(); $pengumuman = DummyDataService::getPengumuman();
$pemberitahuan = DummyDataService::getPemberitahuan(); $pemberitahuan = DummyDataService::getPemberitahuan();
$progressMembaca = DummyDataService::getProgressMembaca(); $progressMembaca = DummyDataService::getProgressMembaca();
$statistikBulanan = DummyDataService::getStatistikBulanan(); $statistikBulanan = DummyDataService::getStatistikBulanan();
$bukuPinjamOffline = DummyDataService::getBukuPinjamOffline($user); $bukuPinjamOffline = $bukuPinjam;
$bacaBukuOnline = DummyDataService::getBacaBukuOnline($user); $bacaBukuOnline = DummyDataService::getBacaBukuOnline($user);
$rekomendasiPembelajaran = DummyDataService::getRekomendasiPembelajaran(); $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 // Menambahkan thumbnail YouTube ke setiap rekomendasi
$rekomendasiPembelajaran = DummyDataService::getRekomendasiPembelajaran()->map(function ($item) { $rekomendasiPembelajaran = $rekomendasiPembelajaran->map(function ($item) {
$videoId = $this->extractYouTubeId($item['youtube_link']); $videoId = $this->extractYouTubeId($item['youtube_link']);
if ($videoId) { if ($videoId) {
$item['thumbnail'] = "https://img.youtube.com/vi/{$videoId}/hqdefault.jpg"; $item['thumbnail'] = "https://img.youtube.com/vi/{$videoId}/hqdefault.jpg";
@ -31,28 +45,30 @@ public function index()
return $item; return $item;
}); });
$dendaAlert = collect($personalNotif)->where('type', 'denda_active');
$hour = date('H'); $hour = date('H');
$greeting = "Selamat Pagi"; $greeting = "Selamat Pagi";
if ($hour >= 12 && $hour < 15) { if ($hour >= 12 && $hour < 15)
$greeting = "Selamat Siang"; $greeting = "Selamat Siang";
} elseif ($hour >= 15 && $hour < 18) { elseif ($hour >= 15 && $hour < 18)
$greeting = "Selamat Sore"; $greeting = "Selamat Sore";
} elseif ($hour >= 18) { elseif ($hour >= 18)
$greeting = "Selamat Malam"; $greeting = "Selamat Malam";
}
return view('dashboard', compact( return view('dashboard', compact(
'user', 'user',
'stats', 'stats',
'pengumuman', 'pengumuman',
'pemberitahuan', 'pemberitahuan',
'dendaAlert',
'progressMembaca', 'progressMembaca',
'statistikBulanan', 'statistikBulanan',
'bukuPinjamOffline', 'bukuPinjamOffline',
'bacaBukuOnline', 'bacaBukuOnline',
'greeting', 'greeting',
'rekomendasiPembelajaran' 'rekomendasiPembelajaran'
)); ))->with('notifikasi', $personalNotif);
} }
/** /**

View File

@ -4,13 +4,23 @@
use App\Services\DummyDataService; use App\Services\DummyDataService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class KatalogController extends Controller class KatalogController extends Controller
{ {
public function index(Request $request) 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); $semuaBuku = DummyDataService::getKatalogBuku($filters);
$filterOptions = DummyDataService::getFilterOptions(); $filterOptions = DummyDataService::getFilterOptions();
@ -20,6 +30,7 @@ public function index(Request $request)
'input' => $filters, 'input' => $filters,
'pageTitle' => 'Katalog Buku', 'pageTitle' => 'Katalog Buku',
'mode' => 'umum', 'mode' => 'umum',
'isBanned' => $isBanned,
]); ]);
} }
} }

View File

@ -10,6 +10,18 @@ class PeminjamanController extends Controller
{ {
public function index(Request $request) 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 = $request->only(['search', 'kategori', 'tahun', 'penulis']);
$filters['tipe_akses'] = 'offline'; $filters['tipe_akses'] = 'offline';
$semuaBuku = DummyDataService::getKatalogBuku($filters); $semuaBuku = DummyDataService::getKatalogBuku($filters);

View File

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

View File

@ -3,6 +3,7 @@
namespace App\Providers; namespace App\Providers;
use App\Services\DummyDataService; use App\Services\DummyDataService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
@ -26,18 +27,18 @@ public function boot(): void
URL::forceScheme('https'); URL::forceScheme('https');
} }
// View Composer untuk semua view (*)
View::composer('*', function ($view) { View::composer('*', function ($view) {
$notifikasi = collect([]); if (Auth::check()) {
$unreadNotificationsCount = 0; $user = Auth::user();
// Hanya ambil notifikasi jika ada pengguna yang login
if (auth()->check()) {
$user = auth()->user();
$notifikasi = collect(DummyDataService::getNotifikasiForUser($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', 'role' => 'siswa',
'kelas' => 'XII RPL', 'kelas' => 'XII RPL',
'golongan' => 'A', 'golongan' => 'A',
'is_banned' => false,
], ],
[ [
'id' => 2, 'id' => 2,
@ -41,6 +42,7 @@ public static function getAllSiswa(): array
'role' => 'siswa', 'role' => 'siswa',
'kelas' => 'XII RPL A', 'kelas' => 'XII RPL A',
'golongan' => 'A', 'golongan' => 'A',
'is_banned' => true,
], ],
[ [
@ -53,6 +55,7 @@ public static function getAllSiswa(): array
'role' => 'siswa', 'role' => 'siswa',
'kelas' => 'XII RPL A', 'kelas' => 'XII RPL A',
'golongan' => 'A', 'golongan' => 'A',
'is_banned' => false,
], ],
[ [
'id' => 5, 'id' => 5,
@ -61,6 +64,7 @@ public static function getAllSiswa(): array
'email' => 'rina.marlina@smkn1perpus.sch.id', 'email' => 'rina.marlina@smkn1perpus.sch.id',
'password' => 'password', 'password' => 'password',
'role' => 'guru', 'role' => 'guru',
'is_banned' => false,
], ],
]; ];
} }
@ -81,157 +85,39 @@ public static function getAktivitasMingguan(): array
*/ */
public static function getAdminPeminjamanAktif(): \Illuminate\Support\Collection public static function getAdminPeminjamanAktif(): \Illuminate\Support\Collection
{ {
return collect([ $allBooks = self::getAllBooks();
[ $allSiswa = collect(self::getAllSiswa());
'id_peminjaman' => 'PIN-202510-001', $bukuDipinjam = $allBooks->whereIn('status', ['Dipinjam', 'Terlambat']);
'peminjam' => 'Silvi Rahmawati', $grouped = $bukuDipinjam->groupBy(function ($item) {
'nomor_hp' => '08123456789', return is_array($item['user_id']) ? $item['user_id'][0] : $item['user_id'];
'tanggal_pinjam' => Carbon::now()->subDays(12), });
'tenggat_kembali' => Carbon::now()->subDays(5), // Terlambat 5 hari
'denda_per_hari' => 1000, return $grouped->map(function ($books, $userId) use ($allSiswa) {
'books' => [ $siswa = $allSiswa->firstWhere('id', $userId);
['id' => 5, 'judul' => 'Si Anak Pintar'], $firstBook = $books->first();
['id' => 8, 'judul' => 'Ayah'], $tglKembali = Carbon::now()->addDays($firstBook['sisa_hari']);
]
], return [
[ 'id_peminjaman' => 'PIN-ADM-' . sprintf('%03d', $userId),
'id_peminjaman' => 'PIN-202510-002', 'user_id' => $userId,
'peminjam' => 'Andi Pratama', 'role' => $siswa ? $siswa['role'] : 'siswa',
'nomor_hp' => '081556677889', 'is_banned' => $siswa ? ($siswa['is_banned'] ?? false) : false,
'tanggal_pinjam' => Carbon::now()->subDays(4),
'tenggat_kembali' => Carbon::now()->addDays(3), // Sisa 3 hari 'peminjam' => $siswa ? $siswa['nama_lengkap'] : 'User Tidak Dikenal',
'denda_per_hari' => 1000, 'nomor_hp' => $siswa ? ($siswa['nomor_hp'] ?? '-') : '-',
'books' => [
['id' => 7, 'judul' => 'The Last Spell Breather'],
]
],
[
'id_peminjaman' => 'PIN-202510-003',
'peminjam' => 'Siti Nurhaliza',
'nomor_hp' => '081998877665',
'tanggal_pinjam' => Carbon::now()->subDays(7), 'tanggal_pinjam' => Carbon::now()->subDays(7),
'tenggat_kembali' => Carbon::now(), // Hari ini 'tenggat_kembali' => $tglKembali,
'denda_per_hari' => 1000, 'denda_per_hari' => 1000,
'books' => [
['id' => 1, 'judul' => 'Modul Ajar IPAS'], 'books' => $books->map(function ($b) {
] return [
], 'id' => $b['id'],
[ 'judul' => $b['judul'],
'id_peminjaman' => 'PIN-202510-004', 'cover' => $b['cover']
'peminjam' => 'Dewi Lestari', ];
'nomor_hp' => '082134567891', })->toArray()
'tanggal_pinjam' => Carbon::now()->subDays(15), ];
'tenggat_kembali' => Carbon::now()->subDays(8), // Terlambat 8 hari })->values();
'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'],
]
],
]);
} }
/** /**
@ -328,7 +214,7 @@ public static function getSiswaTeraktif(): array
['nama' => 'Rina Marlina', 'total_buku' => 20, 'kelas' => 'XII TKJ A'], ['nama' => 'Rina Marlina', 'total_buku' => 20, 'kelas' => 'XII TKJ A'],
['nama' => 'Budi Santoso', 'total_buku' => 18, 'kelas' => 'X OTKP'], ['nama' => 'Budi Santoso', 'total_buku' => 18, 'kelas' => 'X OTKP'],
['nama' => 'Putri Amelia', 'total_buku' => 16, 'kelas' => 'XI RPL C'], ['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'],
]; ];
} }
@ -485,166 +371,172 @@ public static function getStatistikBulanan(): array
public static function getAllBooks() public static function getAllBooks()
{ {
return collect([ return collect([
// --- BUKU MILIK USER 1 (SILVI) ---
[ [
'id' => 1, 'id' => 1,
'judul' => 'Modul Ajar IPAS', 'judul' => 'Modul Ajar IPAS',
'penulis' => 'Tim Kemdikbud Ristek', 'penulis' => 'Tim Kemdikbud Ristek',
'cover' => 'images/covers/ipas.jpg', 'cover' => 'images/covers/ipas.jpg',
'kode_buku' => '510', // 500 (Sains) 'kode_buku' => '510',
'kategori' => 'Sains', 'kategori' => 'Sains',
'tahun' => 2022, 'tahun' => 2022,
'status' => 'Tersedia', 'status' => 'Dipinjam',
'is_new' => true, 'is_new' => true,
'tipe_akses' => ['online', 'offline'], 'tipe_akses' => ['online', 'offline'],
'file_pdf' => 'ipas.pdf', 'sisa_hari' => -5,
'progress' => 75,
'sisa_hari' => 14,
'user_id' => 1, 'user_id' => 1,
], ],
[ [
'id' => 2, 'id' => 2,
'judul' => 'Modul Ajar Pendidikan Pancasila', 'judul' => 'Perahu Kertas',
'penulis' => 'Tim Guru Pancasila', 'penulis' => 'Dewi Lestari',
'cover' => 'images/covers/pancasila.jpg', 'cover' => 'images/covers/ayah.png',
'kode_buku'=> '370', // 370 (Pendidikan, bagian dari 300-Sosial) 'kode_buku' => '844',
'kategori' => 'Pendidikan', 'kategori' => 'Fiksi',
'tahun' => 2023, 'tahun' => 2012,
'status' => 'Tersedia', 'status' => 'Dipinjam',
'is_new' => false, 'is_new' => false,
'tipe_akses' => 'offline', 'tipe_akses' => 'offline',
'sisa_hari' => 3, 'sisa_hari' => 3,
'progress' => 100, 'user_id' => 1,
'user_id' => [3, 1],
], ],
// --- BUKU TERSEDIA (UNTUK CEK PEMINJAMAN BARU) ---
[ [
'id' => 3, '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', 'judul' => 'Si Anak Pintar',
'penulis' => 'Tere Liye', 'penulis' => 'Tere Liye',
'cover' => 'images/covers/sianakpintar.jpg', 'cover' => 'images/covers/sianakpintar.jpg',
'kode_buku' => '843', // 800 (Fiksi/Sastra) 'kode_buku' => '843',
'kategori' => 'Fiksi', 'kategori' => 'Fiksi',
'tahun' => 2018, 'tahun' => 2018,
'status' => 'Dipinjam', 'status' => 'Tersedia',
'is_new' => true, 'is_new' => true,
'tipe_akses' => 'offline', 'tipe_akses' => 'offline',
'sisa_hari' => 5, 'sisa_hari' => 0,
'user_id' => 1, '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, 'id' => 6,
'judul' => 'Matematika Dasar', 'judul' => 'Atomic Habits',
'penulis' => 'Prof. Dr. Matematikus', 'penulis' => 'James Clear',
'cover' => 'images/covers/mtk.jpg', 'cover' => 'https://images-na.ssl-images-amazon.com/images/I/91bYsX41DVL.jpg',
'kode_buku' => '374', // 370 (Pendidikan) 'kode_buku' => 'SELF-003',
'kategori' => 'Pendidikan', 'kategori' => 'Refleksi Diri',
'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, 'tahun' => 2018,
'status' => 'Tersedia', 'status' => 'Tersedia',
'is_new' => true, 'is_new' => true,
'tipe_akses' => 'online', 'tipe_akses' => ['offline', 'online'],
'file_pdf' => 'hijrah.pdf', 'sisa_hari' => 0,
'progress' => 0, 'user_id' => null,
'user_id' => [2, 3], ],
] [
'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 * Data untuk buku pinjam offline
*/ */
public static function getBukuPinjamOffline($user): array public static function getBukuPinjamOffline($user): array
{ {
return self::getAllBooks() 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) { ->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'])) { if (is_array($buku['user_id'])) {
return in_array($user->id, $buku['user_id']); return in_array($user->id, $buku['user_id']);
@ -669,7 +561,8 @@ public static function getBacaBukuOnline($user): array
return self::getAllBooks() return self::getAllBooks()
->where('tipe_akses', 'online') ->where('tipe_akses', 'online')
->filter(function ($buku) use ($user) { ->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'])) { if (is_array($buku['user_id'])) {
return in_array($user->id, $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. * Data untuk riwayat peminjaman offline.
* Setiap item mewakili satu transaksi peminjaman. * Setiap item mewakili satu transaksi peminjaman.
*/ */
public static function getRiwayatOffline(): array public static function getRiwayatOffline($user): array
{ {
$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 [ return [
[ 'id' => $index + 1,
'id' => 1, 'id_peminjaman' => 'PIN-' . date('Ym') . '-' . sprintf('%03d', $buku['id']),
'id_peminjaman' => 'PIN-20240520-001', 'kode_buku' => $buku['kode_buku'],
'kode_buku' => '510', 'judul_utama' => $buku['judul'],
'judul_utama' => 'Modul Ajar IPAS', 'tanggal_pinjam' => $tglPinjam->format('d/m/Y'),
'tanggal_pinjam' => '20/05/2024', 'tanggal_kembali' => $tglKembali->format('d/m/Y'),
'tanggal_kembali' => '27/05/2024', 'status' => $buku['status'],
'status' => 'Dikembalikan',
'books' => [ 'books' => [
[ [
'id' => 1, 'id' => $buku['id'],
'judul' => 'Modul Ajar IPAS', 'judul' => $buku['judul'],
'kode_buku' => '510', 'kode_buku' => $buku['kode_buku'],
'cover' => 'images/covers/ipas.jpg', 'cover' => $buku['cover'],
'deskripsi' => 'Buku ini berisi ajakan kepada anak-anak untuk semangat pergi ke sekolah dan menuntut ilmu.', 'deskripsi' => 'Deskripsi buku ' . $buku['judul'],
'kategori' => 'Pendidikan', 'kategori' => $buku['kategori'],
'tahun' => 2022, 'tahun' => $buku['tahun'],
'keterangan' => 'Buku dikembalikan dalam kondisi baik. Terdapat denda keterlambatan 2 hari: Rp 2.000,-' '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 public static function getNotifikasiForUser($user): array
{ {
$notifikasi = []; $notifikasi = [];
$allBooks = self::getAllBooks();
$bukuPinjaman = self::getBukuPinjamOffline($user); $myBooks = $allBooks->filter(function ($buku) use ($user) {
if (!isset($buku['user_id']) || $buku['user_id'] === null) {
if (!empty($bukuPinjaman)) { return false;
$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,
];
} }
if (is_array($buku['user_id'])) {
return in_array($user->id, $buku['user_id']);
}
return $buku['user_id'] == $user->id;
});
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[] = [ $notifikasi[] = [
'icon' => 'bi-exclamation-triangle', 'icon' => 'bi-exclamation-octagon-fill',
'color' => 'danger', 'color' => 'danger',
'title' => 'Buku "' . $buku['judul'] . '" akan jatuh tempo!', 'title' => 'TERLAMBAT: ' . $buku['judul'],
'time' => '1 jam yang lalu', 'content' => "Telat {$hariTelat} hari. Denda: Rp " . number_format($denda, 0, ',', '.'),
'time' => 'Sekarang',
'read' => false, 'read' => false,
'type' => 'riwayat_peminjaman', 'type' => 'denda_active',
'link_id' => null, 'link_id' => null,
]; ];
} }
} // LOGIC JATUH TEMPO
elseif ($buku['sisa_hari'] >= 0 && $buku['sisa_hari'] <= 3 && $buku['status'] == 'Dipinjam') {
if ($user->role === 'guru') {
$notifikasi[] = [ $notifikasi[] = [
'icon' => 'bi-lightbulb-fill', 'icon' => 'bi-exclamation-triangle-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', 'color' => 'warning',
'title' => 'Perpustakaan akan tutup lebih awal pada hari Jumat.', 'title' => 'Jatuh Tempo: ' . $buku['judul'],
'time' => '1 hari yang lalu', 'content' => "Sisa waktu tinggal " . $buku['sisa_hari'] . " hari lagi.",
'read' => true, 'time' => 'Segera',
'type' => 'halaman_profil', 'read' => false,
'type' => 'warning_jatuh_tempo',
'link_id' => null, 'link_id' => null,
], ];
[ }
'icon' => 'bi-calendar-event', }
// Notif Default
$notifikasi[] = [
'icon' => 'bi-info-circle-fill',
'color' => 'primary', 'color' => 'primary',
'title' => 'Acara "Bedah Buku" akan diadakan minggu depan.', 'title' => 'Selamat Datang di DigiPus!',
'time' => '2 hari yang lalu', 'content' => 'Silakan jelajahi koleksi buku terbaru kami.',
'time' => 'Baru saja',
'read' => true, 'read' => true,
'type' => 'halaman_profil', 'type' => 'info',
'link_id' => null, '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", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
@ -2590,7 +2589,6 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^5.0.2", "immutable": "^5.0.2",
@ -2677,8 +2675,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
"integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.3.0", "version": "2.3.0",
@ -2735,7 +2732,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -2779,7 +2775,6 @@
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@ -2884,7 +2879,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },

View File

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

@ -25,7 +25,11 @@
@foreach ($groupedUsers as $role => $users) @foreach ($groupedUsers as $role => $users)
<optgroup label="{{ ucfirst($role) }}"> <optgroup label="{{ ucfirst($role) }}">
@foreach ($users as $user) @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 @endforeach
</optgroup> </optgroup>
@endforeach @endforeach
@ -40,8 +44,8 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label for="tanggal_kembali" class="form-label">Tenggat Kembali</label> <label for="tanggal_kembali" class="form-label">Tenggat Kembali</label>
<input type="text" class="form-control" id="tanggal_kembali" <input type="text" class="form-control" id="tanggal_kembali" name="tanggal_kembali"
name="tanggal_kembali" placeholder="Pilih tenggat kembali"> placeholder="Pilih tenggat kembali">
</div> </div>
</div> </div>
@ -96,19 +100,16 @@
<div class="d-flex align-items-center book-item-list" <div class="d-flex align-items-center book-item-list"
onclick="toggleBookSelection(this, {{ $buku['id'] }})" onclick="toggleBookSelection(this, {{ $buku['id'] }})"
style="cursor: pointer;"> style="cursor: pointer;">
<img src="{{ asset($buku['cover']) }}" alt="Cover" <img src="{{ asset($buku['cover']) }}" alt="Cover" class="rounded me-3"
class="rounded me-3"
style="width: 50px; height: 70px; object-fit: cover;"> style="width: 50px; height: 70px; object-fit: cover;">
<div class="flex-grow-1"> <div class="flex-grow-1">
<h6 class="fw-bold mb-1 line-clamp-2">{{ $buku['judul'] }}</h6> <h6 class="fw-bold mb-1 line-clamp-2">{{ $buku['judul'] }}</h6>
<p class="text-muted small mb-1">{{ $buku['penulis'] }}</p> <p class="text-muted small mb-1">{{ $buku['penulis'] }}</p>
<span <span class="badge bg-info-soft">{{ $buku['kategori'] }}</span>
class="badge bg-info-soft">{{ $buku['kategori'] }}</span>
</div> </div>
<div class="form-check ms-3"> <div class="form-check ms-3">
<input class="form-check-input book-checkbox" type="checkbox" <input class="form-check-input book-checkbox" type="checkbox"
value="{{ $buku['id'] }}" value="{{ $buku['id'] }}" id="book-check-{{ $buku['id'] }}"
id="book-check-{{ $buku['id'] }}"
style="pointer-events: none;"> style="pointer-events: none;">
</div> </div>
</div> </div>
@ -125,9 +126,6 @@ class="badge bg-info-soft">{{ $buku['kategori'] }}</span>
@push('scripts') @push('scripts')
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
// Inisialisasi Library & Simpan Instansinya
const tomSelect = new TomSelect("#peminjam_id", { const tomSelect = new TomSelect("#peminjam_id", {
create: false, create: false,
sortField: { sortField: {
@ -160,8 +158,7 @@ class="badge bg-info-soft">{{ $buku['kategori'] }}</span>
// Logika Pemilihan Buku // Logika Pemilihan Buku
const MAX_BOOKS = 2;
const MAX_BOOKS = 3;
let selectedBookIds = new Set(); let selectedBookIds = new Set();
const allBooks = new Map(); const allBooks = new Map();
document.querySelectorAll('.book-option').forEach(el => { document.querySelectorAll('.book-option').forEach(el => {
@ -257,7 +254,8 @@ function renderSelectedBooks() {
const stringId = String(id); const stringId = String(id);
selectedBookIds.delete(stringId); 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) { if (itemElement) {
itemElement.querySelector('.book-checkbox').checked = false; itemElement.querySelector('.book-checkbox').checked = false;
itemElement.style.background = 'transparent'; itemElement.style.background = 'transparent';
@ -266,8 +264,6 @@ function renderSelectedBooks() {
renderSelectedBooks(); renderSelectedBooks();
} }
// Logika Reset Saat Reload Halaman
const form = document.getElementById('formPeminjaman'); const form = document.getElementById('formPeminjaman');
form.reset(); form.reset();

View File

@ -14,7 +14,8 @@
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <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"> <thead class="table-light">
<tr> <tr>
<th>NO</th> <th>NO</th>
@ -32,6 +33,7 @@
$now = \Carbon\Carbon::now(); $now = \Carbon\Carbon::now();
$counter = 1; $counter = 1;
@endphp @endphp
@forelse ($peminjamanAktif as $transaksi) @forelse ($peminjamanAktif as $transaksi)
@php @php
$tenggat = $transaksi['tenggat_kembali']; $tenggat = $transaksi['tenggat_kembali'];
@ -39,177 +41,178 @@
$isHariIni = $now->startOfDay()->isSameDay($tenggat->startOfDay()); $isHariIni = $now->startOfDay()->isSameDay($tenggat->startOfDay());
$selisihHari = $now->startOfDay()->diffInDays($tenggat->startOfDay(), false); $selisihHari = $now->startOfDay()->diffInDays($tenggat->startOfDay(), false);
$statusKeterlambatan = '';
$dendaKeterlambatan = 0;
$statusClass = '';
if ($isTerlambat) { 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'; $statusClass = 'badge rounded-pill bg-danger-subtle text-danger-emphasis';
$statusText =
'Terlambat ' . $tenggat->startOfDay()->diffInDays($now->startOfDay()) . ' hari';
} elseif ($isHariIni) { } elseif ($isHariIni) {
$statusKeterlambatan = 'Hari ini';
$statusClass = 'badge rounded-pill bg-warning-subtle text-warning-emphasis'; $statusClass = 'badge rounded-pill bg-warning-subtle text-warning-emphasis';
$statusText = 'Jatuh Tempo Hari Ini';
} else { } else {
$sisaHari = abs($selisihHari);
$statusKeterlambatan = "Sisa $sisaHari hari";
$statusClass = 'badge rounded-pill bg-success-subtle text-success-emphasis'; $statusClass = 'badge rounded-pill bg-success-subtle text-success-emphasis';
$statusText = 'Sisa ' . abs($selisihHari) . ' hari';
} }
@endphp @endphp
@foreach ($transaksi['books'] as $index => $buku)
<tr> <tr>
<td>{{ $counter++ }}</td> <td>{{ $counter++ }}</td>
<td> <td>
<span <span
class="badge bg-primary-light text-primary fw-semibold">{{ $transaksi['id_peminjaman'] }}</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>
<td>{{ $transaksi['peminjam'] }}</td>
<td>{{ $buku['judul'] }}</td>
<td>{{ $transaksi['tanggal_pinjam']->format('d/m/Y') }}</td> <td>{{ $transaksi['tanggal_pinjam']->format('d/m/Y') }}</td>
<td>{{ $transaksi['tenggat_kembali']->format('d/m/Y') }}</td> <td>{{ $transaksi['tenggat_kembali']->format('d/m/Y') }}</td>
<td> <td>
<span class="badge {{ $statusClass }}">{{ $statusKeterlambatan }}</span> <span class="{{ $statusClass }}">{{ $statusText }}</span>
</td> </td>
<td> <td>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" <button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal"
data-bs-target="#modalPengembalian-{{ $transaksi['id_peminjaman'] }}-{{ $buku['id'] }}"> data-bs-target="#modalPengembalian-{{ $transaksi['id_peminjaman'] }}">
Proses Proses Pengembalian
</button> </button>
</td> </td>
</tr> </tr>
<div class="modal fade" {{-- MODAL PENGEMBALIAN --}}
id="modalPengembalian-{{ $transaksi['id_peminjaman'] }}-{{ $buku['id'] }}" <div class="modal fade" id="modalPengembalian-{{ $transaksi['id_peminjaman'] }}"
tabindex="-1" aria-labelledby="modalLabel-{{ $buku['id'] }}" aria-hidden="true"> tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered"> <div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-3"> <div class="modal-content border-0 shadow-lg rounded-3">
<div class="modal-header p-4"> <div class="modal-header p-4">
<h1 class="modal-title fs-5" id="modalLabel-{{ $buku['id'] }}"> <h5 class="modal-title fw-bold">Proses Pengembalian
Proses Pengembalian Buku</h1> ({{ count($transaksi['books']) }} Buku)
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" <button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
<div class="modal-body p-4"> <div class="modal-body p-4">
<div class="row g-4"> <div class="alert alert-light border mb-4">
<div class="col-md-6"> <div class="d-flex justify-content-between">
<h6 class="fw-bold">Informasi Peminjaman</h6> <span>Peminjam:
<div class="mb-3"> <strong>{{ $transaksi['peminjam'] }}</strong></span>
<label class="form-label small text-muted">Nama <span>ID: <strong>{{ $transaksi['id_peminjaman'] }}</strong></span>
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> </div>
<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"> <div class="col-md-6">
<h6 class="fw-bold">2. Status Keterlambatan</h6> <label class="form-label small text-muted mb-1">Kondisi
<div class="mb-3"> Buku</label>
<label class="form-label small text-muted">Status</label> <div class="d-flex gap-3">
<input type="text" class="form-control" <div class="form-check">
value="{{ $statusKeterlambatan }}" readonly> <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>
<div class="mb-3"> <div class="form-check">
<label class="form-label small text-muted">Denda <input class="form-check-input radio-kondisi"
Keterlambatan</label> type="radio"
<input type="text" name="kondisi_{{ $buku['id'] }}_{{ $transaksi['id_peminjaman'] }}"
class="form-control denda-keterlambatan-display" value="rusak">
value="Rp {{ number_format($dendaKeterlambatan, 0, ',', '.') }}" <label
readonly class="form-check-label small">Rusak/Hilang</label>
data-denda-keterlambatan="{{ $dendaKeterlambatan }}">
</div> </div>
<hr> </div>
<h6 class="fw-bold">3. Rangkuman & Aksi</h6> </div>
<div class="mb-3"> <div class="col-md-6 denda-rusak-input-wrapper d-none">
<label for="catatan_petugas_{{ $buku['id'] }}" <label class="form-label small text-muted mb-1">Denda
class="form-label small text-muted">Catatan Petugas Kerusakan</label>
(Opsional) <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> </label>
<textarea class="form-control" id="catatan_petugas_{{ $buku['id'] }}" rows="2"></textarea> <textarea class="form-control form-control-sm" rows="2"
</div> placeholder="Contoh: Ada coretan di halaman 10, Cover sedikit terlipat, dll..."></textarea>
<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>
</div>
</div>
</div>
</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> </div>
</div> </div>
</div> </div>
</div> </div>
@endforeach @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="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>
</div>
@empty @empty
<tr> <tr>
<td colspan="8" class="text-center py-5"> <td colspan="8" class="text-center py-5">Tidak ada data.</td>
<div class="empty-state">
<i class="bi bi-journal-x"></i>
<p>Tidak ada data peminjaman yang sedang aktif.</p>
</div>
</td>
</tr> </tr>
@endforelse @endforelse
</tbody> </tbody>
@ -220,18 +223,15 @@ class="form-label small text-muted">Catatan Petugas
@push('scripts') @push('scripts')
<script> <script>
// Inisialisasi DataTables
$(document).ready(function() { $(document).ready(function() {
$('#peminjamanTable').DataTable({ $('#peminjamanTable').DataTable({
pageLength: 10, pageLength: 10,
order: [ order: [
[0, 'asc'] [0, 'asc']
], ],
}); });
}); });
// Logika Modal
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
function formatRupiah(angka) { function formatRupiah(angka) {
return new Intl.NumberFormat('id-ID', { return new Intl.NumberFormat('id-ID', {
@ -243,21 +243,33 @@ function formatRupiah(angka) {
function hitungTotalDenda(modal) { function hitungTotalDenda(modal) {
const dendaKeterlambatanEl = modal.querySelector('.denda-keterlambatan-display'); 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 totalDendaEl = modal.querySelector('.total-denda-display');
const dendaKeterlambatan = parseInt(dendaKeterlambatanEl.dataset.dendaKeterlambatan) || 0;
const dendaRusak = parseInt(dendaRusakEl.value) || 0; let dendaKeterlambatan = parseInt(dendaKeterlambatanEl.dataset.dendaKeterlambatan) || 0;
const totalDenda = dendaKeterlambatan + dendaRusak; 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); totalDendaEl.textContent = formatRupiah(totalDenda);
} }
// Logic Radio Button Kondisi Buku
const radioKondisi = document.querySelectorAll('.radio-kondisi'); const radioKondisi = document.querySelectorAll('.radio-kondisi');
radioKondisi.forEach(radio => { radioKondisi.forEach(radio => {
radio.addEventListener('change', function() { radio.addEventListener('change', function() {
const modal = this.closest('.modal-content'); const cardBody = this.closest('.card-body');
const dendaRusakWrapper = modal.querySelector('.denda-rusak-input-wrapper'); const dendaRusakWrapper = cardBody.querySelector('.denda-rusak-input-wrapper');
const dendaRusakInput = modal.querySelector('.denda-rusak-input'); const dendaRusakInput = cardBody.querySelector('.denda-rusak-input');
if (this.value === 'rusak' || this.value === 'hilang') {
const modal = this.closest('.modal');
if (this.value === 'rusak') {
dendaRusakWrapper.classList.remove('d-none'); dendaRusakWrapper.classList.remove('d-none');
} else { } else {
dendaRusakWrapper.classList.add('d-none'); dendaRusakWrapper.classList.add('d-none');
@ -267,15 +279,71 @@ function hitungTotalDenda(modal) {
}); });
}); });
// Logic Input Denda Rusak
const dendaRusakInputs = document.querySelectorAll('.denda-rusak-input'); const dendaRusakInputs = document.querySelectorAll('.denda-rusak-input');
dendaRusakInputs.forEach(input => { dendaRusakInputs.forEach(input => {
input.addEventListener('input', function() { input.addEventListener('input', function() {
const modal = this.closest('.modal-content'); const modal = this.closest('.modal');
hitungTotalDenda(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> </script>
@endpush @endpush
</x-app-layout> </x-app-layout>

View File

@ -6,6 +6,28 @@
Pinjam Buku Baru Pinjam Buku Baru
</button> --}} </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="card border-0 shadow-sm rounded-4 my-4 p-4 position-relative" style="background-color: #CEDEFF;">
<div class="row"> <div class="row">
<div class="col-12 col-md-8 p-4 p-md-5" style="z-index: 2;"> <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) @forelse($bukuPinjamOffline as $buku)
<div class="col-xl-4 col-md-6" x-show="expanded || {{ $loop->index }} < 3" x-transition> <div class="col-xl-4 col-md-6" x-show="expanded || {{ $loop->index }} < 3" x-transition>
<x-book-card :buku="$buku"> <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> <span class="fw-bold small">Sisa: {{ $buku['sisa_hari'] }} hari</span>
</div> </div>
</x-book-card> </x-book-card>

View File

@ -68,10 +68,29 @@ class="badge fw-normal {{ $buku['status'] == 'Tersedia' ? 'bg-success-subtle tex
</div> </div>
<div class="d-flex flex-column gap-2 mt-auto pt-2 border-top"> <div class="d-flex flex-column gap-2 mt-auto pt-2 border-top">
@if ($mode === 'offline') @php
@if ($buku['status'] == 'Dipinjam') $userBanned = $isBanned ?? false;
<button class="btn btn-sm btn-secondary w-100" disabled> $isOfflineAccess =
<i class="bi bi-x-circle me-1"></i> Tidak Tersedia $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> </button>
@else @else
<a href="{{ route('peminjaman.ringkasan', $buku['id']) }}" <a href="{{ route('peminjaman.ringkasan', $buku['id']) }}"
@ -79,38 +98,15 @@ 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> </a>
@endif @endif
@elseif($mode === 'online') @endif
<a href="{{ route('baca.ringkasan', $buku['id']) }}"
class="btn btn-sm btn-primary w-100"> {{-- TOMBOL BACA (ONLINE) - Selalu Nyala --}}
<i class="bi bi-search me-1"></i> Baca @if ($isOnlineAccess && $mode !== 'offline')
</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']) }}" <a href="{{ route('baca.ringkasan', $buku['id']) }}"
class="btn btn-sm btn-primary w-100"> class="btn btn-sm btn-primary w-100">
<i class="bi bi-book-half me-1"></i> Baca <i class="bi bi-book-half me-1"></i> Baca
</a> </a>
@endif @endif
@endif
@endif
</div> </div>
</div> </div>
</div> </div>

View File

@ -32,6 +32,13 @@
<div class="main-wrapper flex-grow-1"> <div class="main-wrapper flex-grow-1">
@include('layouts.navigation') @include('layouts.navigation')
<main class="container-fluid py-4 px-4"> <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 }} {{ $slot }}
</main> </main>
<footer class="footer text-center py-3"> <footer class="footer text-center py-3">
@ -60,6 +67,8 @@
if (isset($item['type'])) { if (isset($item['type'])) {
if ($item['type'] === 'riwayat_peminjaman') { if ($item['type'] === 'riwayat_peminjaman') {
$url = route('riwayat.offline'); $url = route('riwayat.offline');
} elseif ($item['type'] === 'denda_active') {
$url = route('riwayat.offline');
} elseif ($item['type'] === 'rekomendasi' && isset($item['link_id'])) { } elseif ($item['type'] === 'rekomendasi' && isset($item['link_id'])) {
$url = route('rekomendasi.show', $item['link_id']); $url = route('rekomendasi.show', $item['link_id']);
} elseif ($item['type'] === 'katalog_kategori' && isset($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 --> <!-- 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/tom-select/dist/js/tom-select.complete.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const sidebar = document.getElementById('sidebar'); 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> </script>
@stack('scripts') @stack('scripts')

View File

@ -44,6 +44,8 @@ class="badge bg-primary-subtle text-primary-emphasis rounded-pill">{{ $unreadNot
if (isset($item['type'])) { if (isset($item['type'])) {
if ($item['type'] === 'riwayat_peminjaman') { if ($item['type'] === 'riwayat_peminjaman') {
$url = route('riwayat.offline'); $url = route('riwayat.offline');
} elseif ($item['type'] === 'denda_active') {
$url = route('riwayat.offline');
} elseif ($item['type'] === 'rekomendasi' && isset($item['link_id'])) { } elseif ($item['type'] === 'rekomendasi' && isset($item['link_id'])) {
$url = route('rekomendasi.show', $item['link_id']); $url = route('rekomendasi.show', $item['link_id']);
} elseif ($item['type'] === 'katalog_kategori' && isset($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="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> class="bi bi-arrow-left-right"></i><span class="nav-text ms-2">Manajemen Pinjaman</span> </a>
</li> </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') }}" <li class="nav-item"><a href="{{ route('admin.pengumuman.index') }}"
class="nav-link {{ request()->routeIs('admin.pengumuman.*') ? 'active' : '' }}"><i class="nav-link {{ request()->routeIs('admin.pengumuman.*') ? 'active' : '' }}"><i
class="bi bi-megaphone-fill"></i><span class="nav-text ms-2">Pengumuman</span></a> 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 border-0 shadow-sm">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center"> <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> <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>
<div class="card-body p-4"> <div class="card-body p-4">
<div class="row g-3"> <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') }}"> value="{{ \Carbon\Carbon::now()->addDays(7)->format('d F Y') }}">
</div> </div>
</div> </div>
</div> </div>
{{-- Peraturan Peminjaman --}} {{-- Peraturan Peminjaman --}}
@ -105,7 +104,7 @@ class="rounded me-3 form-book-cover">
<div class="alert alert-info border-0 bg-info-subtle"> <div class="alert alert-info border-0 bg-info-subtle">
<ol class="mb-0 ps-3"> <ol class="mb-0 ps-3">
<li>Perpustakaan buka pada jam operasional sekolah.</li> <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>Buku yang dipinjam harus dikembalikan dalam jangka waktu yang telah ditentukan.
</li> </li>
<li>Jika buku tidak dikembalikan tepat waktu, akan dikenakan denda atau sanksi <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="modal-body">
<div class="alert alert-info border-0 bg-info-subtle mb-4 d-flex align-items-center"> <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> <i class="bi bi-info-circle-fill me-2"></i>
<span>Anda dapat memilih maksimal <strong><span id="sisaSlot">2</span> buku</strong> lagi. <span>Anda dapat memilih maksimal <strong><span id="sisaSlot">1</span> buku</strong> lagi.
Total maksimal 3 buku.</span> Total maksimal 2 buku.</span>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@ -206,12 +205,14 @@ class="bi bi-star-fill me-1"></i>Buku Utama</span>
@endforeach @endforeach
</div> </div>
</div>
<!-- Counter Buku Terpilih --> <!-- Counter Buku Terpilih -->
<div class="mt-4 p-3 bg-light rounded shadow-sm"> <div class="p-3 bg-light">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<span class="fw-semibold"> <span class="fw-semibold">
<i class="bi bi-collection me-2 text-primary"></i>Buku Terpilih: <i class="bi bi-collection me-2 text-primary"></i>Buku Terpilih:
<span id="counterBuku" class="text-dark">1</span>/3 <span id="counterBuku" class="text-dark">1</span>/2
</span> </span>
<ul> <ul>
<li id="selectedBooks" class="text-muted small"></li> <li id="selectedBooks" class="text-muted small"></li>
@ -219,8 +220,6 @@ class="bi bi-star-fill me-1"></i>Buku Utama</span>
</div> </div>
</div> </div>
</div>
<!-- Footer --> <!-- Footer -->
<div class="modal-footer bg-light"> <div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal"> <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
@ -252,6 +251,23 @@ class="bi bi-star-fill me-1"></i>Buku Utama</span>
<div id="ringkasanBuku" class="mt-2"></div> <div id="ringkasanBuku" class="mt-2"></div>
</div> </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"> <div class="modal-footer border-0 justify-content-center">
<button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Batal</button> <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> </div>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { 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", dateFormat: "d F Y",
altInput: true, altInput: true,
altFormat: "d F Y", altFormat: "d F Y",
defaultDate: "{{ \Carbon\Carbon::now()->format('Y-m-d') }}", defaultDate: new Date().fp_incr(2),
locale: "id", locale: "id",
minDate: "today", minDate: new Date().fp_incr(1),
onChange: function(selectedDates, dateStr) { maxDate: new Date().fp_incr(2)
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));
}
}); });
const tglKembali = flatpickr("#tanggalKembali", { // Inisialisasi Tanggal Pinjam
flatpickr("#tanggalPinjam", {
dateFormat: "d F Y", dateFormat: "d F Y",
altInput: true, altInput: true,
altFormat: "d F Y", altFormat: "d F Y",
defaultDate: "{{ \Carbon\Carbon::now()->addDays(7)->format('Y-m-d') }}", defaultDate: "today",
locale: "id", 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> </script>

View File

@ -21,8 +21,33 @@
</thead> </thead>
<tbody> <tbody>
@php $counter = 1; @endphp @php $counter = 1; @endphp
@forelse ($riwayatOffline as $transaksi) @if (count($riwayatOffline) > 0)
@foreach ($riwayatOffline as $transaksi)
@foreach ($transaksi['books'] as $buku) @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> <tr>
<td>{{ $counter++ }}</td> <td>{{ $counter++ }}</td>
<td>{{ $transaksi['id_peminjaman'] }}</td> <td>{{ $transaksi['id_peminjaman'] }}</td>
@ -33,46 +58,44 @@
<td> <td>
@if ($transaksi['status'] == 'Dikembalikan') @if ($transaksi['status'] == 'Dikembalikan')
<span <span
class="badge rounded-pill bg-success-subtle text-success-emphasis">{{ $transaksi['status'] }}</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 @else
<span <span
class="badge rounded-pill bg-warning-subtle text-warning-emphasis">{{ $transaksi['status'] }}</span> class="badge rounded-pill bg-warning-subtle text-warning-emphasis">Dipinjam</span>
@endif @endif
</td> </td>
<td> <td>
<button class="btn btn-primary btn-sm text-white" data-bs-toggle="modal" <button class="btn btn-primary btn-sm text-white" data-bs-toggle="modal"
data-bs-target="#detailModal" data-transaksi-id="{{ $transaksi['id'] }}" data-bs-target="#detailModal" data-transaksi-id="{{ $transaksi['id'] }}"
data-buku-id="{{ $buku['id'] }}"> data-buku-id="{{ $buku['id'] }}"
data-is-telat="{{ $isTelat ? 'true' : 'false' }}"
data-hari-telat="{{ $hariTelat }}"
data-denda="{{ number_format($denda, 0, ',', '.') }}">
Detail Detail
</button> </button>
</td> </td>
</tr> </tr>
@endforeach @endforeach
@empty @endforeach
<tr> @endif
<td colspan="7" class="text-center">Tidak ada riwayat peminjaman.</td> </tr>
@endforelse
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
{{-- MODAL DETAIL RIWAYAT --}} {{-- MODAL DETAIL --}}
<div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true"> <div class="modal fade" id="detailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable"> <div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow-lg"> <div class="modal-content border-0 shadow-lg">
<div class="modal-header bg-light"> <div class="modal-header bg-light">
<h5 class="modal-title fw-bold" id="detailModalLabel"><i <h5 class="modal-title fw-bold"><i class="bi bi-book-half me-2 text-primary"></i>Detail Riwayat</h5>
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> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body p-4"> <div class="modal-body p-4">
<div id="modal-content-placeholder" class="text-center"> <div id="modal-content-placeholder" class="text-center">Loading...</div>
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -90,9 +113,20 @@ class="bi bi-book-half me-2 text-primary"></i>Detail Riwayat</h5>
order: [ order: [
[0, 'asc'] [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: [{ columnDefs: [{
responsivePriority: 1, responsivePriority: 1,
targets: 2 targets: 3
}, },
{ {
responsivePriority: 2, responsivePriority: 2,
@ -106,74 +140,106 @@ class="bi bi-book-half me-2 text-primary"></i>Detail Riwayat</h5>
}); });
}); });
// Modal Logic
// Modal Detail
const detailModal = document.getElementById('detailModal'); const detailModal = document.getElementById('detailModal');
detailModal.addEventListener('show.bs.modal', event => { detailModal.addEventListener('show.bs.modal', event => {
const button = event.relatedTarget; const button = event.relatedTarget;
const transaksiId = button.getAttribute('data-transaksi-id'); const transaksiId = button.getAttribute('data-transaksi-id');
const bukuId = button.getAttribute('data-buku-id'); const bukuId = button.getAttribute('data-buku-id');
const modalBody = detailModal.querySelector('#modal-content-placeholder'); const isTelat = button.getAttribute('data-is-telat') === 'true';
const modalTitle = detailModal.querySelector('#detailModalLabel'); 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); const transaksiItem = riwayatOfflineData.find(item => item.id == transaksiId);
if (!transaksiItem) return; if (!transaksiItem) return;
const buku = transaksiItem.books.find(b => b.id == bukuId); const buku = transaksiItem.books.find(b => b.id == bukuId);
if (!buku) return; if (!buku) return;
modalTitle.textContent = `Detail Buku`; const dendaHtml = isTelat ? `
<div class="card border-0 border-start border-4 border-danger bg-danger-subtle mb-3 shadow-sm">
const keteranganHtml = buku.keterangan ? <div class="card-body p-3 d-flex align-items-center">
` <div class="flex-shrink-0 me-3">
<hr class="my-3"> <i class="bi bi-exclamation-octagon-fill text-danger fs-1"></i>
<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>
</div> </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"> <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> </div>
<h4 class="fw-bold text-center">${buku.judul}</h4> <h5 class="fw-bold text-center mb-1">${buku.judul}</h5>
<p class="text-muted text-center">${buku.deskripsi}</p> <p class="text-muted text-center small mb-4">${buku.deskripsi}</p>
<hr class="my-4"> ${dendaHtml}
${keteranganHtml}
<table class="table table-borderless table-sm"> <div class="card bg-light border-0 rounded-3 p-3">
<table class="table table-borderless table-sm mb-0">
<tbody> <tbody>
<tr> <tr>
<td class="fw-bold text-start" style="width: 35%;">ID Peminjaman</td> <td class="fw-bold text-start" style="width: 35%;">ID Peminjaman</td>
<td style="width: 5%;">:</td> <td style="width: 1%;">:</td>
<td class="text-start">${transaksiItem.id_peminjaman}</td> <td class="text-start">${transaksiItem.id_peminjaman}</td>
</tr> </tr>
<tr> <tr>
<td class="fw-bold text-start">Kode Buku</td> <td class="fw-bold text-start">Kode Buku</td>
<td>:</td> <td>:</td>
<td class="text-start">${buku.kode_buku}</td> <td class="text-start fw-medium">${buku.kode_buku}</td>
</tr> </tr>
<tr> <tr>
<td class="fw-bold text-start">Kategori Buku</td> <td class="fw-bold text-start align-middle">Kategori</td>
<td>:</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> <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>
<tr> <tr>
<td class="fw-bold text-start">Tahun Terbit</td> <td class="fw-bold text-start">Tahun Terbit</td>
<td>:</td> <td>:</td>
<td class="text-start">${buku.tahun}</td> <td class="text-start">${buku.tahun}</td>
</tr> </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> </tbody>
</table> </table>
</div>`;
${keteranganHtml} `;
modalBody.innerHTML = contentHTML;
}); });
</script> </script>
@endpush @endpush

View File

@ -100,6 +100,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::get('/denda', [AdminPeminjamanController::class, 'dendaIndex'])->name('denda.index');
Route::post('/denda/sanksi', [AdminPeminjamanController::class, 'berikanSanksi'])->name('denda.sanksi');
}); });
// --- RUTE LOGIN KHUSUS ADMIN --- // --- RUTE LOGIN KHUSUS ADMIN ---