feat: migrate dummy data to database and refactor all controllers

This commit is contained in:
cukiprit 2026-02-05 02:29:54 +07:00
parent 779ef38952
commit 1764c5f9f4
42 changed files with 1316 additions and 393 deletions

View File

@ -3,42 +3,41 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\DummyDataService;
use Illuminate\Http\Request;
use App\Models\Book;
use App\Models\Loan;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
class AdminPeminjamanController extends Controller
{
public function index(Request $request)
{
$peminjamanAktif = DummyDataService::getAdminPeminjamanAktif();
$loans = Loan::with(['user', 'book'])
->whereIn('status', ['Dipinjam', 'Terlambat'])
->get();
// 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();
$groupedLoans = $loans->groupBy('user_id');
$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;
});
$peminjamanAktif = $groupedLoans->map(function ($userLoans, $userId) {
$user = $userLoans->first()->user;
$firstLoan = $userLoans->first();
return [
'id_peminjaman' => 'PIN-ADM-' . sprintf('%03d', $userId),
'user_id' => $userId,
'peminjam' => $user->nama_lengkap ?? 'Unknown',
'nomor_hp' => $user->phone ?? '-',
'tanggal_pinjam' => $firstLoan->borrowed_at,
'tenggat_kembali' => $firstLoan->due_at,
'status' => $firstLoan->status,
'books' => $userLoans->map(fn($l) => [
'id' => $l->book->id,
'judul' => $l->book->judul,
'cover' => $l->book->cover,
])->toArray(),
];
})->values();
$daftarPeminjam = $peminjamanAktif->pluck('peminjam')->unique();
@ -51,44 +50,31 @@ public function index(Request $request)
public function create()
{
$allUsers = collect(DummyDataService::getAllSiswa());
$peminjamanAktif = DummyDataService::getAdminPeminjamanAktif();
$users = User::whereIn('role', ['siswa', 'guru'])->get();
$groupedUsers = $users->map(function ($user) {
$jumlahPinjam = Loan::where('user_id', $user->id)
->whereIn('status', ['Dipinjam', 'Terlambat'])
->count();
$groupedUsers = $allUsers
->whereIn('role', ['siswa', 'guru'])
->map(function ($user) use ($peminjamanAktif) {
$user->jumlah_pinjam = $jumlahPinjam;
$user->kena_limit = $jumlahPinjam >= 2;
$user->disabled = $user->kena_limit || $user->is_banned;
// Hitung berapa buku yang sedang dipinjam
$jumlahPinjam = $peminjamanAktif->where('peminjam', $user['nama_lengkap'])->count();
if ($user->is_banned) {
$user->status_text = "(Akun Dibekukan)";
} elseif ($user->kena_limit) {
$user->status_text = "(Limit Penuh: 2/2)";
} else {
$user->status_text = "";
}
// Cek Status
$user['jumlah_pinjam'] = $jumlahPinjam;
$user['kena_limit'] = $jumlahPinjam >= 2;
return $user;
})->groupBy('role');
// 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');
$daftarBuku = DummyDataService::getAllBooks()
->where('status', 'Tersedia')
->filter(function ($buku) {
if (is_array($buku['tipe_akses'])) {
return in_array('offline', $buku['tipe_akses']);
}
return $buku['tipe_akses'] === 'offline';
});
$daftarBuku = Book::where('status', 'Tersedia')
->whereJsonContains('tipe_akses', 'offline')
->get();
return view('admin.peminjaman.create', [
'pageTitle' => 'Buat Peminjaman Manual',
@ -97,80 +83,58 @@ public function create()
]);
}
/**
* Menampilkan halaman KHUSUS Manajemen Denda (Siswa & Guru Telat).
*/
public function dendaIndex()
{
$allData = DummyDataService::getAdminPeminjamanAktif();
$allSiswaRaw = collect(DummyDataService::getAllSiswa());
$now = \Carbon\Carbon::now();
$now = Carbon::now();
// Fetch all loans that are overdue or users that are banned
$loans = Loan::with(['user', 'book'])
->where(function($query) use ($now) {
$query->where('due_at', '<', $now)
->whereIn('status', ['Dipinjam', 'Terlambat']);
})
->orWhereHas('user', function($query) {
$query->where('is_banned', true);
})
->get();
// LOGIC AUTO-BAN
$allSiswa = $allSiswaRaw->map(function ($siswa) use ($allData, $now) {
$groupedLoans = $loans->groupBy('user_id');
// 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';
$siswaTelat = $groupedLoans->map(function ($userLoans, $userId) use ($now) {
$user = $userLoans->first()->user;
$firstLoan = $userLoans->first();
$tenggat = Carbon::parse($firstLoan->due_at);
$hariTelat = $now->greaterThan($tenggat) ? (int) $tenggat->diffInDays($now) : 0;
$totalDenda = $hariTelat * 1000;
// 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.";
$hp = $user->phone ?? '';
$waLink = '#';
if ($hp) {
if (substr($hp, 0, 1) == '0') $hp = '62' . substr($hp, 1);
$pesan = $hariTelat > 0
? "Halo {$user->nama_lengkap}, anda terlambat pengembalian buku. Total Denda: Rp " . number_format($totalDenda, 0, ',', '.')
: "Halo {$user->nama_lengkap}, akun anda sedang dinonaktifkan sementara. Mohon hubungi petugas.";
$waLink = "https://wa.me/{$hp}?text=" . urlencode($pesan);
}
$item['wa_link'] = "https://wa.me/{$hp}?text=" . urlencode($pesan);
return $item;
});
return [
'id' => $firstLoan->id,
'peminjam' => $user->nama_lengkap,
'nomor_hp' => $user->phone ?? '-',
'kelas' => $user->kelas ?? 'Guru',
'hari_terlambat' => $hariTelat,
'total_denda' => $totalDenda,
'wa_link' => $waLink,
'is_banned' => $user->is_banned,
'tenggat_kembali' => $firstLoan->due_at,
'books' => $userLoans->map(fn($l) => [
'id' => $l->book->id,
'judul' => $l->book->judul,
])->toArray(),
];
})->values();
$listKelas = $siswaTelat->pluck('kelas')->unique()->values();
@ -181,11 +145,9 @@ public function dendaIndex()
]);
}
/**
* Dummy function untuk tombol Sanksi
*/
public function berikanSanksi(Request $request)
{
// Actually implement banning logic here if needed
return response()->json(['status' => 'success']);
}
}

View File

