feat: Feature peminjaman offline

This commit is contained in:
zhadaarsita 2025-09-24 17:01:20 +07:00
parent 88490ce78b
commit 172ac9e3b7
18 changed files with 1232 additions and 438 deletions

View File

@ -19,6 +19,16 @@ public function index()
$bukuPinjamOffline = DummyDataService::getBukuPinjamOffline($user);
$bacaBukuOnline = DummyDataService::getBacaBukuOnline($user);
$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('dashboard', compact(
'user',
'stats',
@ -27,7 +37,8 @@ public function index()
'progressMembaca',
'statistikBulanan',
'bukuPinjamOffline',
'bacaBukuOnline'
'bacaBukuOnline',
'greeting',
));
}
}
}

View File

@ -25,7 +25,6 @@ public function index(Request $request, $tipe = null)
'semuaBuku' => $semuaBuku,
'filterOptions' => $filterOptions,
'input' => $filters,
'tipe' => $tipe,
]);
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers;
use App\Services\DummyDataService;
use Illuminate\Http\Request;
class PeminjamanController extends Controller
{
/**
* Menampilkan daftar buku yang bisa dipinjam offline,
* dengan memanfaatkan filter dari getKatalogBuku.
*/
public function index(Request $request)
{
$filters = $request->only(['search', 'kategori', 'tahun', 'penulis']);
$filters['tipe_akses'] = 'offline';
$semuaBuku = DummyDataService::getKatalogBuku($filters);
$filterOptions = DummyDataService::getFilterOptions();
return view('peminjaman.index', [
'semuaBuku' => $semuaBuku,
'filterOptions' => $filterOptions,
'input' => $request->query(),
]);
}
/**
* Menampilkan halaman ringkasan buku sebelum meminjam.
*/
public function ringkasan($id)
{
// Untuk simulasi, kita anggap user sudah login
// $user = Auth::user();
$user = DummyDataService::getAllSiswa()[0];
$buku = DummyDataService::getKatalogBuku()->firstWhere('id', $id);
return view('peminjaman.ringkasan', compact('user', 'buku'));
}
/**
* Menampilkan form peminjaman dengan semua buku offline untuk pilihan tambahan.
*/
public function form($id)
{
// $user = Auth::user();
$user = DummyDataService::getAllSiswa()[0];
// Buku yang dipilih pertama kali
$buku = DummyDataService::getKatalogBuku()->firstWhere('id', $id);
// Semua buku offline untuk pilihan tambahan
$filters = ['tipe_akses' => 'offline'];
$semuaBuku = DummyDataService::getKatalogBuku($filters);
return view('peminjaman.form', compact('user', 'buku', 'semuaBuku'));
}
/**
* Proses pengiriman form peminjaman multi buku
*/
public function store(Request $request)
{
// Validasi input
$request->validate([
'buku_ids' => 'required|array|min:1|max:3',
'buku_ids.*' => 'integer|exists:books,id' // Sesuaikan dengan tabel yang ada
]);
// Logic untuk menyimpan peminjaman multiple books
$bukuIds = $request->input('buku_ids');
// Contoh logic penyimpanan:
foreach ($bukuIds as $bukuId) {
// Simpan ke database peminjaman
// Peminjaman::create([
// 'user_id' => auth()->id(),
// 'book_id' => $bukuId,
// 'tanggal_pinjam' => now(),
// 'tanggal_kembali' => now()->addDays(7),
// 'status' => 'dipinjam'
// ]);
}
return redirect()->route('dashboard')
->with('success', 'Berhasil meminjam ' . count($bukuIds) . ' buku!');
}
}

View File

