feat: Add book detail page and update book index to navigate to it instead of using a modal.

This commit is contained in:
cukiprit 2026-03-08 03:26:36 +07:00
parent 528c120fc3
commit b0c7a8b2cf
11 changed files with 236 additions and 163 deletions

View File

@ -150,41 +150,48 @@ public function export(Request $request)
$date = Carbon::parse($request->bulan_laporan);
$query->whereMonth('borrowed_at', $date->month)
->whereYear('borrowed_at', $date->year);
$fileName = 'Laporan_Peminjaman_'.$date->format('Y-m').'.csv';
$fileName = 'Laporan_Peminjaman_'.$date->format('Y-m').'.xls'; // Changed to .xls
} else {
$fileName = 'Laporan_Peminjaman_Semua.csv';
$fileName = 'Laporan_Peminjaman_Semua.xls'; // Changed to .xls
}
$loans = $query->get();
$headers = [
"Content-type" => "text/csv",
"Content-Type" => "application/vnd.ms-excel",
"Content-Disposition" => "attachment; filename=$fileName",
"Pragma" => "no-cache",
"Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
"Expires" => "0"
];
$columns = ['NO', 'ID PEMINJAMAN', 'PEMINJAM', 'ROLE', 'JUDUL BUKU', 'TGL PINJAM', 'T tenggat KEMBALI', 'STATUS', 'DENDA KETERLAMBATAN'];
$columns = ['NO', 'ID PEMINJAMAN', 'PEMINJAM', 'ROLE', 'JUDUL BUKU', 'TGL PINJAM', 'TENGGAT KEMBALI', 'STATUS', 'DENDA KETERLAMBATAN'];
$callback = function() use($loans, $columns) {
$file = fopen('php://output', 'w');
fputcsv($file, $columns);
// Generate HTML table for Excel
echo '<table border="1">';
echo '<thead><tr style="background-color: #f2f2f2;">';
foreach($columns as $col) echo "<th>$col</th>";
echo '</tr></thead>';
echo '<tbody>';
$i = 1;
foreach ($loans as $loan) {
$row['NO'] = $i++;
$row['ID_PEMINJAMAN'] = $loan->loan_code;
$row['PEMINJAM'] = $loan->user->nama_lengkap ?? 'Unknown';
$row['ROLE'] = $loan->user->role ?? '-';
$row['JUDUL_BUKU'] = $loan->book->judul ?? 'Unknown';
$row['TGL_PINJAM'] = $loan->borrowed_at->format('d/m/Y');
$row['TENGGAT_KEMBALI'] = $loan->due_at ? $loan->due_at->format('d/m/Y') : '-';
$row['STATUS'] = $loan->status;
$row['DENDA'] = $loan->fine_overdue ?? 0;
fputcsv($file, array_values($row));
echo '<tr>';
echo "<td>" . $i++ . "</td>";
echo "<td>" . ($loan->loan_code ?? '-') . "</td>";
echo "<td>" . ($loan->user->nama_lengkap ?? 'Unknown') . "</td>";
echo "<td>" . ($loan->user->role ?? '-') . "</td>";
echo "<td>" . ($loan->book->judul ?? 'Unknown') . "</td>";
echo "<td>" . $loan->borrowed_at->format('d/m/Y') . "</td>";
echo "<td>" . ($loan->due_at ? $loan->due_at->format('d/m/Y') : '-') . "</td>";
echo "<td>" . ($loan->status ?? '-') . "</td>";
echo "<td>" . ($loan->fine_overdue ?? 0) . "</td>";
echo '</tr>';
}
echo '</tbody></table>';
fclose($file);
};

View File

@ -53,6 +53,19 @@ public function create()
]);
}
/**
* Menampilkan detail buku.
*/
public function show($id)
{
$buku = Book::with('category')->findOrFail($id);
return view('admin.buku.show', [
'pageTitle' => 'Detail Buku: ' . $buku->judul,
'buku' => $buku
]);
}
public function edit($id)
{
$buku = Book::with('category')->findOrFail($id);

View File

@ -104,6 +104,26 @@ public function index(Request $request): \Illuminate\View\View|\Illuminate\Http\
['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'],
];
// Analytics for Siswa (same as Guru for consistency)
$viewData['laporan'] = [
'buku_terpopuler' => Book::withCount('loans')
->orderBy('loans_count', 'desc')
->take(3)
->get()
->map(fn($b) => [
'judul' => $b->judul,
'total_pembaca' => $b->loans_count
]),
'kategori_populer' => \App\Models\Category::select('categories.name as nama')
->join('books', 'categories.id', '=', 'books.category_id')
->join('loans', 'books.id', '=', 'loans.book_id')
->selectRaw('count(loans.id) as total_pembaca')
->groupBy('categories.id', 'categories.name')
->orderBy('total_pembaca', 'desc')
->take(3)
->get(),
];
}
return view('profile.index', $viewData);