@ -3,7 +3,7 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\DummyDataService;
use App\Models\Recommendation;
use Illuminate\Http\Request;
class AdminRekomendasiController extends Controller
@ -19,17 +19,19 @@ private function extractYouTubeId(string $url): ?string
public function index()
{
$rekomendasiMentah = DummyDataService::getRekomendasiPembelajaran();
$rekomendasiMentah = Recommendation::latest()->get();
// Menambahkan thumbnail YouTube ke setiap rekomendasi
$semuaRekomendasi = $rekomendasiMentah->map(function ($item) {
$videoId = $this->extractYouTubeId($item['youtube_link']);
if ($videoId) {
$item['thumbnail'] = "https://img.youtube.com/vi/{$videoId}/hqdefault.jpg";
} else {
$item['thumbnail'] = 'https://via.placeholder.com/150?text=No+Preview';
}
return $item;
$videoId = $this->extractYouTubeId($item->youtube_link);
return [
'id' => $item->id,
'judul' => $item->judul,
'kategori' => $item->kategori,
'youtube_link' => $item->youtube_link,
'thumbnail' => $videoId ? "https://img.youtube.com/vi/{$videoId}/hqdefault.jpg" : 'https://via.placeholder.com/150?text=No+Preview',
'deskripsi' => $item->deskripsi,
];
});
return view('admin.rekomendasi.index', [
@ -45,8 +47,11 @@ public function create()
public function edit($id)
{
$rekomendasi = DummyDataService::getRekomendasiPembelajaran()->firstWhere('id', (int)$id);
abort_if(!$rekomendasi, 404);
return view('admin.rekomendasi.edit', ['pageTitle' => 'Edit Rekomendasi', 'rekomendasi' => $rekomendasi]);
$rekomendasi = Recommendation::findOrFail($id);
return view('admin.rekomendasi.edit', [
'pageTitle' => 'Edit Rekomendasi: ' . $rekomendasi->judul,
'rekomendasi' => $rekomendasi
]);
}
}

View File

@ -3,7 +3,8 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\DummyDataService;
use App\Models\Book;
use App\Models\Category;
use Illuminate\Http\Request;
class BookController extends Controller
@ -11,12 +12,19 @@ class BookController extends Controller
public function index(Request $request)
{
$filters = $request->only(['search']);
$semuaBuku = DummyDataService::getKatalogBuku($filters);
$query = Book::with('category');
if ($request->filled('search')) {
$query->where('judul', 'like', '%' . $request->search . '%');
}
$semuaBuku = $query->latest()->get();
// Memisahkan buku menjadi dua koleksi: online dan offline
[$bukuOnline, $bukuOffline] = $semuaBuku->partition(function ($buku) {
$tipe = $buku['tipe_akses'];
return $tipe === 'online' || (is_array($tipe) && in_array('online', $tipe));
$tipe = $buku->tipe_akses;
return in_array('online', $tipe ?? []);
});
return view('admin.buku.index', [
@ -33,18 +41,21 @@ public function index(Request $request)
public function create()
{
return view('admin.buku.create', [
'pageTitle' => 'Tambah Buku Baru'
'pageTitle' => 'Tambah Buku Baru',
'categories' => Category::all()
]);
}
public function edit($id)
{
$buku = DummyDataService::getKatalogBuku()->firstWhere('id', (int)$id);
abort_if(!$buku, 404);
{
$buku = Book::with('category')->findOrFail($id);
return view('admin.buku.edit', [
'pageTitle' => 'Edit Buku: ' . $buku['judul'],
'buku' => $buku
]);
}
return view('admin.buku.edit', [
'pageTitle' => 'Edit Buku: ' . $buku->judul,
'buku' => $buku,
'categories' => Category::all()
]);
}
// You might also want to implement store, update, destroy here later
}

View File

@ -3,21 +3,78 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\DummyDataService;
use App\Models\Announcement;
use App\Models\Book;
use App\Models\Loan;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
class DashboardController extends Controller
{
public function index()
{
$allBooks = Book::count();
$allUsers = User::count();
$bukuDipinjam = Loan::whereIn('status', ['Dipinjam', 'Terlambat'])->count();
$stats = [
['label' => 'Total Buku', 'value' => $allBooks, 'icon' => 'bi-journal-bookmark-fill', 'color' => 'primary'],
['label' => 'Total Anggota', 'value' => $allUsers, 'icon' => 'bi-people-fill', 'color' => 'success'],
['label' => 'Buku Dipinjam', 'value' => $bukuDipinjam, 'icon' => 'bi-arrow-up-right-circle-fill', 'color' => 'warning'],
['label' => 'Denda Menunggu', 'value' => Loan::where('status', 'Terlambat')->count(), 'icon' => 'bi-cash-coin', 'color' => 'danger'],
];
// Monthly stats (last 7 months)
$labels = [];
$data = [];
for ($i = 6; $i >= 0; $i--) {
$month = Carbon::now()->subMonths($i);
$labels[] = $month->translatedFormat('M');
$data[] = Loan::whereMonth('borrowed_at', $month->month)
->whereYear('borrowed_at', $month->year)
->count();
}
$statistikBulanan = [
'labels' => $labels,
'data' => $data,
];
$komposisiBuku = [
'tersedia' => Book::where('status', 'Tersedia')->count(),
'dipinjam' => Book::where('status', 'Dipinjam')->count(),
];
$pengumuman = Announcement::latest()->take(5)->get();
$aktivitasTerakhir = Loan::with(['user', 'book'])
->latest()
->take(4)
->get()
->map(fn($loan) => [
'nama' => $loan->user->nama_lengkap ?? 'Unknown',
'judul_buku' => $loan->book->judul ?? 'Unknown',
'tipe' => $loan->status === 'Dikembalikan' ? 'Pengembalian' : 'Peminjaman',
'waktu' => $loan->created_at->diffForHumans(),
'status' => $loan->status,
]);
$hour = date('H');
$greeting = "Selamat Pagi";
if ($hour >= 12 && $hour < 15) $greeting = "Selamat Siang";
elseif ($hour >= 15 && $hour < 18) $greeting = "Selamat Sore";
elseif ($hour >= 18) $greeting = "Selamat Malam";
return view('admin.dashboard', [
'pageTitle' => 'Beranda',
'user' => auth()->user(),
'greeting' => 'Selamat Datang',
'stats' => DummyDataService::getAdminDashboardStats(),
'statistikBulanan' => DummyDataService::getStatistikPeminjamanAdmin(),
'komposisiBuku' => DummyDataService::getKomposisiBukuAdmin(),
'pengumuman' => DummyDataService::getPengumuman(),
'aktivitasTerakhir' => DummyDataService::getAktivitasTerakhir(),
'user' => Auth::user(),
'greeting' => $greeting,
'stats' => $stats,
'statistikBulanan' => $statistikBulanan,
'komposisiBuku' => $komposisiBuku,
'pengumuman' => $pengumuman,
'aktivitasTerakhir' => $aktivitasTerakhir,
]);
}
}

View File

@ -3,7 +3,7 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\DummyDataService;
use App\Models\Announcement;
use Illuminate\Http\Request;
class PengumumanController extends Controller
@ -13,7 +13,7 @@ class PengumumanController extends Controller
*/
public function index()
{
$semuaPengumuman = DummyDataService::getPengumuman();
$semuaPengumuman = Announcement::latest()->get();
return view('admin.pengumuman.index', [
'pageTitle' => 'Manajemen Pengumuman',
'semuaPengumuman' => $semuaPengumuman,
@ -35,13 +35,10 @@ public function create()
*/
public function edit($id)
{
$pengumuman = collect(DummyDataService::getPengumuman())->firstWhere('id', (int)$id);
// Hentikan jika pengumuman tidak ditemukan
abort_if(!$pengumuman, 404);
$pengumuman = Announcement::findOrFail($id);
return view('admin.pengumuman.edit', [
'pageTitle' => 'Edit Pengumuman',
'pageTitle' => 'Edit Pengumuman: ' . $pengumuman->title,
'pengumuman' => $pengumuman,
]);
}

View File

@ -3,13 +3,14 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Services\DummyDataService;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function index()
{
$semuaSiswa = DummyDataService::getAllSiswa();
$semuaSiswa = User::whereIn('role', ['siswa', 'guru', 'penjaga perpus'])->latest()->get();
return view('admin.pengguna.index', [
'pageTitle' => 'Manajemen Pengguna',
'semuaSiswa' => $semuaSiswa
@ -31,12 +32,13 @@ public function create()
*/
public function edit($id)
{
$pengguna = collect(DummyDataService::getAllSiswa())->firstWhere('id', (int)$id);
abort_if(!$pengguna, 404);
$pengguna = User::findOrFail($id);
return view('admin.pengguna.edit', [
'pageTitle' => 'Edit Pengguna',
'pageTitle' => 'Edit Pengguna: ' . $pengguna->nama_lengkap,
'pengguna' => $pengguna,
]);
}
// You might also want to implement store, update, destroy here later
}

View File

@ -31,52 +31,35 @@ public function create(Request $request): View
*/
public function store(Request $request): RedirectResponse
{
// Bagian Validasi Dinamis
$role = $request->input('role');
$rules = [
'name' => ['required', 'string', 'max:255'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'role' => ['required', 'in:siswa,guru'], // Sesuaikan dengan role yang diizinkan
'role' => ['required', 'in:siswa,guru'],
];
// Tambahkan validasi NISN atau NIP berdasarkan role
if ($role === 'siswa') {
$rules['nisn'] = ['required', 'string', 'max:255']; // Tambahkan 'unique:users' jika perlu
} else { // Asumsi 'guru'
$rules['nip'] = ['required', 'string', 'max:255']; // Tambahkan 'unique:users' jika perlu
$rules['nisn'] = ['required', 'string', 'max:255', 'unique:users,nomor_induk'];
} else {
$rules['nip'] = ['required', 'string', 'max:255', 'unique:users,nomor_induk'];
}
$request->validate($rules);
// Bagian Pembuatan User
$userArray = [
'id' => rand(100, 999), // ID unik sementara
'nama_lengkap' => $request->name,
$user = User::create([
'name' => $request->name,
'password' => Hash::make($request->password), // Gunakan Hash jika login Anda sudah pakai Hash
// 'password' => $request->password, // Gunakan ini jika login (LoginRequest) masih cek teks biasa
'nama_lengkap' => $request->name,
'email' => ($request->nisn ?: $request->nip) . '@smkn1perpus.sch.id',
'password' => Hash::make($request->password),
'role' => $request->role,
];
// Tambahkan field dinamis (NISN/NIP) dan buat email unik palsu
if ($role === 'siswa') {
$userArray['nisn'] = $request->nisn;
$userArray['email'] = $request->nisn . '@smkn1perpus.sch.id'; // Email unik sementara
} else {
$userArray['nip'] = $request->nip;
$userArray['email'] = $request->nip . '@smkn1perpus.sch.id'; // Email unik sementara
}
$user = new User();
$user->forceFill($userArray);
// $user->save(); // Aktifkan ini jika menggunakan database
'nomor_induk' => $request->nisn ?: $request->nip,
]);
event(new Registered($user));
Auth::login($user);
// Bagian Redirect
if ($user->role === 'penjaga perpus') {
return redirect()->route('admin.dashboard');
} else {

View File

@ -2,10 +2,13 @@
namespace App\Http\Controllers;
use App\Services\DummyDataService;
use App\Models\Book;
use App\Models\Category;
use App\Models\Loan;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
@ -15,9 +18,34 @@ class BacaOnlineController extends Controller
public function index(Request $request): View
{
$filters = $request->only(['search', 'kategori', 'tahun', 'penulis']);
$filters['tipe_akses'] = 'online';
$semuaBuku = DummyDataService::getKatalogBuku($filters);
$filterOptions = DummyDataService::getFilterOptions();
$query = Book::with('category')->whereJsonContains('tipe_akses', 'online');
if ($request->filled('search')) {
$query->where('judul', 'like', '%' . $request->search . '%');
}
if ($request->filled('kategori')) {
$query->whereHas('category', function($q) use ($request) {
$q->where('name', $request->kategori);
});
}
if ($request->filled('tahun')) {
$query->where('tahun', $request->tahun);
}
if ($request->filled('penulis')) {
$query->where('penulis', $request->penulis);
}
$semuaBuku = $query->latest()->get();
$filterOptions = [
'kategori' => Category::pluck('name')->unique()->sort()->values(),
'tahun' => Book::pluck('tahun')->unique()->sortDesc()->values(),
'penulis' => Book::pluck('penulis')->unique()->sort()->values(),
];
return view('katalog.index', [
'semuaBuku' => $semuaBuku,
@ -30,7 +58,7 @@ public function index(Request $request): View
public function ringkasan(int $id): View
{
$book = $this->getBookOrFail($id);
$book = Book::with('category')->findOrFail($id);
return view('katalog.ringkasan', [
'buku' => $book,
@ -44,13 +72,13 @@ public function ringkasan(int $id): View
public function showCodeRequestPage(int $id): View
{
$book = $this->getBookOrFail($id);
$book = Book::findOrFail($id);
$sessionKey = 'access_code_for_book_' . $id;
if (session()->has($sessionKey)) {
$accessCode = session($sessionKey);
} else {
$accessCode = 'BCO-' . date('Ymd') . '-' . $book['id'] . '-' . Str::upper(Str::random(4));
$accessCode = 'BCO-' . date('Ymd') . '-' . $book->id . '-' . Str::upper(Str::random(4));
session([$sessionKey => $accessCode]);
}
@ -69,6 +97,20 @@ public function verifyCode(Request $request, int $id): RedirectResponse
if ($request->input('kode_akses') === $correctCode) {
session(['book_verified_' . $id => true]);
// Track history
Loan::updateOrCreate(
[
'user_id' => Auth::id(),
'book_id' => $id,
'status' => 'Online',
],
[
'loan_code' => $correctCode,
'borrowed_at' => now(),
]
);
session()->forget('access_code_for_book_' . $id);
return redirect()->route('baca.view_book', ['id' => $id]);
}
@ -82,7 +124,7 @@ public function viewBook(int $id): View|RedirectResponse
return redirect()->route('baca.request_code', ['id' => $id])
->with('error', 'Silakan masukkan kode akses terlebih dahulu.');
}
$book = $this->getBookOrFail($id);
$book = Book::findOrFail($id);
return view('baca.view_book', ['book' => $book]);
}
@ -91,23 +133,16 @@ public function streamPdf(int $id): BinaryFileResponse|Response
if (!session('book_verified_' . $id, false)) {
abort(403, 'Akses Ditolak.');
}
$book = $this->getBookOrFail($id);
$filePath = 'books/' . $book['file_pdf'];
$book = Book::findOrFail($id);
$filePath = 'books/' . ($book->file_pdf ?? 'sample.pdf');
$absolutePath = storage_path('app/' . $filePath);
// For demo purposes, if file doesn't exist, we might want to use a placeholder or handle it gracefully
if (!file_exists($absolutePath)) {
abort(404, 'GAGAL - PHP tidak dapat menemukan file di: ' . $absolutePath);
// Create a dummy file for testing if it doesn't exist? (Optional, maybe just abort)
abort(404, 'File PDF tidak ditemukan di server.');
}
return response()->file($absolutePath);
}
private function getBookOrFail(int $id): array
{
$book = DummyDataService::getKatalogBuku()->firstWhere('id', $id);
if (!$book) {
abort(404, 'Buku tidak ditemukan.');
}
return $book;
}
}

View File

@ -2,7 +2,11 @@
namespace App\Http\Controllers;
use App\Services\DummyDataService;
use App\Models\Announcement;
use App\Models\Book;
use App\Models\Loan;
use App\Models\Recommendation;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -11,50 +15,122 @@ class DashboardController extends Controller
public function index()
{
$user = Auth::user();
$bukuPinjam = DummyDataService::getBukuPinjamOffline($user);
// Fetch active loans for the user
$loans = Loan::with('book')
->where('user_id', $user->id)
->whereIn('status', ['Dipinjam', 'Terlambat'])
->get();
$isTelat = collect($bukuPinjam)->contains(function ($buku) {
return $buku['sisa_hari'] < 0;
$bukuPinjamOffline = $loans->map(function ($loan) {
$dueAt = Carbon::parse($loan->due_at);
$sisaHari = (int) now()->diffInDays($dueAt, false);
return [
'id' => $loan->book->id,
'judul' => $loan->book->judul,
'penulis' => $loan->book->penulis,
'sisa_hari' => $sisaHari,
'cover' => $loan->book->cover,
];
});
// Check if user has overdue books
$isTelat = $bukuPinjamOffline->contains(fn($b) => $b['sisa_hari'] < 0);
if ($isTelat && $user->role === 'siswa') {
$user->is_banned = true;
}
$user->update(['is_banned' => true]);
}
$stats = DummyDataService::getDashboardStats();
$pengumuman = DummyDataService::getPengumuman();
$pemberitahuan = DummyDataService::getPemberitahuan();
$progressMembaca = DummyDataService::getProgressMembaca();
$statistikBulanan = DummyDataService::getStatistikBulanan();
$bukuPinjamOffline = $bukuPinjam;
$bacaBukuOnline = DummyDataService::getBacaBukuOnline($user);
$rekomendasiPembelajaran = DummyDataService::getRekomendasiPembelajaran();
$personalNotif = DummyDataService::getNotifikasiForUser($user);
// Stats calculation
$stats = [
['label' => 'Buku yang dipinjam', 'value' => $loans->count(), 'icon' => 'bi-book-half', 'color' => 'primary'],
['label' => 'Tenggat Waktu', 'value' => $bukuPinjamOffline->where('sisa_hari', '<=', 3)->where('sisa_hari', '>=', 0)->count(), 'icon' => 'bi-clock-history', 'color' => 'danger'],
['label' => 'Buku dikembalikan', 'value' => Loan::where('user_id', $user->id)->where('status', 'Dikembalikan')->count(), 'icon' => 'bi-check-circle', 'color' => 'success'],
['label' => 'History Baca', 'value' => Loan::where('user_id', $user->id)->count(), 'icon' => 'bi-hourglass-split', 'color' => 'warning'],
];
// Cek apakah ada notifikasi denda aktif
$dendaAlert = collect($personalNotif)->where('type', 'denda_active');
$pengumuman = Announcement::latest()->take(5)->get();
// Placeholder for pemberitahuan (system notifications)
$pemberitahuan = collect([
['type' => 'info', 'icon' => 'bi-bell-fill', 'title' => 'Selamat Datang', 'content' => 'Selamat datang di perpustakaan digital SMKN 1.', 'badge' => 'Baru']
]);
// Menambahkan thumbnail YouTube ke setiap rekomendasi
$rekomendasiPembelajaran = $rekomendasiPembelajaran->map(function ($item) {
$videoId = $this->extractYouTubeId($item['youtube_link']);
if ($videoId) {
$item['thumbnail'] = "https://img.youtube.com/vi/{$videoId}/hqdefault.jpg";
} else {
$item['thumbnail'] = 'https://via.placeholder.com/150?text=No+Preview';
}
return $item;
$progressMembaca = ['selesai' => 70, 'sisa' => 30]; // Still dummy as we don't track pages yet
$statistikBulanan = [
'labels' => ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul'],
'data' => [10, 15, 8, 20, 18, 25, 22],
];
// Online books (books with 'online' in tipe_akses)
$bacaBukuOnline = $loans->filter(function ($loan) {
return in_array('online', $loan->book->tipe_akses ?? []);
})->map(fn($loan) => [
'judul' => $loan->book->judul,
'penulis' => $loan->book->penulis,
'progress' => 0, // Placeholder
'cover' => $loan->book->cover,
]);
$recommendations = Recommendation::all();
$rekomendasiPembelajaran = $recommendations->map(function ($item) {
$videoId = $this->extractYouTubeId($item->youtube_link);
return [
'id' => $item->id,
'judul' => $item->judul,
'kategori' => $item->kategori,
'thumbnail' => $videoId ? "https://img.youtube.com/vi/{$videoId}/hqdefault.jpg" : 'https://via.placeholder.com/150?text=No+Preview',
'youtube_link' => $item->youtube_link,
'deskripsi' => $item->deskripsi,
];
});
$dendaAlert = collect($personalNotif)->where('type', 'denda_active');
// Dynamic notifications based on loans
$personalNotif = collect();
foreach ($bukuPinjamOffline as $buku) {
if ($buku['sisa_hari'] < 0) {
$hariTelat = abs($buku['sisa_hari']);
$denda = $hariTelat * 1000;
$personalNotif->push([
'icon' => 'bi-exclamation-octagon-fill',
'color' => 'danger',
'title' => 'TERLAMBAT: ' . $buku['judul'],
'content' => "Telat {$hariTelat} hari. Denda: Rp " . number_format($denda, 0, ',', '.'),
'time' => 'Sekarang',
'read' => false,
'type' => 'denda_active',
]);
} elseif ($buku['sisa_hari'] <= 3) {
$personalNotif->push([
'icon' => 'bi-exclamation-triangle-fill',
'color' => 'warning',
'title' => 'Jatuh Tempo: ' . $buku['judul'],
'content' => "Sisa waktu tinggal " . $buku['sisa_hari'] . " hari lagi.",
'time' => 'Segera',
'read' => false,
'type' => 'warning_jatuh_tempo',
]);
}
}
$personalNotif->push([
'icon' => 'bi-info-circle-fill',
'color' => 'primary',
'title' => 'Selamat Datang!',
'content' => 'Jelajahi koleksi buku terbaru kami.',
'time' => 'Baru saja',
'read' => true,
'type' => 'info',
]);
$dendaAlert = $personalNotif->where('type', 'denda_active');
$hour = date('H');
$greeting = "Selamat Pagi";
if ($hour >= 12 && $hour < 15)
$greeting = "Selamat Siang";
elseif ($hour >= 15 && $hour < 18)
$greeting = "Selamat Sore";
elseif ($hour >= 18)
$greeting = "Selamat Malam";
if ($hour >= 12 && $hour < 15) $greeting = "Selamat Siang";
elseif ($hour >= 15 && $hour < 18) $greeting = "Selamat Sore";
elseif ($hour >= 18) $greeting = "Selamat Malam";
return view('dashboard', compact(
'user',
@ -70,10 +146,7 @@ public function index()
'rekomendasiPembelajaran'
))->with('notifikasi', $personalNotif);
}
/**
* Helper function untuk mengekstrak ID video dari URL YouTube.
*/
private function extractYouTubeId(string $url): ?string
{
preg_match('/(v=|vi=|youtu.be\/|embed\/|\/v\/|\?v=|\&v=)(.+?)\b/i', $url, $matches);

View File

@ -3,16 +3,74 @@
namespace App\Http\Controllers\Guru;
use App\Http\Controllers\Controller;
use App\Services\DummyDataService;
use App\Models\Book;
use App\Models\Category;
use App\Models\Loan;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class LaporanController extends Controller
{
public function index()
{
$laporan = DummyDataService::getLaporanMinatBaca();
$siswaTeraktif = DummyDataService::getSiswaTeraktif();
$aktivitasMingguan = DummyDataService::getAktivitasMingguan();
// 1. Laporan Minat Baca (Buku Terpopuler & Kategori Populer)
$bukuTerpopuler = Book::withCount('loans')
->orderBy('loans_count', 'desc')
->take(3)
->get()
->map(fn($book) => [
'judul' => $book->judul,
'penulis' => $book->penulis,
'total_pembaca' => $book->loans_count,
'cover' => $book->cover,
]);
$kategoriPopuler = Category::withCount(['books as total_pembaca' => function ($query) {
$query->join('loans', 'books.id', '=', 'loans.book_id');
}])
->orderBy('total_pembaca', 'desc')
->take(4)
->get()
->map(fn($cat) => [
'nama' => $cat->name,
'total_pembaca' => $cat->total_pembaca,
'trend' => 'naik', // Placeholder
'icon' => 'bi-arrow-up-right',
]);
$laporan = [
'buku_terpopuler' => $bukuTerpopuler,
'kategori_populer' => $kategoriPopuler,
'insight' => 'Siswa menunjukkan minat baca yang dinamis. Kategori ' . ($kategoriPopuler->first()['nama'] ?? 'Populer') . ' menjadi favorit saat ini.',
];
// 2. Siswa Teraktif
$siswaTeraktif = User::where('role', 'siswa')
->withCount('loans')
->orderBy('loans_count', 'desc')
->take(10)
->get()
->map(fn($user) => [
'nama' => $user->nama_lengkap,
'total_buku' => $user->loans_count,
'kelas' => $user->kelas ?? 'N/A',
]);
// 3. Aktivitas Mingguan (7 hari terakhir)
$labels = [];
$data = [];
for ($i = 6; $i >= 0; $i--) {
$date = Carbon::now()->subDays($i);
$labels[] = $date->translatedFormat('l');
$data[] = Loan::whereDate('borrowed_at', $date->toDateString())->count();
}
$aktivitasMingguan = [
'labels' => $labels,
'data' => $data,
];
return view('guru.laporan.index', [
'pageTitle' => 'Laporan Minat Baca Siswa',

View File

@ -2,7 +2,10 @@
namespace App\Http\Controllers;
use App\Services\DummyDataService;
use App\Models\Book;
use App\Models\Category;
use App\Models\Loan;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -14,15 +17,46 @@ public function index(Request $request)
$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;
// Check for overdue loans
$hasOverdue = Loan::where('user_id', $user->id)
->whereIn('status', ['Dipinjam', 'Terlambat'])
->where('due_at', '<', now())
->exists();
$isBanned = $hasOverdue || ($user->is_banned ?? false);
}
$filters = $request->only(['search', 'kategori', 'tahun', 'penulis']);
$semuaBuku = DummyDataService::getKatalogBuku($filters);
$filterOptions = DummyDataService::getFilterOptions();
// Query books with filters
$query = Book::with('category');
if ($request->filled('search')) {
$query->where('judul', 'like', '%' . $request->search . '%');
}
if ($request->filled('kategori')) {
$query->whereHas('category', function($q) use ($request) {
$q->where('name', $request->kategori);
});
}
if ($request->filled('tahun')) {
$query->where('tahun', $request->tahun);
}
if ($request->filled('penulis')) {
$query->where('penulis', $request->penulis);
}
$semuaBuku = $query->latest()->get();
// Get filter options dynamically
$filterOptions = [
'kategori' => Category::pluck('name')->unique()->sort()->values(),
'tahun' => Book::pluck('tahun')->unique()->sortDesc()->values(),
'penulis' => Book::pluck('penulis')->unique()->sort()->values(),
];
return view('katalog.index', [
'semuaBuku' => $semuaBuku,

View File

@ -2,30 +2,60 @@
namespace App\Http\Controllers;
use App\Services\DummyDataService;
use App\Models\Book;
use App\Models\Category;
use App\Models\Loan;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class PeminjamanController extends Controller
{
public function index(Request $request)
{
$user = \Illuminate\Support\Facades\Auth::user();
$bukuPinjam = \App\Services\DummyDataService::getBukuPinjamOffline($user);
$isTelat = collect($bukuPinjam)->contains(function ($buku) {
return $buku['sisa_hari'] < 0;
});
$user = Auth::user();
$isBannedManual = $user->is_banned ?? false;
// Check for overdue loans
$hasOverdue = Loan::where('user_id', $user->id)
->whereIn('status', ['Dipinjam', 'Terlambat'])
->where('due_at', '<', now())
->exists();
if (($isTelat || $isBannedManual) && $user->role === 'siswa') {
if (($hasOverdue || $user->is_banned) && $user->role === 'siswa') {
return redirect()->route('dashboard')->with('error', 'AKSES DITOLAK: Akun Anda sedang dibekukan karena ada buku terlambat!');
}
$filters = $request->only(['search', 'kategori', 'tahun', 'penulis']);
$filters['tipe_akses'] = 'offline';
$semuaBuku = DummyDataService::getKatalogBuku($filters);
$filterOptions = DummyDataService::getFilterOptions();
$query = Book::with('category')->whereJsonContains('tipe_akses', 'offline');
if ($request->filled('search')) {
$query->where('judul', 'like', '%' . $request->search . '%');
}
if ($request->filled('kategori')) {
$query->whereHas('category', function($q) use ($request) {
$q->where('name', $request->kategori);
});
}
if ($request->filled('tahun')) {
$query->where('tahun', $request->tahun);
}
if ($request->filled('penulis')) {
$query->where('penulis', $request->penulis);
}
$semuaBuku = $query->latest()->get();
$filterOptions = [
'kategori' => Category::pluck('name')->unique()->sort()->values(),
'tahun' => Book::pluck('tahun')->unique()->sortDesc()->values(),
'penulis' => Book::pluck('penulis')->unique()->sort()->values(),
];
return view('katalog.index', [
'semuaBuku' => $semuaBuku,
@ -39,7 +69,7 @@ public function index(Request $request)
public function ringkasan($id)
{
$user = Auth::user();
$buku = DummyDataService::getKatalogBuku()->firstWhere('id', $id);
$buku = Book::with('category')->findOrFail($id);
return view('katalog.ringkasan', [
'user' => $user,
@ -52,13 +82,14 @@ public function ringkasan($id)
]);
}
public function form($id)
{
$user = Auth::user();
$buku = DummyDataService::getKatalogBuku()->firstWhere('id', $id);
$filters = ['tipe_akses' => 'offline'];
$semuaBuku = DummyDataService::getKatalogBuku($filters);
$buku = Book::with('category')->findOrFail($id);
$semuaBuku = Book::whereJsonContains('tipe_akses', 'offline')
->where('status', 'Tersedia')
->get();
return view('peminjaman.form', compact('user', 'buku', 'semuaBuku'));
}
@ -67,21 +98,31 @@ public function store(Request $request)
{
$request->validate([
'buku_ids' => 'required|array|min:1|max:3',
'buku_ids.*' => 'integer'
'buku_ids.*' => 'exists:books,id'
]);
$bukuIds = $request->input('buku_ids');
foreach ($bukuIds as $bukuId) {
// Di backend nanti kayak gini
// Peminjaman::create([
// 'user_id' => auth()->id(),
// 'book_id' => $bukuId,
// 'tanggal_pinjam' => now(),
// 'tanggal_kembali' => now()->addDays(7),
// 'status' => 'dipinjam'
// ]);
}
DB::transaction(function () use ($bukuIds) {
foreach ($bukuIds as $bukuId) {
$book = Book::lockForUpdate()->find($bukuId);
if ($book->status !== 'Tersedia') {
throw new \Exception("Buku '{$book->judul}' sudah tidak tersedia.");
}
Loan::create([
'user_id' => Auth::id(),
'book_id' => $bukuId,
'loan_code' => 'PIN-' . date('Ym') . '-' . strtoupper(Str::random(4)) . '-' . $bukuId,
'borrowed_at' => now(),
'due_at' => now()->addDays(7),
'status' => 'Dipinjam',
]);
$book->update(['status' => 'Dipinjam']);
}
});
return redirect()->route('dashboard')
->with('success', 'Berhasil meminjam ' . count($bukuIds) . ' buku!');

View File

@ -3,12 +3,15 @@
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use App\Models\Book;
use App\Models\Loan;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
use App\Services\DummyDataService;
class ProfileController extends Controller
{
@ -19,7 +22,7 @@ public function index(): RedirectResponse|View
{
$user = Auth::user();
if (!$user) {
return redirect()->route(route: 'login');
return redirect()->route('login');
}
$viewData = ['user' => $user];
@ -27,20 +30,68 @@ public function index(): RedirectResponse|View
// Menyiapkan data berdasarkan role pengguna
if ($user->role === 'penjaga perpus') {
// Data untuk Penjaga Perpus: Statistik global & aktivitas terkini
$viewData['statistik'] = DummyDataService::getAdminDashboardStats();
$viewData['aktivitasTerakhir'] = DummyDataService::getAktivitasTerakhir();
$viewData['statistik'] = [
['label' => 'Total Buku', 'value' => Book::count(), 'icon' => 'bi-journal-bookmark-fill', 'color' => 'primary'],
['label' => 'Total Anggota', 'value' => User::count(), 'icon' => 'bi-people-fill', 'color' => 'success'],
['label' => 'Buku Dipinjam', 'value' => Loan::whereIn('status', ['Dipinjam', 'Terlambat'])->count(), 'icon' => 'bi-arrow-up-right-circle-fill', 'color' => 'warning'],
['label' => 'Denda Menunggu', 'value' => Loan::where('status', 'Terlambat')->count(), 'icon' => 'bi-cash-coin', 'color' => 'danger'],
];
$viewData['aktivitasTerakhir'] = Loan::with(['user', 'book'])->latest()->take(4)->get()->map(fn($loan) => [
'nama' => $loan->user->nama_lengkap ?? 'Unknown',
'judul_buku' => $loan->book->judul ?? 'Unknown',
'tipe' => $loan->status === 'Dikembalikan' ? 'Pengembalian' : 'Peminjaman',
'waktu' => $loan->created_at->diffForHumans(),
'status' => $loan->status,
]);
} elseif ($user->role === 'guru') {
// Data untuk Guru: Data personal + ringkasan laporan minat baca
$viewData['bukuOffline'] = DummyDataService::getBukuPinjamOffline($user);
$viewData['bukuOnline'] = DummyDataService::getBacaBukuOnline($user);
$viewData['laporan'] = DummyDataService::getLaporanMinatBaca();
// Data untuk Guru
$loans = Loan::with('book')->where('user_id', $user->id)->whereIn('status', ['Dipinjam', 'Terlambat'])->get();
$viewData['bukuOffline'] = $loans->map(fn($loan) => [
'judul' => $loan->book->judul,
'penulis' => $loan->book->penulis,
'sisa_hari' => (int) now()->diffInDays(Carbon::parse($loan->due_at), false),
'cover' => $loan->book->cover,
]);
$viewData['bukuOnline'] = $loans->filter(fn($loan) => in_array('online', $loan->book->tipe_akses ?? []))->map(fn($loan) => [
'judul' => $loan->book->judul,
'penulis' => $loan->book->penulis,
'progress' => 0,
'cover' => $loan->book->cover,
]);
// Analytics for Guru (simplified for profile view)
$viewData['laporan'] = [
'buku_terpopuler' => Book::withCount('loans')->orderBy('loans_count', 'desc')->take(3)->get(),
'insight' => 'Siswa aktif meminjam buku kategori Sains.'
];
} else {
// Data default untuk Siswa
$viewData['bukuOffline'] = DummyDataService::getBukuPinjamOffline($user);
$viewData['bukuOnline'] = DummyDataService::getBacaBukuOnline($user);
$viewData['statistik'] = DummyDataService::getDashboardStats();
// Data untuk Siswa
$loans = Loan::with('book')->where('user_id', $user->id)->whereIn('status', ['Dipinjam', 'Terlambat'])->get();
$viewData['bukuOffline'] = $loans->map(fn($loan) => [
'judul' => $loan->book->judul,
'penulis' => $loan->book->penulis,
'sisa_hari' => (int) now()->diffInDays(Carbon::parse($loan->due_at), false),
'cover' => $loan->book->cover,
]);
$viewData['bukuOnline'] = $loans->filter(fn($loan) => in_array('online', $loan->book->tipe_akses ?? []))->map(fn($loan) => [
'judul' => $loan->book->judul,
'penulis' => $loan->book->penulis,
'progress' => 0,
'cover' => $loan->book->cover,
]);
$viewData['statistik'] = [
['label' => 'Buku dipinjam', 'value' => $loans->count(), 'icon' => 'bi-book-half', 'color' => 'primary'],
['label' => 'Tenggat Waktu', 'value' => $viewData['bukuOffline']->where('sisa_hari', '<=', 3)->where('sisa_hari', '>=', 0)->count(), 'icon' => 'bi-clock-history', 'color' => 'danger'],
['label' => 'Buku dikembalikan', 'value' => Loan::where('user_id', $user->id)->where('status', 'Dikembalikan')->count(), 'icon' => 'bi-check-circle', 'color' => 'success'],
['label' => 'History Baca', 'value' => Loan::where('user_id', $user->id)->count(), 'icon' => 'bi-hourglass-split', 'color' => 'warning'],
];
}
return view('profile.index', $viewData);

View File

@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Services\DummyDataService;
use App\Models\Recommendation;
class RekomendasiController extends Controller
{
@ -17,20 +17,20 @@ private function extractYouTubeId(string $url): ?string
public function show($id)
{
$rekomendasi = DummyDataService::getRekomendasiPembelajaran()->firstWhere('id', (int)$id);
abort_if(!$rekomendasi, 404);
$rekomendasi = Recommendation::findOrFail($id);
// Menambahkan thumbnail YouTube ke setiap rekomendasi
$embedLink = null;
$videoId = $this->extractYouTubeId($rekomendasi['youtube_link']);
$videoId = $this->extractYouTubeId($rekomendasi->youtube_link);
if ($videoId) {
$embedLink = "https://www.youtube.com/embed/" . $videoId;
}
$rekomendasi['youtube_embed_link'] = $embedLink;
$data = $rekomendasi->toArray();
$data['youtube_embed_link'] = $embedLink;
return view('rekomendasiShow', [
'pageTitle' => $rekomendasi['judul'],
'rekomendasi' => $rekomendasi,
'pageTitle' => $rekomendasi->judul,
'rekomendasi' => $data,
]);
}
}

View File

@ -2,18 +2,43 @@
namespace App\Http\Controllers;
use App\Services\DummyDataService;
use App\Models\Loan;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; // Tambahkan ini
use Illuminate\Support\Facades\Auth;
class RiwayatController extends Controller
{
public function offlineIndex()
{
$user = \Illuminate\Support\Facades\Auth::user();
if (!$user) $user = (object) ['id' => 1];
$user = Auth::user();
$loans = Loan::with('book.category')
->where('user_id', $user->id)
->whereIn('status', ['Dipinjam', 'Dikembalikan', 'Terlambat'])
->latest()
->get();
$riwayatOffline = DummyDataService::getRiwayatOffline($user);
$riwayatOffline = $loans->map(fn($loan) => [
'id' => $loan->id,
'id_peminjaman' => $loan->loan_code,
'kode_buku' => $loan->book->kode_buku,
'judul_utama' => $loan->book->judul,
'tanggal_pinjam' => $loan->borrowed_at->format('d/m/Y'),
'tanggal_kembali' => $loan->due_at ? $loan->due_at->format('d/m/Y') : '-',
'status' => $loan->status,
'books' => [
[
'id' => $loan->book->id,
'judul' => $loan->book->judul,
'kode_buku' => $loan->book->kode_buku,
'cover' => $loan->book->cover,
'deskripsi' => 'Buku ' . $loan->book->judul,
'kategori' => $loan->book->category->name ?? 'Tanpa Kategori',
'tahun' => $loan->book->tahun,
'keterangan' => $loan->status === 'Terlambat' ? 'Buku Terlambat' : null,
]
]
]);
return view('riwayat.offline', [
'pageTitle' => 'Riwayat Peminjaman Offline',
@ -23,7 +48,32 @@ public function offlineIndex()
public function onlineIndex()
{
$riwayatOnline = DummyDataService::getRiwayatOnline();
$user = Auth::user();
$loans = Loan::with('book.category')
->where('user_id', $user->id)
->where('status', 'Online')
->latest()
->get();
$riwayatOnline = $loans->map(fn($loan) => [
'id' => $loan->id,
'id_baca' => $loan->loan_code,
'judul_buku' => $loan->book->judul,
'tanggal_akses' => $loan->borrowed_at->format('d/m/Y'),
'status' => 'Selesai',
'books' => [
[
'id' => $loan->book->id,
'judul' => $loan->book->judul,
'cover' => $loan->book->cover,
'deskripsi' => 'Buku ' . $loan->book->judul,
'kategori' => $loan->book->category->name ?? 'Tanpa Kategori',
'tahun' => $loan->book->tahun,
'keterangan' => null
]
]
]);
return view('riwayat.online', [
'pageTitle' => 'Riwayat Baca Online',

View File

@ -52,64 +52,50 @@ public function rules(): array
*/
public function authenticate(): void
{
// Pastikan pengguna tidak mencoba login terlalu sering (mencegah brute-force).
$this->ensureIsNotRateLimited();
// Ambil data yang dikirim dari form login.
$roleDariForm = $this->input('role');
$allUsers = DummyDataService::getAllSiswa();
$inputPassword = $this->input('password');
$userArray = null;
// Tentukan field mana yang akan menerima pesan error jika gagal (nisn atau email).
$loginIdentifier = $this->input('nisn') ?: $this->input('nip');
$password = $this->input('password');
$errorField = $this->filled('nisn') ? 'nisn' : 'nip';
// Cari data pengguna berdasarkan input yang diberikan.
if ($this->filled('nisn')) {
// Jika form diisi dengan 'nisn', cari pengguna berdasarkan 'nisn'.
$userArray = collect($allUsers)->firstWhere('nisn', $this->input('nisn'));
} else {
// Jika tidak, cari pengguna berdasarkan 'nip'.
$userArray = collect($allUsers)->firstWhere('nip', $this->input('nip'));
if (!Auth::attempt(['nomor_induk' => $loginIdentifier, 'password' => $password], $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
$errorField => trans('auth.failed'),
]);
}
// Lakukan Pengecekan Kredensial dan Role.
if ($userArray && $userArray['password'] === $inputPassword) {
// Cek #2: Jika kredensial benar, apakah role pengguna sesuai dengan form yang digunakan?
if (isset($userArray['role']) && $userArray['role'] === $roleDariForm) {
// Buat objek User dari data dummy.
$userModel = new User();
$userArray['name'] = $userArray['nama_lengkap'];
$userModel->forceFill($userArray);
$user = Auth::user();
// Loginkan pengguna secara resmi ke dalam sistem.
Auth::login($userModel);
// Cek jika role sesuai
if ($user->role !== $roleDariForm) {
Auth::logout();
$this->session()->invalidate();
$this->session()->regenerateToken();
// Reset hitungan percobaan login yang gagal.
RateLimiter::clear($this->throttleKey());
return; // Proses autentikasi berhasil.
RateLimiter::hit($this->throttleKey());
} else {
// Tambah hitungan percobaan login yang gagal.
RateLimiter::hit($this->throttleKey());
// Ambil nama role asli pengguna untuk ditampilkan di pesan error.
$actualRole = Str::title($userArray['role'] ?? 'Tidak Dikenal');
// Lemparkan error validasi khusus 'forbidden' dengan pesan yang jelas.
throw ValidationException::withMessages([
'forbidden' => "Akses ditolak. Akun ini terdaftar sebagai {$actualRole}.",
]);
}
$actualRole = Str::title($user->role ?? 'Tidak Dikenal');
throw ValidationException::withMessages([
'forbidden' => "Akses ditolak. Akun ini terdaftar sebagai {$actualRole}.",
]);
}
// Jika pengguna tidak ditemukan atau password salah.
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
$errorField => trans('auth.failed'), // Pesan error umum "These credentials do not match...".
]);
// Cek jika akun di-banned
if ($user->is_banned) {
Auth::logout();
$this->session()->invalidate();
$this->session()->regenerateToken();
throw ValidationException::withMessages([
'forbidden' => "Akun Anda telah dinonaktifkan. Silakan hubungi admin.",
]);
}
RateLimiter::clear($this->throttleKey());
}
/**

View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Announcement extends Model
{
protected $fillable = ['type', 'icon', 'title', 'content'];
}

28
app/Models/Book.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
protected $fillable = [
'judul', 'penulis', 'cover', 'kode_buku',
'category_id', 'tahun', 'status', 'is_new', 'tipe_akses'
];
protected $casts = [
'tipe_akses' => 'array',
'is_new' => 'boolean',
];
public function category()
{
return $this->belongsTo(Category::class);
}
public function loans()
{
return $this->hasMany(Loan::class);
}
}

15
app/Models/Category.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
protected $fillable = ['name', 'slug'];
public function books()
{
return $this->hasMany(Book::class);
}
}

29
app/Models/Loan.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Loan extends Model
{
protected $fillable = [
'user_id', 'book_id', 'loan_code',
'borrowed_at', 'due_at', 'returned_at', 'status'
];
protected $casts = [
'borrowed_at' => 'datetime',
'due_at' => 'datetime',
'returned_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function book()
{
return $this->belongsTo(Book::class);
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Recommendation extends Model
{
protected $fillable = ['judul', 'kategori', 'youtube_link', 'deskripsi'];
}

View File

@ -19,8 +19,16 @@ class User extends Authenticatable
*/
protected $fillable = [
'name',
'nama_lengkap',
'email',
'password',
'nomor_induk',
'phone',
'nuptk',
'role',
'is_banned',
'kelas',
'golongan',
];
/**

10
app/Models/classes.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class classes extends Model
{
//
}

View File

@ -2,7 +2,8 @@
namespace App\Providers;
use App\Services\DummyDataService;
use App\Models\Loan;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
@ -31,7 +32,42 @@ public function boot(): void
View::composer('*', function ($view) {
if (Auth::check()) {
$user = Auth::user();
$notifikasi = collect(DummyDataService::getNotifikasiForUser($user));
// Fetch active loans to derive notifications
$loans = Loan::with('book')
->where('user_id', $user->id)
->whereIn('status', ['Dipinjam', 'Terlambat'])
->get();
$notifikasi = collect();
foreach ($loans as $loan) {
$dueAt = Carbon::parse($loan->due_at);
$sisaHari = (int) now()->diffInDays($dueAt, false);
if ($sisaHari < 0) {
$notifikasi->push([
'icon' => 'bi-exclamation-octagon-fill',
'color' => 'danger',
'title' => 'TERLAMBAT: ' . $loan->book->judul,
'content' => "Segera kembalikan buku. Denda berlaku.",
'time' => 'Sekarang',
'read' => false,
'type' => 'denda_active',
]);
} elseif ($sisaHari <= 3) {
$notifikasi->push([
'icon' => 'bi-exclamation-triangle-fill',
'color' => 'warning',
'title' => 'Jatuh Tempo: ' . $loan->book->judul,
'content' => "Tersisa {$sisaHari} hari lagi.",
'time' => 'Segera',
'read' => false,
'type' => 'warning_jatuh_tempo',
]);
}
}
$unreadCount = $notifikasi->where('read', false)->count();
$view->with('notifikasi', $notifikasi);
$view->with('unreadNotificationsCount', $unreadCount);

View File

@ -22,9 +22,6 @@ class AuthServiceProvider extends ServiceProvider
*/
public function boot(): void
{
// Daftarkan provider kustom kita di sini
Auth::provider('dummy', function ($app, array $config) {
return new DummyUserProvider();
});
//
}
}

View File

@ -38,7 +38,7 @@
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'dummy',
'provider' => 'users',
],
],
@ -64,10 +64,6 @@
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
'dummy' => [
'driver' => 'dummy',
],
],
/*

View File

@ -25,9 +25,12 @@ public function definition(): array
{
return [
'name' => fake()->name(),
'nama_lengkap' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'nomor_induk' => fake()->unique()->numerify('##########'),
'role' => 'siswa',
'remember_token' => Str::random(10),
];
}

View File

@ -14,9 +14,17 @@ public function up(): void
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('nama_lengkap')->nullable();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('nomor_induk')->unique();
$table->string('phone')->nullable();
$table->string('nuptk')->nullable();
$table->string('role');
$table->boolean('is_banned')->default(false);
$table->string('kelas')->nullable();
$table->string('golongan')->nullable();
$table->rememberToken();
$table->timestamps();
});

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('classes', function (Blueprint $table) {
$table->id();
$table->string('class');
$table->string('class_name');
$table->foreignId('id_users')->nullable()->constrained('users')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('classes');
}
};

View File

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('categories');
}
};

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->string('judul');
$table->string('penulis');
$table->string('cover')->nullable();
$table->string('kode_buku')->unique();
$table->foreignId('category_id')->constrained('categories')->onDelete('cascade');
$table->integer('tahun');
$table->string('status')->default('Tersedia');
$table->boolean('is_new')->default(false);
$table->json('tipe_akses'); // ['online', 'offline']
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('books');
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('loans', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->foreignId('book_id')->constrained('books')->onDelete('cascade');
$table->string('loan_code')->unique();
$table->timestamp('borrowed_at')->useCurrent();
$table->timestamp('due_at')->nullable();
$table->timestamp('returned_at')->nullable();
$table->string('status'); // Dipinjam, Dikembalikan, Terlambat, Online
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('loans');
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('announcements', function (Blueprint $table) {
$table->id();
$table->string('type'); // warning, info, success, etc.
$table->string('icon');
$table->string('title');
$table->text('content');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('announcements');
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('recommendations', function (Blueprint $table) {
$table->id();
$table->string('judul');
$table->string('kategori');
$table->string('youtube_link');
$table->text('deskripsi');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('recommendations');
}
};

View File

@ -0,0 +1,30 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Announcement;
use App\Services\DummyDataService;
class AnnouncementSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$announcements = DummyDataService::getPengumuman();
foreach ($announcements as $data) {
Announcement::updateOrCreate(
['id' => $data['id']],
[
'type' => $data['type'],
'icon' => $data['icon'],
'title' => $data['title'],
'content' => $data['content'],
]
);
}
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Book;
use App\Models\Category;
use App\Services\DummyDataService;
use Illuminate\Support\Str;
class BookSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$books = DummyDataService::getAllBooks();
foreach ($books as $data) {
$category = Category::where('name', $data['kategori'])->first();
Book::updateOrCreate(
['kode_buku' => $data['kode_buku']],
[
'id' => $data['id'],
'judul' => $data['judul'],
'penulis' => $data['penulis'],
'cover' => $data['cover'],
'category_id' => $category ? $category->id : null,
'tahun' => $data['tahun'],
'status' => $data['status'],
'is_new' => $data['is_new'],
'tipe_akses' => is_array($data['tipe_akses']) ? $data['tipe_akses'] : [$data['tipe_akses']],
]
);
}
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Category;
use App\Services\DummyDataService;
use Illuminate\Support\Str;
class CategorySeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$books = DummyDataService::getAllBooks();
$recommendations = DummyDataService::getRekomendasiPembelajaran();
$categories = collect($books)->pluck('kategori')
->merge(collect($recommendations)->pluck('kategori'))
->unique()
->filter();
foreach ($categories as $name) {
Category::updateOrCreate(
['slug' => Str::slug($name)],
['name' => $name]
);
}
}
}

View File

@ -13,11 +13,15 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
// User seeder is already handled or skipped as per user request
$this->call([
UserSeeder::class,
CategorySeeder::class,
BookSeeder::class,
RecommendationSeeder::class,
AnnouncementSeeder::class,
LoanSeeder::class,
]);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Loan;
use App\Models\User;
use App\Models\Book;
use App\Services\DummyDataService;
use Carbon\Carbon;
class LoanSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$books = DummyDataService::getAllBooks();
foreach ($books as $data) {
if ($data['user_id'] && $data['status'] !== 'Tersedia') {
// In dummy data, user_id is sometimes an array, handle it
$userIds = is_array($data['user_id']) ? $data['user_id'] : [$data['user_id']];
foreach ($userIds as $uId) {
$user = User::find($uId);
$book = Book::find($data['id']);
if ($user && $book) {
Loan::updateOrCreate(
[
'user_id' => $user->id,
'book_id' => $book->id,
'status' => $data['status'],
],
[
'loan_code' => 'PIN-' . date('Ym') . '-' . sprintf('%03d', $book->id),
'borrowed_at' => Carbon::now()->subDays(7),
'due_at' => Carbon::now()->addDays($data['sisa_hari'] ?? 7),
'status' => $data['status'],
]
);
}
}
}
}
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Recommendation;
use App\Services\DummyDataService;
class RecommendationSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$recommendations = DummyDataService::getRekomendasiPembelajaran();
foreach ($recommendations as $data) {
Recommendation::updateOrCreate(
['id' => $data['id']],
[
'judul' => $data['judul'],
'kategori' => $data['kategori'],
'youtube_link' => $data['youtube_link'],
'deskripsi' => $data['deskripsi'],
]
);
}
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Services\DummyDataService;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$allSiswa = DummyDataService::getAllSiswa();
foreach ($allSiswa as $data) {
$nomorInduk = $data['nisn'] ?? $data['nip'] ?? null;
if (!$nomorInduk) continue;
User::updateOrCreate(
['nomor_induk' => $nomorInduk],
[
'id' => $data['id'],
'name' => $data['nama_lengkap'],
'nama_lengkap' => $data['nama_lengkap'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'phone' => $data['nomor_hp'] ?? null,
'role' => $data['role'],
'is_banned' => $data['is_banned'] ?? false,
'kelas' => $data['kelas'] ?? null,
'golongan' => $data['golongan'] ?? null,
]
);
}
// Default admin
User::updateOrCreate(
['nomor_induk' => 'admin'],
[
'name' => 'Admin Perpustakaan',
'nama_lengkap' => 'Admin Perpustakaan',
'email' => 'admin@smkn1perpus.sch.id',
'password' => Hash::make('password'),
'role' => 'penjaga perpus',
]
);
}
}

View File

@ -37,7 +37,7 @@
// --- RUTE UNTUK PENGGUNA TERAUTENTIKASI (SISWA, GURU, & PENJAGA PERPUS) ---
Route::middleware(['auth'])->group(function () {
// Rute Umum untuk Siswa & Guru
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::get('/katalog', [KatalogController::class, 'index'])->name('katalog.index');
@ -80,11 +80,11 @@
// --- GRUP RUTE KHUSUS UNTUK ADMIN / PENJAGA PERPUSTAKAAN ---
Route::middleware(['auth', 'role:penjaga perpus'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [AdminDashboardController::class, 'index'])->name('dashboard');
Route::get('/buku', [AdminBookController::class, 'index'])->name('buku.index');
Route::get('/buku/tambah', [AdminBookController::class, 'create'])->name('buku.create');
Route::get('/buku/{id}/edit', [AdminBookController::class, 'edit'])->name('buku.edit');
Route::get('/pengguna', [AdminUserController::class, 'index'])->name('pengguna.index');
Route::get('/pengguna/tambah', [AdminUserController::class, 'create'])->name('pengguna.create');
Route::get('/pengguna/{id}/edit', [AdminUserController::class, 'edit'])->name('pengguna.edit');
@ -98,9 +98,9 @@
Route::get('/rekomendasi/tambah', [AdminRekomendasiController::class, 'create'])->name('rekomendasi.create');
Route::get('/rekomendasi/{id}/edit', [AdminRekomendasiController::class, 'edit'])->name('rekomendasi.edit');
Route::get('/peminjaman', [AdminPeminjamanController::class, 'index'])->name('peminjaman.index');
Route::get('/peminjaman/tambah', [AdminPeminjamanController::class, 'create'])->name('peminjaman.create');
Route::get('/peminjaman', [AdminPeminjamanController::class, 'index'])->name('peminjaman.index');
Route::get('/peminjaman/tambah', [AdminPeminjamanController::class, 'create'])->name('peminjaman.create');
Route::get('/denda', [AdminPeminjamanController::class, 'dendaIndex'])->name('denda.index');
Route::post('/denda/sanksi', [AdminPeminjamanController::class, 'berikanSanksi'])->name('denda.sanksi');
});
@ -111,4 +111,4 @@
Route::post('/admin/login', [AdminLoginController::class, 'store'])->name('admin.login.store');
});
require __DIR__ . '/auth.php';
require __DIR__ . '/auth.php';