@ -10,11 +10,13 @@ public static function getAllSiswa(): array
[
'id' => 1,
'nisn' => '1234567890',
'nama_lengkap' => 'John Doe',
'email' => 'johnskuy@smkn1perpus.sch.id',
'nomor_hp' => '081234567890',
'nama_lengkap' => 'Silvi Rahmawati',
'email' => 'silvi.rahmawati@smkn1perpus.sch.id',
'nomor_hp' => '08123456789',
'password' => 'password',
'role' => 'siswa',
'kelas' => 'XII RPL',
'golongan' => 'A',
],
[
'id' => 2,
@ -33,6 +35,8 @@ public static function getAllSiswa(): array
'nomor_hp' => '081998877665',
'password' => 'password',
'role' => 'siswa',
'kelas' => 'XII RPL A',
'golongan' => 'A',
],
[
'id' => 4,
@ -42,6 +46,8 @@ public static function getAllSiswa(): array
'nomor_hp' => '081556677889',
'password' => 'password',
'role' => 'siswa',
'kelas' => 'XII RPL A',
'golongan' => 'A',
],
[
'id' => 5,
@ -152,7 +158,7 @@ private static function getAllBooks()
'tahun' => 2023,
'status' => 'Tersedia',
'is_new' => false,
'tipe_akses' => 'online',
'tipe_akses' => ['online', 'offline'],
'progress' => 100,
'user_id' => [3, 1],
],
@ -206,7 +212,7 @@ private static function getAllBooks()
'is_new' => true,
'tipe_akses' => 'online',
'progress' => 40,
'user_id' => [1,4,5],
'user_id' => [1, 4, 5],
],
[
'id' => 7,
@ -219,9 +225,9 @@ private static function getAllBooks()
'is_new' => true,
'tipe_akses' => 'online',
'progress' => 0,
'user_id' => [3,1]
'user_id' => [3, 1]
],
[
[
'id' => 8,
'judul' => 'Buku Offline Tanpa Peminjam',
'penulis' => 'Penulis Misteri',
@ -289,24 +295,38 @@ public static function getBacaBukuOnline($user): array
/**
* Mengambil daftar buku untuk katalog dengan filter.
*/
public static function getKatalogBuku(array $filters = []): \Illuminate\Support\Collection
{
$buku = self::getAllBooks();
// app/Services/DummyDataService.php
$buku = $buku->when($filters['search'] ?? null, function ($query, $search) {
return $query->filter(fn($item) => str_contains(strtolower($item['judul']), strtolower($search)));
})->when($filters['kategori'] ?? null, function ($query, $kategori) {
return $query->where('kategori', $kategori);
})->when($filters['tahun'] ?? null, function ($query, $tahun) {
return $query->where('tahun', $tahun);
})->when($filters['penulis'] ?? null, function ($query, $penulis) {
return $query->where('penulis', $penulis);
})->when($filters['tipe'] ?? null, function ($query, $tipe) {
return $query->where('tipe_akses', $tipe);
});
public static function getKatalogBuku(array $filters = []): \Illuminate\Support\Collection
{
$buku = self::getAllBooks();
return $buku;
}
$buku = $buku->when($filters['search'] ?? null, function ($query, $search) {
return $query->filter(fn($item) => str_contains(strtolower($item['judul']), strtolower($search)));
})->when($filters['kategori'] ?? null, function ($query, $kategori) {
return $query->where('kategori', $kategori);
})->when($filters['tahun'] ?? null, function ($query, $tahun) {
return $query->where('tahun', $tahun);
})->when($filters['penulis'] ?? null, function ($query, $penulis) {
return $query->where('penulis', $penulis);
})
->when($filters['tipe_akses'] ?? null, function ($query, $tipe) {
if ($tipe === 'offline') {
return $query->filter(function ($buku) {
return $buku['tipe_akses'] === 'offline' || (is_array($buku['tipe_akses']) && in_array('offline', $buku['tipe_akses']));
});
}
if ($tipe === 'online') {
return $query->filter(function ($buku) {
return $buku['tipe_akses'] === 'online' || (is_array($buku['tipe_akses']) && in_array('online', $buku['tipe_akses']));
});
}
return $query;
})
->sortByDesc('status');
return $buku;
}
/**
* Method baru untuk mengambil daftar unik untuk dropdown filter

View File

@ -0,0 +1,83 @@
// public/js/dashboard-charts.js
document.addEventListener('DOMContentLoaded', function () {
// Ambil elemen canvas Bar Chart
const barChartEl = document.getElementById('barChart');
if (barChartEl) {
const statistikData = JSON.parse(barChartEl.dataset.stats);
new Chart(barChartEl.getContext('2d'), {
type: 'bar',
data: {
labels: statistikData.labels,
datasets: [{
label: 'Buku Dibaca',
backgroundColor: '#435ebe',
data: statistikData.data,
borderRadius: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0,0,0,0.05)',
},
ticks: {
color: '#6c757d',
font: {
size: 12
}
}
},
x: {
grid: {
display: false,
},
ticks: {
color: '#6c757d',
font: {
size: 12
}
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
const donutChartEl = document.getElementById('donutChart');
if (donutChartEl) {
const progressData = JSON.parse(donutChartEl.dataset.progress);
new Chart(donutChartEl.getContext('2d'), {
type: 'doughnut',
data: {
labels: ['Telah Dibaca', 'Belum Dibaca'],
datasets: [{
data: [progressData.selesai, progressData.sisa],
backgroundColor: ['#435ebe', '#e9ecef'],
borderColor: ['#ffffff'],
borderWidth: 3,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
cutout: '75%'
}
});
}
});

View File

@ -1,8 +1,9 @@
import './bootstrap';
import 'bootstrap';
import './peminjaman-form.js';
import * as bootstrap from 'bootstrap';
window.bootstrap = bootstrap;
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
Alpine.start();

View File

@ -0,0 +1,188 @@
const formPeminjamanElement = document.getElementById('formPeminjaman');
if (formPeminjamanElement) {
// Ambil data yang sudah disimpan di elemen HTML
const semuaBuku = JSON.parse(formPeminjamanElement.dataset.semuaBuku);
const bukuAwal = JSON.parse(formPeminjamanElement.dataset.bukuAwal);
let bukuTerpilih = [bukuAwal.id];
const maxBuku = 3;
document.addEventListener('DOMContentLoaded', function () {
updateCounter();
updateSelectedBooks();
updateCheckboxes();
updateBtnTambahBuku();
updateDaftarBuku();
});
function updateCounter() {
document.getElementById('counterBuku').textContent = bukuTerpilih.length;
document.getElementById('sisaSlot').textContent = maxBuku - bukuTerpilih.length;
}
function updateBtnTambahBuku() {
const btnTambah = document.getElementById('btnTambahBuku');
if (bukuTerpilih.length >= maxBuku) {
btnTambah.classList.add('disabled');
btnTambah.innerHTML = '<i class="bi bi-check-circle me-1"></i>Maksimal Buku Tercapai';
} else {
btnTambah.classList.remove('disabled');
btnTambah.innerHTML = '<i class="bi bi-plus-circle me-1"></i>Tambah Buku';
}
}
function updateSelectedBooks() {
const container = document.getElementById('selectedBooks');
const selected = semuaBuku.filter(book => bukuTerpilih.includes(book.id));
container.innerHTML = selected.map(book => `<li>${book.judul}</li>`).join('');
}
function updateCheckboxes() {
document.querySelectorAll('.book-checkbox').forEach(checkbox => {
const bookId = parseInt(checkbox.value);
checkbox.checked = bukuTerpilih.includes(bookId);
if (bukuTerpilih.length >= maxBuku && !bukuTerpilih.includes(bookId)) {
checkbox.disabled = true;
} else if (bookId !== bukuAwal.id) {
checkbox.disabled = false;
}
});
}
window.toggleBookSelection = function (bookId) {
if (bookId === bukuAwal.id) return;
const checkbox = document.getElementById('book' + bookId);
if (!checkbox.disabled) {
checkbox.checked = !checkbox.checked;
handleCheckboxChange(checkbox);
}
}
function handleCheckboxChange(checkbox) {
const bookId = parseInt(checkbox.value);
if (checkbox.checked) {
if (bukuTerpilih.length < maxBuku && !bukuTerpilih.includes(bookId)) {
bukuTerpilih.push(bookId);
} else {
checkbox.checked = false;
return;
}
} else {
bukuTerpilih = bukuTerpilih.filter(id => id !== bookId);
}
updateCounter();
updateSelectedBooks();
updateCheckboxes();
updateBtnTambahBuku();
}
document.addEventListener('change', function (e) {
if (e.target.classList.contains('book-checkbox')) {
if (e.target.value != bukuAwal.id) {
handleCheckboxChange(e.target);
}
}
});
document.getElementById('searchBuku')?.addEventListener('input', function (e) {
const searchTerm = e.target.value.toLowerCase();
document.querySelectorAll('.book-option').forEach(option => {
const title = option.getAttribute('data-book-title');
const author = option.getAttribute('data-book-author');
if (title.includes(searchTerm) || author.includes(searchTerm)) {
option.style.display = 'block';
} else {
option.style.display = 'none';
}
});
});
function updateDaftarBuku() {
const container = document.getElementById('daftarBukuPinjam');
const hiddenInputs = document.getElementById('hiddenInputs');
const selected = semuaBuku.filter(book => bukuTerpilih.includes(book.id));
container.innerHTML = selected.map((book) => `
<div class="book-item border rounded p-3 mb-3 shadow-sm" data-book-id="${book.id}">
<div class="d-flex align-items-start">
<img src="/${book.cover}" alt="Cover"
class="rounded me-3 form-book-cover"
style="width: 60px; height: 80px; object-fit: cover;">
<div class="flex-grow-1">
<h6 class="fw-bold mb-1">${book.judul}</h6>
<p class="text-muted small mb-1">${book.penulis}</p>
<span class="badge bg-info">${book.kategori}</span>
</div>
<div class="d-flex flex-column align-items-end gap-2">
${book.id === bukuAwal.id
? '<span class="badge bg-success">Buku Utama</span>'
: '<span class="badge bg-primary">Tambahan</span>'}
${book.id !== bukuAwal.id
? `<button type="button" class="btn btn-sm btn-outline-danger remove-book"
onclick="removeBuku(${book.id})"
title="Hapus Buku">
<i class="bi bi-trash"></i>
</button>`
: ''}
</div>
</div>
</div>
`).join('');
hiddenInputs.innerHTML = bukuTerpilih.map(id =>
`<input type="hidden" name="buku_ids[]" value="${id}">`
).join('');
}
window.removeBuku = function (bookId) {
if (bookId !== bukuAwal.id) {
bukuTerpilih = bukuTerpilih.filter(id => id !== bookId);
updateDaftarBuku();
updateCounter();
updateSelectedBooks();
updateCheckboxes();
updateBtnTambahBuku();
}
}
window.konfirmasiPilihanBuku = function () {
updateDaftarBuku();
const modal = bootstrap.Modal.getInstance(document.getElementById('pilihBukuModal'));
modal.hide();
}
document.getElementById('konfirmasiModal')?.addEventListener('show.bs.modal', function () {
const selected = semuaBuku.filter(book => bukuTerpilih.includes(book.id));
const ringkasan = selected.map((book, index) =>
`<div class="d-flex justify-content-between">
<span>${index + 1}. ${book.judul}</span>
<small class="text-muted">${book.penulis}</small>
</div>`
).join('');
document.getElementById('ringkasanBuku').innerHTML = ringkasan;
});
window.kirimForm = function () {
document.getElementById('formPeminjaman').submit();
}
document.getElementById('pilihBukuModal')?.addEventListener('hidden.bs.modal', function () {
const searchInput = document.getElementById('searchBuku');
if (searchInput) {
searchInput.value = '';
document.querySelectorAll('.book-option').forEach(option => {
option.style.display = 'block';
});
}
});
}

View File

@ -1,128 +1,139 @@
$primary: #435ebe;
$secondary: #6c757d;
$success: #198754;
$info: #0dcaf0;
$warning: #ffc107;
$danger: #dc3545;
$light: #f4f7f8;
$dark: #212529;
// ===================================
// VARIABLES & MAPS
// ===================================
// Derived colors untuk background light
$primary-light: rgba($primary, 0.1);
$success-light: rgba($success, 0.1);
$warning-light: rgba($warning, 0.1);
$danger-light: rgba($danger, 0.1);
$info-light: rgba($info, 0.1);
$secondary-light: rgba($secondary, 0.1);
// Theme Colors Map
$theme-colors: (
"primary": #435ebe,
"secondary": mix(#6c757d, #ffffff, 80%),
"success": mix(#198754, #ffffff, 85%),
"info": mix(#0dcaf0, #ffffff, 80%),
"warning": mix(#ffc107, #ffffff, 80%),
"danger": mix(#dc3545, #ffffff, 80%),
);
// Border radius
// Gray Colors Map
$grays: (
"light": #f4f7f8,
"dark": #4c5053
);
// Spacing & Sizing
$border-radius: 0.5rem;
$card-box-shadow: 0 0 1.25rem rgba(33,37,41,.05);
$border-radius-sm: 0.25rem;
$border-radius-lg: 1rem;
$shadow-sm: 0 2px 4px rgba(0,0,0,0.08);
$shadow-md: 0 4px 8px rgba(0,0,0,0.12);
$shadow-lg: 0 8px 20px rgba(0,0,0,0.15);
// Shadows & Transitions
$card-box-shadow: 0 0 1.25rem rgba(33, 37, 41, 0.05);
$shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);
$shadow-md: 0 4px 8px rgba(0, 0, 0, 0.12);
$shadow-lg: 0 8px 20px rgba(0, 0, 0, 0.15);
$transition: all 0.3s ease;
// ===================================
// CARD COMPONENTS
// UTILITIES (GENERATED FROM MAPS)
// ===================================
.card {
box-shadow: $card-box-shadow;
.card-header {
&.bg-white {
background-color: #fff !important;
border-bottom: 1px solid rgba($secondary, 0.125);
}
@each $color, $value in $theme-colors {
.bg-#{$color}-light {
background-color: rgba($value, 0.25) !important;
}
.bg-#{$color}-soft {
background-color: rgba($value, 0.25);
color: $value;
}
.alert-#{$color} {
background-color: rgba($value, 0.2);
color: darken($value, 25%);
border-color: rgba($value, 0.3);
}
}
// ===================================
// ICON COMPONENTS
// BASE COMPONENTS
// ===================================
.icon-circle {
width: 50px;
height: 50px;
.card {
box-shadow: $card-box-shadow;
.card-header {
background-color: #fff;
border-bottom: 1px solid rgba(map-get($grays, "dark"), 0.1); // DIPERBAIKI
}
}
.alert {
border-radius: $border-radius;
box-shadow: $shadow-sm;
.alert-icon {
width: 24px;
display: flex;
align-items: center;
justify-content: center;
}
}
.modal {
.modal-content {
border: none;
border-radius: $border-radius-lg;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
.modal-header,
.modal-footer {
border-color: rgba(map-get($grays, "dark"), 0.1);
padding: 1.5rem;
}
.modal-title {
color: map-get($grays, "dark"); // DIPERBAIKI
font-weight: 700;
}
}
}
.btn{
padding: 0.5rem 1.5rem;
font-weight: 500;
&.btn-sm {
padding: 0.375rem 1.25rem;
font-size: 0.875rem;
}
}
.badge.rounded-pill {
font-weight: 500;
padding: 0.5rem 1rem;
}
// ===================================
// CUSTOM COMPONENTS
// ===================================
.icon-circle, .icon-box {
display: flex;
align-items: center;
justify-content: center;
transition: $transition;
}
.section-icon,
.chart-icon,
.notification-icon,
.activity-icon {
.icon-circle {
width: 50px;
height: 50px;
border-radius: 50%;
}
.icon-box {
width: 40px;
height: 40px;
border-radius: 10px;
background-color: $primary-light;
display: flex;
align-items: center;
justify-content: center;
transition: $transition;
i {
font-size: 1.2rem;
}
}
// ===================================
// BACKGROUND COLORS
// ===================================
// Light backgrounds for icon circles
.bg-primary-light { background-color: $primary-light; }
.bg-success-light { background-color: $success-light; }
.bg-warning-light { background-color: $warning-light; }
.bg-danger-light { background-color: $danger-light; }
.bg-info-light { background-color: $info-light; }
.bg-secondary-light { background-color: $secondary-light; }
// Soft badge colors
.bg-primary-soft {
background-color: $primary-light;
color: $primary;
}
.bg-success-soft {
background-color: $success-light;
color: $success;
}
.bg-warning-soft {
background-color: $warning-light;
color: $warning;
}
.bg-danger-soft {
background-color: $danger-light;
color: $danger;
}
.bg-info-soft {
background-color: $info-light;
color: $info;
}
.bg-secondary-soft {
background-color: $secondary-light;
color: $secondary;
}
// ===================================
// BOOK COMPONENTS
// ===================================
.book-card {
transition: $transition;
box-shadow: $card-box-shadow;
&:hover {
transform: translateY(-2px);
box-shadow: $shadow-md !important;
box-shadow: $shadow-md;
.book-cover {
transform: scale(1.03);
}
@ -132,180 +143,169 @@ $transition: all 0.3s ease;
.book-cover {
transition: $transition;
border-radius: $border-radius-sm;
&-container {
background: linear-gradient(135deg, rgba($light, 0.5) 0%, rgba($light, 0.8) 100%);
background: linear-gradient(
135deg,
rgba(map-get($grays, "light"), 0.5) 0%, // DIPERBAIKI
rgba(map-get($grays, "light"), 0.8) 100% // DIPERBAIKI
);
border-radius: $border-radius-sm 0 0 $border-radius-sm;
}
}
// ===================================
// ALERT COMPONENTS
// ===================================
.alert {
border-radius: $border-radius;
box-shadow: $shadow-sm;
.alert-icon {
width: 24px;
display: flex;
align-items: center;
justify-content: center;
}
&.alert-primary {
background-color: rgba($primary, 0.1);
color: darken($primary, 10%);
}
&.alert-success {
background-color: rgba($success, 0.1);
color: darken($success, 10%);
}
&.alert-warning {
background-color: rgba($warning, 0.1);
color: darken($warning, 20%);
}
&.alert-danger {
background-color: rgba($danger, 0.1);
color: darken($danger, 10%);
}
&.alert-info {
background-color: rgba($info, 0.1);
color: darken($info, 10%);
}
}
// ===================================
// SECTION COMPONENTS
// ===================================
.section-header {
padding-bottom: 15px;
border-bottom: 2px solid $light;
border-bottom: 2px solid map-get($grays, "light"); // DIPERBAIKI
h5 {
color: $dark;
color: map-get($grays, "dark"); // DIPERBAIKI
font-weight: 700;
}
}
// ===================================
// MODAL COMPONENTS
// ===================================
.modal {
.modal-content {
border: none;
border-radius: $border-radius-lg;
box-shadow: 0 20px 40px rgba(0,0,0,0.15);
.modal-header {
border-bottom: 1px solid rgba($secondary, 0.125);
padding: 1.5rem;
.modal-title {
color: $dark;
font-weight: 700;
}
}
.modal-footer {
border-top: 1px solid rgba($secondary, 0.125);
padding: 1.5rem;
}
}
}
// ===================================
// UTILITY CLASSES
// ===================================
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
}
// Rounded buttons
.btn {
&.rounded-pill {
border-radius: 50px;
padding: 0.5rem 1.5rem;
font-weight: 500;
&.btn-sm {
padding: 0.375rem 1.25rem;
font-size: 0.875rem;
}
}
}
// Badge
.badge {
&.rounded-pill {
font-weight: 500;
padding: 0.5rem 1rem;
}
}
// ===================================
// SCROLLBAR CUSTOMIZATION
// ===================================
.modal-dialog-scrollable .modal-body {
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
}
// ===================================
// EMPTY STATES
// ===================================
.empty-state {
text-align: center;
padding: 3rem 1rem;
i {
font-size: 4rem;
color: $secondary;
color: map-get($grays, "dark"); // DIPERBAIKI
margin-bottom: 1rem;
opacity: 0.5;
opacity: 0.25;
}
p {
color: $secondary;
color: map-get($grays, "dark"); // DIPERBAIKI
opacity: 0.7;
font-size: 1rem;
margin: 0;
}
}
.btn-primary-soft {
--bs-btn-color: #{map-get($grays, "dark")};
--bs-btn-bg: #{map-get($theme-colors, "primary")};
--bs-btn-border-color: #{map-get($theme-colors, "primary")};
--bs-btn-hover-color: #{map-get($grays, "dark")};
--bs-btn-hover-bg: #{darken(map-get($theme-colors, "primary"), 5%)};
--bs-btn-hover-border-color: #{darken(map-get($theme-colors, "primary"), 7.5%)};
}
// ===================================
// UTILITIES & CUSTOMIZATIONS
// ===================================
.line-clamp-2,
.line-clamp-3 {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.4;
}
.line-clamp-2 { -webkit-line-clamp: 2; }
.line-clamp-3 { -webkit-line-clamp: 3; }
// Custom Scrollbar
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: map-get($grays, "light"); // DIPERBAIKI
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: rgba(map-get($grays, "dark"), 0.25); // DIPERBAIKI
border-radius: 3px;
&:hover {
background: rgba(map-get($grays, "dark"), 0.4); // DIPERBAIKI
}
}
// ===================================
// Style untuk kartu pemilihan buku di dalam modal
// ===================================
.card-book-select {
cursor: pointer;
position: relative;
transition: $transition;
&:hover {
transform: translateY(-5px);
box-shadow: $shadow-md;
}
.card-select-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(#435ebe, 0.7);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: $transition;
border-radius: $border-radius-sm;
i {
text-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
}
&.selected {
border: 2px solid map-get($theme-colors, "primary");
.card-select-overlay {
opacity: 1;
}
}
&.disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
// ===================================
// Styling untuk Form Peminjaman
// ===================================
.form-book-cover {
width: 60px;
height: 80px;
object-fit: cover;
}
.book-item {
background-color: map-get($grays, "light");
border-left: 4px solid map-get($theme-colors, "primary");
}
// Styling untuk kartu buku di dalam modal pemilihan
.book-card {
cursor: pointer;
transition: $transition;
border: 2px solid transparent;
&:hover {
border-color: map-get($theme-colors, "primary");
box-shadow: $shadow-md;
}
// Style khusus ketika checkbox di dalamnya terpilih
&:has(.book-checkbox:checked) {
border-color: map-get($theme-colors, "success");
background-color: rgba(map-get($theme-colors, "success"), 0.05);
}
}
.remove-book {
padding: 2px 6px;
font-size: 12px;
}
.btn.disabled {
pointer-events: none;
opacity: 0.65;
}
.book-option[style*="display: none"] {
display: none !important;
}

View File

@ -1,5 +1,6 @@
@import 'variables';
@import 'bootstrap/scss/bootstrap';
@import "bootstrap/scss/functions";
@import "variables";
@import "bootstrap/scss/bootstrap";
body {
background-color: $light;

View File

@ -0,0 +1,26 @@
@props(['buku'])
<div class="card border-0 shadow-sm h-100 book-card">
<div class="row g-0 h-100">
<div class="col-4">
<div class="book-cover-container d-flex align-items-center justify-content-center p-3 h-100">
<img src="{{ asset($buku['cover']) }}" class="img-fluid rounded shadow-sm book-cover" alt="Cover {{ $buku['judul'] }}">
</div>
</div>
<div class="col-8">
<div class="card-body d-flex flex-column h-100 p-3">
<div class="flex-grow-1">
<h6 class="card-title fw-bold text-dark mb-2 line-clamp-2">
{{ $buku['judul'] }}
</h6>
<p class="card-subtitle text-muted small mb-3">
<i class="bi bi-person-fill me-1"></i>{{ $buku['penulis'] }}
</p>
</div>
<div class="mt-auto">
{{ $slot }}
</div>
</div>
</div>
</div>
</div>

View File

@ -7,15 +7,15 @@
</button> --}}
<div class="mb-4">
<h3>Selamat Pagi, {{ $user->nama_lengkap }} !</h3>
<h3>{{ $greeting }}, {{ $user->nama_lengkap }} !</h3>
<p class="text-muted">Apa yang ingin kamu baca hari ini?</p>
</div>
<div class="row g-4 mb-4">
@foreach ($stats as $stat)
<div class="col-xl-3 col-lg-6 col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<div class="card rounded-2 bg-{{ $stat['color'] }}-light border-0 h-100">
<div class="card-body p-4 ">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="text-muted fw-normal mb-2 text-uppercase small">{{ $stat['label'] }}</h6>
@ -46,7 +46,8 @@
</div>
</div>
<div class="card-body p-4">
<canvas id="barChart" style="max-height: 300px;"></canvas>
<canvas id="barChart" style="max-height: 300px;" data-stats='@json($statistikBulanan ?? ['labels' => [], 'data' => []])'>
</canvas>
</div>
</div>
</div>
@ -62,7 +63,8 @@
</div>
<div class="card-body text-center d-flex justify-content-center align-items-center p-4">
<div class="chart-container" style="position: relative; height: 200px; width: 200px;">
<canvas id="donutChart"></canvas>
<canvas id="donutChart" data-progress='@json($progressMembaca ?? ['selesai' => 0, 'sisa' => 0])'>
</canvas>
</div>
</div>
<div class="text-center py-3">
@ -174,45 +176,22 @@ class="badge bg-{{ $item['type'] }}-soft text-{{ $item['type'] }} rounded-pill p
</div>
<h6 class="mb-0 fw-bold text-dark">Buku Pinjam Offline</h6>
</div>
<button type-="button" :class="expanded ? 'btn btn-sm btn-primary rounded-pill px-3' : 'btn btn-sm btn-outline-primary rounded-pill px-3'"
<button type="button"
:class="expanded ? 'btn btn-sm btn-primary rounded-pill px-3' :
'btn btn-sm btn-outline-primary rounded-pill px-3'"
@click="expanded = !expanded" x-show="{{ count($bukuPinjamOffline) > 2 }}">
<span x-text="expanded ? 'Tampilkan Lebih Sedikit' : 'Lihat Semua'">Lihat Semua</span>
</button>
</div>
<div class="row g-4">
@forelse($bukuPinjamOffline as $buku)
<div class="col-xl-4 col-md-6" x-show="{{ $loop->index }} < 3 || expanded" x-transition>
<div class="card border-0 shadow-sm h-100 book-card">
<div class="row g-0 h-100">
<div class="col-4">
<div
class="book-cover-container d-flex align-items-center justify-content-center p-3 h-100">
<img src="{{ asset($buku['cover']) }}"
class="img-fluid rounded shadow-sm book-cover"
alt="Cover {{ $buku['judul'] }}">
</div>
</div>
<div class="col-8">
<div class="card-body d-flex flex-column h-100 p-3">
<div class="flex-grow-1">
<h6 class="card-title fw-bold text-dark mb-2 line-clamp-2">
{{ $buku['judul'] }}</h6>
<p class="card-subtitle text-muted small mb-2">
<i class="bi bi-person-fill me-1"></i>{{ $buku['penulis'] }}
</p>
</div>
<div class="mt-auto">
<div
class="alert alert-danger border-0 py-2 px-3 mb-0 d-flex align-items-center">
<i class="bi bi-clock-fill me-2"></i>
<span class="fw-bold small">Sisa: {{ $buku['sisa_hari'] }}
hari</span>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6" x-show="{{ $loop->index }} < 2 || expanded" x-transition>
<x-book-card :buku="$buku">
<div class="alert alert-danger border-0 py-2 px-3 mb-0 d-flex align-items-center">
<i class="bi bi-clock-fill me-2"></i>
<span class="fw-bold small">Sisa: {{ $buku['sisa_hari'] }} hari</span>
</div>
</div>
</x-book-card>
</div>
@empty
<div class="col-12">
@ -233,7 +212,9 @@ class="alert alert-danger border-0 py-2 px-3 mb-0 d-flex align-items-center">
</div>
<h6 class="mb-0 fw-bold text-dark">Baca Buku Online</h6>
</div>
<button type="button" :class="expanded ? 'btn btn-sm btn-primary rounded-pill px-3' : 'btn btn-sm btn-outline-primary rounded-pill px-3'"
<button type="button"
:class="expanded ? 'btn btn-sm btn-primary rounded-pill px-3' :
'btn btn-sm btn-outline-primary rounded-pill px-3'"
@click="expanded = !expanded" x-show="{{ count($bacaBukuOnline) > 3 }}">
<span x-text="expanded ? 'Tampilkan Lebih Sedikit' : 'Lihat Semua'">Lihat Semua</span>
</button>
@ -241,45 +222,23 @@ class="alert alert-danger border-0 py-2 px-3 mb-0 d-flex align-items-center">
<div class="row g-4">
@forelse($bacaBukuOnline as $buku)
<div class="col-xl-4 col-md-6" x-show="{{ $loop->index }} < 3 || expanded" x-transition>
<div class="card border-0 shadow-sm h-100 book-card">
<div class="row g-0 h-100">
<div class="col-4">
<div
class="book-cover-container d-flex align-items-center justify-content-center p-3 h-100">
<img src="{{ asset($buku['cover']) }}"
class="img-fluid rounded shadow-sm book-cover"
alt="Cover {{ $buku['judul'] }}">
</div>
<x-book-card :buku="$buku">
<div class="progress-wrapper">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="small text-muted">Progress</span>
<span class="small fw-bold text-primary">{{ $buku['progress'] }}%</span>
</div>
<div class="col-8">
<div class="card-body d-flex flex-column h-100 p-3">
<div class="flex-grow-1">
<h6 class="card-title fw-bold text-dark mb-2 line-clamp-2">
{{ $buku['judul'] }}</h6>
<p class="card-subtitle text-muted small mb-3">
<i class="bi bi-person-fill me-1"></i>{{ $buku['penulis'] }}
</p>
</div>
<div class="mt-auto">
<div class="progress-wrapper">
<div
class="d-flex justify-content-between align-items-center mb-2">
<span class="small text-muted">Progress</span>
<span
class="small fw-bold text-primary">{{ $buku['progress'] }}%</span>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-gradient"
style="width: {{ $buku['progress'] }}%"
role="progressbar">
</div>
</div>
</div>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-primary rounded-pill"
style="width: {{ $buku['progress'] }}%" role="progressbar"
aria-valuenow="{{ $buku['progress'] }}" aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
</div>
</div>
</x-book-card>
</div>
@empty
<div class="col-12">
@ -368,87 +327,13 @@ class="badge bg-{{ $item['type'] }}-soft text-{{ $item['type'] }} rounded-pill p
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const statistikData = @json($statistikBulanan ?? ['labels' => [], 'data' => []]);
const progressData = @json($progressMembaca ?? ['selesai' => 0, 'sisa' => 0]);
// Bar Chart with improved styling
const ctxBar = document.getElementById('barChart').getContext('2d');
new Chart(ctxBar, {
type: 'bar',
data: {
labels: statistikData.labels,
datasets: [{
label: 'Buku Dibaca',
data: statistikData.data,
backgroundColor: '#435ebe',
borderRadius: 8,
borderSkipped: false,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(0,0,0,0.05)',
},
ticks: {
color: '#6c757d',
font: {
size: 12
}
}
},
x: {
grid: {
display: false,
},
ticks: {
color: '#6c757d',
font: {
size: 12
}
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
// Donut Chart
const ctxDonut = document.getElementById('donutChart').getContext('2d');
new Chart(ctxDonut, {
type: 'doughnut',
data: {
labels: ['Telah Dibaca', 'Belum Dibaca'],
datasets: [{
data: [progressData.selesai, progressData.sisa],
backgroundColor: ['#435ebe', '#e9ecef'],
borderColor: ['#435ebe', '#dee2e6'],
borderWidth: 3,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
cutout: '75%'
}
});
});
</script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{ asset('js/dashboard-charts.js') }}"></script>
</x-app-layout>

View File

@ -61,27 +61,48 @@
<div class="card-body d-flex flex-column p-3">
<div class="flex-grow-1">
{{-- Badge diletakkan di atas judul --}}
<div class="mb-2">
<span class="badge fw-normal bg-light text-dark border me-1">{{ $buku['kategori'] }}</span>
<span class="badge fw-normal text-primary border me-1">{{ $buku['kategori'] }}</span>
<span
class="badge fw-normal {{ $buku['status'] == 'Tersedia' ? 'bg-success-subtle text-success-emphasis' : 'bg-warning-subtle text-warning-emphasis' }} border">{{ $buku['status'] }}</span>
</div>
<h5 class="card-title fw-bold ">{{ $buku['judul'] }}</h5>
<h6 class="card-title fw-bold line-clamp-2">{{ $buku['judul'] }}</h6>
<p class="card-text small text-muted mb-2">{{ $buku['penulis'] }}</p>
</div>
<div class="d-grid mt-auto">
@if ($buku['status'] == 'Dipinjam')
<button class="btn btn-secondary btn-sm" disabled><i class="bi bi-x-circle me-1"></i>
Tidak Tersedia</button>
@else
@if ($buku['tipe_akses'] == 'online')
@if ($buku['status'] == 'Tersedia')
@if (is_array($buku['tipe_akses']))
<div class="d-flex gap-2">
<a href="{{ route('peminjaman.ringkasan', $buku['id']) }}"
class="btn btn-outline-primary btn-sm w-100">
<i class="bi bi-arrow-down-up me-1"></i> Pinjam
</a>
<button class="btn btn-primary btn-sm w-100"><i class="bi bi-book me-1"></i>
Baca</button>
</div>
@elseif ($buku['tipe_akses'] == 'online')
<button class="btn btn-primary btn-sm"><i class="bi bi-book me-1"></i> Baca
Online</button>
@else
<button class="btn btn-outline-primary btn-sm"><i
class="bi bi-arrow-down-up me-1"></i> Pinjam Offline</button>
<a href="{{ route('peminjaman.ringkasan', $buku['id']) }}"
class="btn btn-outline-primary btn-sm">
<i class="bi bi-arrow-down-up me-1"></i> Pinjam Offline
</a>
@endif
@else
@if (is_array($buku['tipe_akses']))
<div class="d-flex gap-2">
<button class="btn btn-secondary btn-sm w-100" disabled><i
class="bi bi-x-circle me-1"></i> Dipinjam</button>
<button class="btn btn-primary btn-sm w-100"><i class="bi bi-book me-1"></i>
Baca</button>
</div>
@else
<button class="btn btn-secondary btn-sm" disabled><i
class="bi bi-x-circle me-1"></i> Tidak Tersedia</button>
@endif
@endif
</div>

View File

@ -10,9 +10,15 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
@vite(['resources/scss/app.scss', 'resources/js/app.js'])
<!-- Flatpickr CSS & JS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/themes/material_blue.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://npmcdn.com/flatpickr/dist/l10n/id.js"></script>
<style>
body {
background-color: #f4f7f8;
background-color: #f3f3f3;
}
.sidebar {
@ -156,6 +162,7 @@ function toggleDesktopSidebar() {
});
});
</script>
@stack('scripts')
</body>
</html>
</html>

View File

@ -40,7 +40,8 @@
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">
<a href="{{ route('peminjaman.index') }}"
class="nav-link {{ request()->routeIs('peminjaman.*') ? 'active' : '' }}">
<i class="bi bi-arrow-left-right"></i>
<span class="nav-text ms-2">Peminjaman Offline</span>
</a>
@ -58,4 +59,4 @@
</a>
</li>
</ul>
</aside>
</aside>

View File

@ -0,0 +1,304 @@
<x-app-layout>
<div class="d-flex align-items-center mb-4">
<a href="{{ route('peminjaman.ringkasan', $buku['id']) }}" class="btn btn-outline-secondary me-3">
<i class="bi bi-arrow-left"></i>
</a>
<h1 class="h2 mb-0">Form Peminjaman Offline</h1>
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-0 shadow-sm">
<div class="card-body p-4 p-md-5">
{{-- Informasi Peminjam --}}
<div class="mb-5">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h5 class="fw-bold m-0">Informasi Peminjam</h5>
<a href="#" class="btn btn-sm btn-outline-secondary ">Edit Profil</a>
</div>
<div class="card-body p-4">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label text-muted small">Nama Lengkap</label>
<p class="fw-semibold">{{ $user['nama_lengkap'] }}</p>
</div>
<div class="col-md-6">
<label class="form-label text-muted small">Nomor Handphone</label>
<p class="fw-semibold">{{ $user['nomor_hp'] }}</p>
</div>
<div class="col-md-6">
<label class="form-label text-muted small">Kelas</label>
<p class="fw-semibold">{{ $user['kelas'] }}</p>
</div>
<div class="col-md-6">
<label class="form-label text-muted small">Golongan</label>
<p class="fw-semibold">{{ $user['golongan'] }}</p>
</div>
</div>
</div>
</div>
</div>
{{-- Form untuk submit data --}}
<form action="{{ route('peminjaman.store') }}" method="POST" id="formPeminjaman"
data-semua-buku="{{ json_encode($semuaBuku->values()) }}"
data-buku-awal="{{ json_encode($buku) }}">
@csrf
{{-- Informasi Buku & Tanggal --}}
<div class="mb-5">
<div class="d-flex justify-content-between align-items-center border-bottom pb-2 mb-3">
<h5 class="fw-bold m-0">Buku yang Dipinjam</h5>
<button type="button" class="btn btn-sm btn-primary " data-bs-toggle="modal"
data-bs-target="#pilihBukuModal" id="btnTambahBuku">
<i class="bi bi-plus-circle me-1"></i>Tambah Buku
</button>
</div>
{{-- Container untuk daftar buku yang dipinjam --}}
<div id="daftarBukuPinjam" class="mb-4">
<div class="book-item border rounded p-3 mb-3" data-book-id="{{ $buku['id'] }}">
<div class="d-flex align-items-start">
<img src="{{ asset($buku['cover']) }}" alt="Cover"
class="rounded me-3 form-book-cover">
<div class="flex-grow-1">
<h6 class="fw-bold mb-1">{{ $buku['judul'] }}</h6>
<p class="text-muted small mb-1">{{ $buku['penulis'] }}</p>
<span class="badge bg-info">{{ $buku['kategori'] }}</span>
</div>
<span class="badge bg-success">Buku Utama</span>
</div>
</div>
</div>
{{-- Hidden inputs untuk buku yang dipilih --}}
<div id="hiddenInputs">
<input type="hidden" name="buku_ids[]" value="{{ $buku['id'] }}">
</div>
{{-- Tanggal Peminjaman --}}
<div class="row g-3">
<div class="col-md-6">
<label for="tanggalPinjam" class="form-label text-muted">Tanggal Pinjam</label>
<input type="text" id="tanggalPinjam" name="tanggal_pinjam" class="form-control"
placeholder="Pilih tanggal pinjam"
value="{{ \Carbon\Carbon::now()->format('d F Y') }}">
</div>
<div class="col-md-6">
<label for="tanggalKembali" class="form-label text-muted">Tanggal Kembali</label>
<input type="text" id="tanggalKembali" name="tanggal_kembali"
placeholder="Pilih tanggal kembali" class="form-control"
value="{{ \Carbon\Carbon::now()->addDays(7)->format('d F Y') }}">
</div>
</div>
</div>
{{-- Peraturan Peminjaman --}}
<div class="mb-4">
<h5 class="fw-bold border-bottom pb-2 mb-3">Peraturan Peminjaman</h5>
<div class="alert alert-info border-0 bg-info-subtle">
<ol class="mb-0 ps-3">
<li>Perpustakaan buka pada jam operasional sekolah.</li>
<li>Setiap siswa hanya dapat meminjam sebanyak 2-3 buku dalam satu waktu.</li>
<li>Buku yang dipinjam harus dikembalikan dalam jangka waktu yang telah ditentukan.
</li>
<li>Jika buku tidak dikembalikan tepat waktu, akan dikenakan denda atau sanksi
lainnya.</li>
<li>Buku yang dikembalikan harus dalam kondisi baik, tanpa kerusakan atau hilang.
</li>
<li>Dilarang merusak atau mencoret-coret buku.</li>
<li>Setiap siswa bertanggung jawab penuh terhadap buku yang dipinjam.</li>
</ol>
</div>
</div>
{{-- Tombol Kirim Form --}}
<div class="d-grid">
<button type="button" class="btn btn-primary btn-lg " data-bs-toggle="modal"
data-bs-target="#konfirmasiModal">
Kirim Form
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{{-- Modal Pilih Buku --}}
<div class="modal fade" id="pilihBukuModal" tabindex="-1" aria-labelledby="pilihBukuModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content border-0 shadow-lg rounded-3">
<!-- Header -->
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold" id="pilihBukuModalLabel">
<i class="bi bi-book-half me-2 text-primary"></i>Pilih Buku Tambahan
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<!-- Body -->
<div class="modal-body">
<!-- Alert Info -->
<div class="alert alert-info border-0 bg-info-subtle mb-4 d-flex align-items-center">
<i class="bi bi-info-circle-fill me-2"></i>
<span>Anda dapat memilih maksimal <strong><span id="sisaSlot">2</span> buku</strong> lagi.
Total maksimal 3 buku.</span>
</div>
<!-- Search Box -->
<div class="mb-3">
<div class="input-group shadow-sm">
<span class="input-group-text bg-white"><i class="bi bi-search"></i></span>
<input type="text" class="form-control border-start-0" id="searchBuku"
placeholder="Cari buku...">
</div>
</div>
<!-- Daftar Buku -->
<div id="daftarBukuModal" class="row g-3">
@foreach ($semuaBuku as $bukuItem)
@if ($bukuItem['status'] == 'Tersedia' || $bukuItem['id'] == $buku['id'])
<div class="col-12 book-option" data-book-id="{{ $bukuItem['id'] }}"
data-book-title="{{ strtolower($bukuItem['judul']) }}"
data-book-author="{{ strtolower($bukuItem['penulis']) }}">
<div class="card book-card h-100 border-0 shadow-sm hover-shadow transition"
onclick="toggleBookSelection({{ $bukuItem['id'] }})">
<div class="card-body p-3">
<div class="d-flex align-items-start">
<!-- Cover Buku -->
<img src="{{ asset($bukuItem['cover']) }}" alt="Cover"
class="rounded me-3 form-book-cover"
style="width: 60px; height: 80px; object-fit: cover;">
<!-- Detail Buku -->
<div class="flex-grow-1">
<h6 class="fw-bold mb-1">{{ $bukuItem['judul'] }}</h6>
<p class="text-muted small mb-1">{{ $bukuItem['penulis'] }}</p>
<div class="d-flex align-items-center flex-wrap gap-2">
<span class="badge bg-info">{{ $bukuItem['kategori'] }}</span>
<span class="badge bg-success"><i
class="bi bi-check-circle me-1"></i>Tersedia</span>
@if ($bukuItem['id'] == $buku['id'])
<span class="badge bg-primary"><i
class="bi bi-star-fill me-1"></i>Buku Utama</span>
@endif
</div>
</div>
<!-- Checkbox -->
<div class="form-check ms-2">
<input class="form-check-input book-checkbox" type="checkbox"
value="{{ $bukuItem['id'] }}" id="book{{ $bukuItem['id'] }}"
@if ($bukuItem['id'] == $buku['id']) checked disabled @endif
onclick="event.stopPropagation()">
</div>
</div>
</div>
</div>
</div>
@endif
@endforeach
</div>
<!-- Counter Buku Terpilih -->
<div class="mt-4 p-3 bg-light rounded shadow-sm">
<div class="d-flex justify-content-between align-items-center">
<span class="fw-semibold">
<i class="bi bi-collection me-2 text-primary"></i>Buku Terpilih:
<span id="counterBuku" class="text-dark">1</span>/3
</span>
<ul>
<li id="selectedBooks" class="text-muted small"></li>
</ul>
</div>
</div>
</div>
<!-- Footer -->
<div class="modal-footer bg-light">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-1"></i> Batal
</button>
<button type="button" class="btn btn-primary" id="konfirmasiBuku"
onclick="konfirmasiPilihanBuku()">
<i class="bi bi-check2-circle me-1"></i> Konfirmasi Pilihan
</button>
</div>
</div>
</div>
</div>
{{-- Modal Konfirmasi Kirim --}}
<div class="modal fade" id="konfirmasiModal" tabindex="-1" aria-labelledby="konfirmasiModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header border-0 text-center d-block">
<h5 class="modal-title fw-bold" id="konfirmasiModalLabel">Apakah Anda Yakin Ingin Mengirim Form?
</h5>
</div>
<div class="modal-body text-center">
<div class="text-muted mb-3">Pastikan data yang Anda kirim sudah benar.</div>
<div class="alert alert-info border-0 bg-info-subtle text-start">
<strong>Ringkasan Peminjaman:</strong>
<div id="ringkasanBuku" class="mt-2"></div>
</div>
</div>
<div class="modal-footer border-0 justify-content-center">
<button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal">Batal</button>
{{-- Button Setuju jika sudah ada backend --}}
{{-- <button type="button" class="btn btn-primary px-4" onclick="kirimForm()">Setuju</button> --}}
{{-- Button Setuju jika belum ada backend, akan ter direct ke peminjaman.index --}}
<a href="{{ route('peminjaman.index') }}" class="btn btn-primary px-4">
Setuju
</a>
</div>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const tglPinjam = flatpickr("#tanggalPinjam", {
dateFormat: "d F Y",
altInput: true,
altFormat: "d F Y",
defaultDate: "{{ \Carbon\Carbon::now()->format('Y-m-d') }}",
locale: "id",
minDate: "today",
onChange: function(selectedDates, dateStr) {
if (tglKembali.selectedDates[0] <= selectedDates[0]) {
const newDate = new Date(selectedDates[0]);
newDate.setDate(newDate.getDate() + 1);
tglKembali.setDate(newDate);
}
tglKembali.set("minDate", new Date(selectedDates[0]).fp_incr(1));
}
});
const tglKembali = flatpickr("#tanggalKembali", {
dateFormat: "d F Y",
altInput: true,
altFormat: "d F Y",
defaultDate: "{{ \Carbon\Carbon::now()->addDays(7)->format('Y-m-d') }}",
locale: "id",
minDate: new Date().fp_incr(1)
});
});
</script>
</x-app-layout>

View File

@ -0,0 +1,100 @@
<x-app-layout>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">Daftar Buku Peminjaman Offline</h1>
</div>
<!-- Filter & Pencarian -->
<div class="card mb-4">
<div class="card-body">
<form action="{{ route('peminjaman.index') }}" method="GET">
<div class="row g-3 align-items-center">
<div class="col-md-4">
<input type="text" name="search" class="form-control"
placeholder="Cari buku berdasarkan judul..." value="{{ $input['search'] ?? '' }}">
</div>
<div class="col-md-2">
<select name="kategori" class="form-select">
<option value="">Semua Kategori</option>
@foreach ($filterOptions['kategori'] as $kategori)
<option value="{{ $kategori }}"
{{ ($input['kategori'] ?? '') == $kategori ? 'selected' : '' }}>{{ $kategori }}
</option>
@endforeach
</select>
</div>
<div class="col-md-2">
<select name="tahun" class="form-select">
<option value="">Semua Tahun</option>
@foreach ($filterOptions['tahun'] as $tahun)
<option value="{{ $tahun }}"
{{ ($input['tahun'] ?? '') == $tahun ? 'selected' : '' }}>{{ $tahun }}
</option>
@endforeach
</select>
</div>
<div class="col-md-2">
<select name="penulis" class="form-select">
<option value="">Semua Penulis</option>
@foreach ($filterOptions['penulis'] as $penulis)
<option value="{{ $penulis }}"
{{ ($input['penulis'] ?? '') == $penulis ? 'selected' : '' }}>{{ $penulis }}
</option>
@endforeach
</select>
</div>
<div class="col-md-2 d-grid">
<button type="submit" class="btn btn-primary">Filter</button>
</div>
</div>
</form>
</div>
</div>
<p class="fw-bold">Daftar buku yang tersedia secara offline dan dapat diambil di perpustakaan dengan mengisi form
yang tersedia.</p>
<!-- Daftar Buku -->
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-5 g-4">
@forelse ($semuaBuku as $buku)
<div class="col">
<div class="card h-100 shadow-sm border-0">
<img src="{{ asset($buku['cover']) }}" class="card-img-top rounded-2 object-fit-cover"
style="height: 310px;" alt="Cover {{ $buku['judul'] }}">
<div class="card-body d-flex flex-column p-3">
<div class="flex-grow-1">
<div class="mb-2">
<span class="badge fw-normal text-primary border me-1">{{ $buku['kategori'] }}</span>
<span
class="badge fw-normal {{ $buku['status'] == 'Tersedia' ? 'bg-success-subtle text-success-emphasis' : 'bg-warning-subtle text-warning-emphasis' }} border">{{ $buku['status'] }}</span>
</div>
<h6 class="card-title fw-bold line-clamp-2">{{ $buku['judul'] }}</h6>
<p class="card-text small text-muted mb-2">{{ $buku['penulis'] }}</p>
</div>
<div class="d-grid mt-auto">
@if ($buku['status'] == 'Tersedia')
<a href="{{ route('peminjaman.ringkasan', $buku['id']) }}"
class="btn btn-outline-primary btn-sm">
<i class="bi bi-arrow-down-up me-1"></i> Pinjam Offline
</a>
@else
<button class="btn btn-secondary btn-sm" disabled><i class="bi bi-x-circle me-1"></i>
Tidak Tersedia</button>
@endif
</div>
</div>
</div>
</div>
@empty
<div class="col-12">
<div class="alert alert-warning text-center">
<h4 class="alert-heading">Tidak Ada Hasil</h4>
<p>Tidak ada buku yang cocok dengan kriteria filter Anda. Coba reset atau ubah filter.</p>
<hr>
<a href="{{ route('peminjaman') }}" class="btn btn-primary">Reset Filter</a>
</div>
</div>
@endforelse
</div>
</x-app-layout>

View File

@ -0,0 +1,52 @@
<x-app-layout>
<div class="d-flex flex-column" style="min-height: calc(100vh - 110px);">
{{-- Header --}}
<div class="d-flex align-items-center mb-4">
<a href="{{ route('peminjaman.index')}}" class="btn btn-outline-secondary me-3">
<i class="bi bi-arrow-left"></i>
</a>
<h1 class="h2 mb-0">Ringkasan Buku</h1>
</div>
{{-- Wrapper main content --}}
<main class="flex-grow-1 d-flex align-items-center justify-content-center">
<div class="container-fluid">
<div class="row g-5 align-items-center">
{{-- Kolom Kiri untuk Cover Buku --}}
<div class="col-lg-4 text-center">
<img src="{{ asset($buku['cover']) }}" class="img-fluid rounded-3 shadow-lg"
style="max-height: 450px; max-width: 300px; object-fit: cover;" alt="Cover {{ $buku['judul'] }}">
</div>
{{-- Kolom Kanan untuk Detail & Button --}}
<div class="col-lg-8">
<h1 class="display-5 fw-bold mb-2">{{ $buku['judul'] }}</h1>
<p class="h5 text-muted mb-4">oleh {{ $buku['penulis'] }}</p>
<p class="lead" style="text-align: justify;">
Ini adalah ringkasan atau sinopsis dari buku "{{ $buku['judul'] }}". Lorem ipsum dolor sit
amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</p>
<div class="mt-5">
<a href="{{ route('peminjaman.form', $buku['id']) }}" class="btn btn-primary btn-lg rounded-pill px-5 py-3">
<i class="bi bi-file-earmark-text-fill me-2"></i>
Lanjutkan ke Form Peminjaman
</a>
</div>
</div>
</div>
</div>
</main>
<footer class="mt-auto text-center text-muted py-3">
<small>Copyright &copy; {{ date('Y') }} Perpus.</small>
</footer>
</div>
</x-app-layout>

View File

@ -2,6 +2,7 @@
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\KatalogController;
use App\Http\Controllers\PeminjamanController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
@ -15,6 +16,11 @@
Route::get('/katalog', [KatalogController::class, 'index'])
->middleware('auth')->name('katalog');
// Route untuk peminjaman offline
Route::get('/peminjaman-offline', [PeminjamanController::class, 'index'])->name('peminjaman.index');
Route::get('/peminjaman-offline/{id}/ringkasan', [PeminjamanController::class, 'ringkasan'])->name('peminjaman.ringkasan');
Route::get('/peminjaman-offline/{id}/form', [PeminjamanController::class, 'form'])->name('peminjaman.form');
Route::post('/peminjaman/store', [PeminjamanController::class, 'store'])->name('peminjaman.store');
// Route untuk user profile dari laravel breeze
Route::middleware('auth')->group(function () {