View File

@ -58,4 +58,9 @@ public function getNamaLengkapAttribute()
{
return $this->name;
}
public function loans()
{
return $this->hasMany(Loan::class);
}
}

View File

@ -39,8 +39,12 @@
</div>
<div class="col-md-3 mb-3">
<label for="tahun" class="form-label">Tahun Terbit</label>
<input type="number" name="tahun" class="form-control" id="tahun"
placeholder="Contoh: 2024" min="0" required>
<select name="tahun" class="form-select" id="tahun" required>
<option value="" disabled selected>Pilih Tahun</option>
@for ($year = date('Y'); $year >= 1500; $year--)
<option value="{{ $year }}">{{ $year }}</option>
@endfor
</select>
</div>
<div class="col-md-3 mb-3">
<label for="kode_buku" class="form-label">Kode Buku</label>
@ -49,7 +53,7 @@
</div>
<div class="col-md-3 mb-3">
<label for="stok" class="form-label">Stok</label>
<input type="number" name="stok" class="form-control" id="stok"
<input type="text" name="stok" class="form-control" id="stok"
value="1" min="0" required>
</div>
</div>
@ -73,11 +77,11 @@
<div class="col-md-4">
<div class="mb-3">
<label for="cover" class="form-label">Cover Buku</label>
<input type="file" name="cover" class="form-control" id="cover">
<input type="file" name="cover" class="form-control" id="cover" accept="image/*">
</div>
<div class="mb-3">
<label for="file_pdf" class="form-label">File PDF (untuk buku online)</label>
<input type="file" name="file_pdf" class="form-control" id="file_pdf">
<input type="file" name="file_pdf" class="form-control" id="file_pdf" accept=".pdf">
</div>
</div>
</div>

View File

@ -41,8 +41,13 @@
</div>
<div class="col-md-4 mb-3">
<label for="tahun" class="form-label">Tahun Terbit</label>
<input type="number" name="tahun" class="form-control" id="tahun"
value="{{ old('tahun', $buku->tahun) }}" required>
<select name="tahun" class="form-select" id="tahun" required>
@for ($year = date('Y'); $year >= 1500; $year--)
<option value="{{ $year }}" {{ old('tahun', $buku->tahun) == $year ? 'selected' : '' }}>
{{ $year }}
</option>
@endfor
</select>
</div>
<div class="col-md-4 mb-3">
<label for="stok" class="form-label">Stok</label>
@ -84,13 +89,13 @@
<div class="col-md-4">
<div class="mb-3">
<label for="cover" class="form-label">Cover Buku</label>
<input type="file" name="cover" class="form-control" id="cover">
<input type="file" name="cover" class="form-control" id="cover" accept="image/*">
<img src="{{ asset($buku->cover) }}" alt="Cover saat ini"
class="img-thumbnail mt-2" width="150">
</div>
<div class="mb-3">
<label for="file_pdf" class="form-label">Update File PDF (Opsional)</label>
<input type="file" name="file_pdf" class="form-control" id="file_pdf">
<input type="file" name="file_pdf" class="form-control" id="file_pdf" accept=".pdf">
</div>
</div>
</div>

View File

@ -69,16 +69,9 @@ class="badge bg-warning-subtle text-warning-emphasis">Dipinjam</span>
@endif
</td>
<td>
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal"
data-bs-target="#detailBukuModal" data-id="{{ $buku['id'] }}"
data-cover="{{ asset($buku['cover']) }}"
data-judul="{{ $buku['judul'] }}"
data-kode_buku="{{ $buku['kode_buku'] }}"
data-penulis="{{ $buku['penulis'] }}"
data-kategori="{{ $buku->category->name ?? '-' }}"
data-tahun="{{ $buku['tahun'] }}" data-status="{{ $buku['status'] }}" data-stok="{{ $buku['stok'] ?? 0 }}">
<a href="{{ route('admin.buku.show', $buku->id) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye-fill"></i> Detail
</button>
</a>
<button class="btn btn-sm btn-outline-warning btn-arsipkan"
data-id="{{ $buku->id }}" data-judul="{{ $buku->judul }}" data-penulis="{{ $buku->penulis }}" title="Arsipkan Buku">
<i class="bi bi-archive-fill"></i> Arsip
@ -118,15 +111,9 @@ class="badge bg-warning-subtle text-warning-emphasis">Dipinjam</span>
class="badge bg-info-subtle text-info-emphasis">{{ $buku['file_pdf'] ?? 'N/A' }}</span>
</td>
<td>
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal"
data-bs-target="#detailBukuModal" data-id="{{ $buku['id'] }}"
data-cover="{{ asset($buku['cover']) }}"
data-judul="{{ $buku['judul'] }}"
data-penulis="{{ $buku['penulis'] }}"
data-kategori="{{ $buku->category->name ?? '-' }}"
data-tahun="{{ $buku['tahun'] }}" data-status="Dapat Dibaca Online" data-stok="{{ $buku['stok'] ?? 0 }}">
<a href="{{ route('admin.buku.show', $buku->id) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye-fill"></i> Detail
</button>
</a>
<button class="btn btn-sm btn-outline-warning btn-arsipkan"
data-id="{{ $buku->id }}" data-judul="{{ $buku->judul }}" data-penulis="{{ $buku->penulis }}" title="Arsipkan Buku">
<i class="bi bi-archive-fill"></i> Arsip
@ -193,61 +180,6 @@ class="badge bg-info-subtle text-info-emphasis">{{ $buku['file_pdf'] ?? 'N/A' }}
</div>
</div>
{{-- MODAL DETAIL BUKU --}}
<div class="modal fade" id="detailBukuModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content border-0 shadow">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold" id="modalJudul">Detail Buku</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-4 text-center mb-3">
<img src="" id="modalCover" class="img-fluid rounded shadow"
style="max-height: 250px; object-fit: cover;">
</div>
<div class="col-md-8">
<h3 id="modalJudulContent" class="fw-bold text-dark mb-1"></h3>
<p class="text-muted mb-3" id="modalPenulis"></p>
<div class="bg-light p-3 rounded-3">
<table class="table table-sm table-borderless mb-0">
<tr>
<td class="text-secondary" width="130">Kategori</td>
<td id="modalKategori" class="fw-bold"></td>
</tr>
<tr id="rowKodeBuku">
<td class="text-secondary">Kode Buku</td>
<td id="modalKode" class="fw-bold font-monospace"></td>
</tr>
<tr>
<td class="text-secondary">Tahun Terbit</td>
<td id="modalTahun" class="fw-bold"></td>
</tr>
<tr>
<td class="text-secondary">Stok</td>
<td id="modalStok" class="fw-bold"></td>
</tr>
<tr>
<td class="text-secondary">Status</td>
<td><span id="modalStatus" class="badge"></span></td>
</tr>
</table>
</div>
</div>
</div>
</div>
<div class="modal-footer border-0 bg-light">
<button type="button" class="btn btn-secondary rounded-pill px-4"
data-bs-dismiss="modal">Tutup</button>
<a href="#" id="modalEditButton" class="btn btn-primary rounded-pill px-4">
<i class="bi bi-pencil-square me-2"></i>Edit Buku
</a>
</div>
</div>
</div>
</div>
@push('scripts')
<script>
@ -271,37 +203,6 @@ function updateTableNumbers() {
});
}
// LOGIC MODAL DETAIL
$('#detailBukuModal').on('show.bs.modal', function(event) {
const button = $(event.relatedTarget);
$('#modalCover').attr('src', button.data('cover'));
$('#modalJudulContent').text(button.data('judul'));
$('#modalPenulis').text('Penulis: ' + button.data('penulis'));
$('#modalKategori').text(button.data('kategori'));
$('#modalTahun').text(button.data('tahun'));
$('#modalStok').text(button.data('stok') ? button.data('stok') + ' Buku' : '-');
const kodeBuku = button.data('kode_buku');
if (kodeBuku) {
$('#rowKodeBuku').show();
$('#modalKode').text(kodeBuku);
} else {
$('#rowKodeBuku').hide();
}
// Status Badge
const statusBadge = $('#modalStatus');
const status = button.data('status');
statusBadge.removeClass().addClass('badge');
if (status === 'Tersedia' || status === 'Dapat Dibaca Online' || !status) {
statusBadge.addClass('bg-success-subtle text-success-emphasis').text('Tersedia / Online');
} else {
statusBadge.addClass('bg-warning-subtle text-warning-emphasis').text('Dipinjam');
}
$('#modalEditButton').attr('href', "{{ url('admin/buku') }}/" + buku.id + "/edit");
});
// LOGIC ARSIPKAN BUKU
$(document).on('click', '.btn-arsipkan', function() {

View File

@ -0,0 +1,107 @@
<x-app-layout>
@section('page-title', $pageTitle)
<div class="d-flex align-items-center mb-4">
<a href="{{ route('admin.buku.index') }}" class="btn btn-outline-secondary me-3 shadow-sm rounded-circle">
<i class="bi bi-arrow-left"></i>
</a>
<h3 class="my-0 fw-bold">Detail Buku</h3>
</div>
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="card border-0 shadow-sm overflow-hidden">
<div class="card-body p-0">
<div class="row g-0">
{{-- Cover Section --}}
<div class="col-md-4 bg-light d-flex align-items-center justify-content-center p-4 border-end">
<div class="position-relative">
<img src="{{ asset($buku->cover) }}" alt="{{ $buku->judul }}"
class="img-fluid rounded shadow-lg" style="max-height: 400px; object-fit: cover;">
@if($buku->is_arsip)
<div class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center rounded"
style="background: rgba(0,0,0,0.5);">
<span class="badge bg-warning text-dark fs-5 shadow">DIARSIPKAN</span>
</div>
@endif
</div>
</div>
{{-- Info Section --}}
<div class="col-md-8">
<div class="p-4 p-md-5">
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h2 class="fw-bold text-dark mb-1">{{ $buku->judul }}</h2>
<p class="fs-5 text-muted mb-0">oleh <span class="text-primary fw-semibold">{{ $buku->penulis }}</span></p>
</div>
<div class="d-flex gap-2">
<a href="{{ route('admin.buku.edit', $buku->id) }}" class="btn btn-warning rounded-pill">
<i class="bi bi-pencil-square me-2"></i>Edit
</a>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-sm-6">
<div class="p-3 border rounded-3 bg-light-subtle">
<small class="text-muted d-block text-uppercase fw-bold mb-1" style="font-size: 0.7rem;">Kategori</small>
<span class="fw-bold fs-5 text-dark">{{ $buku->category->name ?? '-' }}</span>
</div>
</div>
<div class="col-sm-6">
<div class="p-3 border rounded-3 bg-light-subtle">
<small class="text-muted d-block text-uppercase fw-bold mb-1" style="font-size: 0.7rem;">Tahun Terbit</small>
<span class="fw-bold fs-5 text-dark">{{ $buku->tahun }}</span>
</div>
</div>
<div class="col-sm-6">
<div class="p-3 border rounded-3 bg-light-subtle">
<small class="text-muted d-block text-uppercase fw-bold mb-1" style="font-size: 0.7rem;">Kode Buku</small>
<code class="fw-bold fs-5 text-primary">{{ $buku->kode_buku ?? 'N/A' }}</code>
</div>
</div>
<div class="col-sm-6">
<div class="p-3 border rounded-3 bg-light-subtle">
<small class="text-muted d-block text-uppercase fw-bold mb-1" style="font-size: 0.7rem;">Stok Tersedia</small>
<span class="fw-bold fs-5 text-dark">{{ $buku->stok }} Buku</span>
</div>
</div>
</div>
<div class="mb-4">
<h6 class="fw-bold text-dark mb-3">Tipe Akses:</h6>
<div class="d-flex gap-2">
@if(in_array('offline', $buku->tipe_akses ?? []))
<span class="badge bg-secondary-subtle text-secondary-emphasis px-3 py-2 rounded-pill">
<i class="bi bi-book-half me-2"></i>Peminjaman Offline
</span>
@endif
@if(in_array('online', $buku->tipe_akses ?? []))
<span class="badge bg-info-subtle text-info-emphasis px-3 py-2 rounded-pill">
<i class="bi bi-globe2 me-2"></i>Baca Online
</span>
@endif
</div>
</div>
@if(in_array('online', $buku->tipe_akses ?? []) && $buku->file_pdf)
<div class="border-top pt-4">
<h6 class="fw-bold text-dark mb-2">File PDF Digital:</h6>
<div class="d-flex align-items-center p-3 border rounded-3 bg-light">
<i class="bi bi-file-earmark-pdf-fill text-danger fs-1 me-3"></i>
<div>
<p class="mb-0 fw-bold">{{ $buku->file_pdf }}</p>
<small class="text-muted">Buku dapat diakses secara digital oleh siswa & guru.</small>
</div>
</div>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -214,8 +214,9 @@ class="form-delete-whitelist" data-induk="{{ $item->nomor_induk }}">
</div>
<div class="mb-3">
<label class="form-label">NIP / NISN / NIK</label>
<input type="number" name="nomor_induk" class="form-control"
placeholder="Contoh: 1234567890" required>
<input type="text" name="nomor_induk" class="form-control"
placeholder="Contoh: 1234567890" maxlength="16"
oninput="this.value = this.value.replace(/[^0-9]/g, '')" required>
</div>
<div class="mb-3">
<label class="form-label">Nama Pemilik</label>

View File

@ -202,54 +202,63 @@ class="btn btn-outline-primary rounded-pill w-100 w-sm-auto">
<h5 class="fw-bold mb-3">Informasi Personal</h5>
<div class="row g-3">
<div class="col-sm-6 col-md-3">
<div class="col-sm-6 col-md-4">
<small class="text-muted d-block mb-1">NISN</small>
<p class="fw-semibold mb-0">{{ $user->nomor_induk ?? '-' }}</p>
</div>
<div class="col-sm-6 col-md-3">
<div class="col-sm-6 col-md-4">
<small class="text-muted d-block mb-1">Email</small>
<p class="fw-semibold mb-0 text-break">{{ $user->email }}</p>
</div>
<div class="col-sm-6 col-md-3">
<div class="col-sm-6 col-md-4">
<small class="text-muted d-block mb-1">Nomor HP</small>
<p class="fw-semibold mb-0">{{ $user->no_hp ?? '-' }}</p>
</div>
<div class="col-sm-6 col-md-3">
<small class="text-muted d-block mb-1">Kelas</small>
<p class="fw-semibold mb-0">{{ $user->kelas ?? '-' }}</p>
</div>
</div>
</div>
<div class="mb-3 mb-md-4 flex-grow-1">
<h5 class="fw-bold mb-3">Statistik Saya</h5>
<div class="row g-3">
@foreach ($statistik as $stat)
<div class="col-sm-6 col-lg-3">
<div class="card border-0 h-100">
<div class="card-body p-3 p-md-4 text-center">
<div class="icon-circle bg-{{ $stat['color'] }}-light mx-auto mb-3"
style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; border-radius: 15px;">
<i class="bi {{ $stat['icon'] }} fs-2 text-{{ $stat['color'] }}"></i>
</div>
<h3 class="fw-bold mb-2">{{ $stat['value'] }}</h3>
<p class="text-muted mb-0 small">{{ $stat['label'] }}</p>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
<div class="col-lg-4">
@include('profile.partials.personal-activities', [
'bukuOffline' => $bukuOffline,
'bukuOnline' => $bukuOnline,
])
<div class="card border-0 flex-grow-1">
<div class="card-body p-3 p-md-4 d-flex flex-column h-100">
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-center mb-4 gap-3">
<h5 class="fw-bold mb-0">Ringkasan Laporan Minat Baca</h5>
</div>
<div class="row flex-grow-1">
<div class="col-md-6 mb-3 mb-md-0">
<h6 class="small text-muted mb-3 text-uppercase fw-semibold">Buku Terpopuler</h6>
<ul class="list-group list-group-flush laporan-list">
@foreach ($laporan['buku_terpopuler'] as $buku)
<li class="list-group-item px-0 py-3 d-flex justify-content-between align-items-center">
<span class="text-truncate me-2">{{ $buku['judul'] }}</span>
<span class="badge bg-primary rounded-pill">{{ $buku['total_pembaca'] }}</span>
</li>
@endforeach
</ul>
</div>
<div class="col-md-6">
<h6 class="small text-muted mb-3 text-uppercase fw-semibold">Kategori Terpopuler</h6>
<ul class="list-group list-group-flush laporan-list">
@foreach ($laporan['kategori_populer'] as $kategori)
<li class="list-group-item px-0 py-3 d-flex justify-content-between align-items-center">
<span class="text-truncate me-2">{{ $kategori['nama'] }}</span>
<span class="badge bg-success rounded-pill">{{ $kategori['total_pembaca'] }}</span>
</li>
@endforeach
</ul>
</div>
</div>
</div>
</div>
</div>
@endif
<div class="col-lg-4">
@include('profile.partials.personal-activities', [
'bukuOffline' => $bukuOffline,
'bukuOnline' => $bukuOnline,
])
</div>
</div>
@endif
</div>
</x-app-layout>

View File

@ -86,6 +86,7 @@
Route::get('/buku', [AdminBookController::class, 'index'])->name('buku.index');
Route::get('/buku/tambah', [AdminBookController::class, 'create'])->name('buku.create');
Route::get('/buku/{id}', [AdminBookController::class, 'show'])->name('buku.show');
Route::post('/buku', [AdminBookController::class, 'store'])->name('buku.store');
Route::get('/buku/{id}/edit', [AdminBookController::class, 'edit'])->name('buku.edit');
Route::post('/buku/arsip', [AdminBookController::class, 'arsip'])->name('buku.arsip');