Compare commits

...

10 Commits

Author SHA1 Message Date
wardhatul1765 17c93a4252 Update project and prepare for JTI Polije upload 2026-06-17 13:42:00 +07:00
wardhatul1765 5f30be4811 mengubah ukuran gambar 2026-05-17 20:44:20 +07:00
wardhatul1765 1071e4e428 denah 2026-05-17 16:53:32 +07:00
wardhatul1765 bf0996430a perbaiki tampilan 2026-05-17 16:33:43 +07:00
wardhatul1765 a656f599a4 Merge branch 'main' of https://github.com/wardhatul1765/Sarakata-Perpustakaan-Daerah-Jember 2026-05-05 02:45:16 +07:00
wardhatul1765 077e2746f6 Memperbarui fitur Buku Tamu dan form Tambah Buku 2026-05-05 02:42:52 +07:00
Lutfi Hakim 570b134f7c fix: hero responsive 2026-05-02 11:29:19 +07:00
wardhatul1765 70a420ae47 Memperbaiki UI dan fitur kirim WA Fonnte 2026-05-02 10:50:45 +07:00
Lutfi Hakim ca98d0a665 Fix peminjaman FK and use id_buku in views
Add migration to fix peminjaman.id_buku foreign key: modify column to INT UNSIGNED and re-create FK referencing buku.id_buku with cascadeOnDelete. Update admin peminjaman blades to use $b->id_buku for option values. Remove unused imports and tidy up formatting/whitespace in Peminjaman and AdminPeminjaman controllers (including small reformat of Fonnte API payload). These changes ensure the dropdowns use the correct book identifier and the DB relation is consistent.
2026-05-01 23:36:20 +07:00
Lutfi Hakim c2a61ea5a0 Update LoginController.php 2026-04-30 15:20:14 +07:00
57 changed files with 2309 additions and 706 deletions

26
.htaccess Normal file
View File

@ -0,0 +1,26 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /E31230887/
# 1. Handle Storage (Internal Rewrite)
# Mapping /storage/... ke /public/storage/... secara internal
RewriteRule ^storage/(.*)$ public/storage/$1 [L]
# 2. Handle Other Static Assets (img, build, images, favicon, robots.txt)
# Menghindari redirect R=302 agar URL tetap bersih tanpa /public/
RewriteCond %{REQUEST_URI} !^/E31230887/public/
RewriteRule ^(img|build|images|favicon\.ico|robots\.txt)(.*)$ public/$1$2 [L]
# 3. Handle Laravel Front Controller
# Jika file fisik tidak ada, lempar ke index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]
</IfModule>
Options -Indexes
<FilesMatch "^\.env">
Order allow,deny
Deny from all
</FilesMatch>

View File

@ -20,20 +20,24 @@ public function store(Request $request)
'bibid' => 'required|string|max:30|unique:buku,bibid', 'bibid' => 'required|string|max:30|unique:buku,bibid',
'judul' => 'required|string', 'judul' => 'required|string',
'pengarang' => 'required|string|max:100', 'pengarang' => 'required|string|max:100',
'penerbit' => 'nullable|string', 'penerbit' => 'required|string',
'tahun_terbit' => 'nullable|digits:4', 'tahun_terbit' => 'required|digits:4',
'edisi' => 'nullable|string|max:50', 'edisi' => 'required|digits:4',
'deskripsi_fisik' => 'nullable|string|max:100', 'deskripsi_fisik' => 'required|string|max:100',
'nomor_panggil' => 'required|string|max:50', 'nomor_panggil' => 'required|string|max:50|unique:buku,nomor_panggil',
'eksemplar' => 'required|integer|min:1', 'eksemplar' => 'required|integer|min:1',
'id_kategori' => 'required|exists:kategori,id_kategori', 'id_kategori' => 'required|exists:kategori,id_kategori',
'cover' => 'nullable|image|mimes:jpeg,png,jpg|max:2048' 'cover' => 'image|mimes:jpeg,png,jpg,webp|max:2048',
]); ]);
if ($request->hasFile('cover')) { if ($request->hasFile('cover')) {
$validated['cover'] = $request->file('cover')->store('covers', 'public'); $validated['cover'] = $request->file('cover')->store('covers', 'public');
} }
$koordinat = $this->tentukanKoordinatRak($validated['nomor_panggil']);
$validated['lokasi_x'] = $koordinat['x'];
$validated['lokasi_y'] = $koordinat['y'];
$validated['konten_digital'] = 0; $validated['konten_digital'] = 0;
Buku::create($validated); Buku::create($validated);
@ -71,14 +75,14 @@ public function update(Request $request, $id)
'bibid' => 'required|string|max:30|unique:buku,bibid,' . $id . ',id_buku', 'bibid' => 'required|string|max:30|unique:buku,bibid,' . $id . ',id_buku',
'judul' => 'required|string', 'judul' => 'required|string',
'pengarang' => 'required|string|max:100', 'pengarang' => 'required|string|max:100',
'penerbit' => 'nullable|string', 'penerbit' => 'required|string',
'tahun_terbit' => 'nullable|digits:4', 'tahun_terbit' => 'required|digits:4',
'edisi' => 'nullable|string|max:50', 'edisi' => 'required|digits:4',
'deskripsi_fisik' => 'nullable|string|max:100', 'deskripsi_fisik' => 'required|string|max:100',
'nomor_panggil' => 'required|string|max:50', 'nomor_panggil' => 'required|string|max:50|unique:buku,nomor_panggil,' . $id . ',id_buku',
'eksemplar' => 'required|integer|min:1', 'eksemplar' => 'required|integer|min:1',
'id_kategori' => 'required|exists:kategori,id_kategori', 'id_kategori' => 'required|exists:kategori,id_kategori',
'cover' => 'nullable|image|mimes:jpeg,png,jpg|max:2048' 'cover' => 'image|mimes:jpeg,png,jpg,webp|max:2048'
]); ]);
if ($request->hasFile('cover')) { if ($request->hasFile('cover')) {
@ -88,6 +92,10 @@ public function update(Request $request, $id)
$validated['cover'] = $request->file('cover')->store('covers', 'public'); $validated['cover'] = $request->file('cover')->store('covers', 'public');
} }
$koordinat = $this->tentukanKoordinatRak($validated['nomor_panggil']);
$validated['lokasi_x'] = $koordinat['x'];
$validated['lokasi_y'] = $koordinat['y'];
$buku->update($validated); $buku->update($validated);
return redirect()->route('admin.buku.index')->with('success', 'Aset buku berhasil diperbarui.'); return redirect()->route('admin.buku.index')->with('success', 'Aset buku berhasil diperbarui.');
@ -100,4 +108,52 @@ public function destroy($id)
return redirect()->route('admin.buku.index')->with('success', 'Aset buku berhasil dihapus dari sistem.'); return redirect()->route('admin.buku.index')->with('success', 'Aset buku berhasil dihapus dari sistem.');
} }
private function tentukanKoordinatRak($nomor_panggil)
{
if (empty($nomor_panggil)) return ['x' => null, 'y' => null];
$kode_utama = (int) substr(trim(preg_replace('/[^0-9]/', '', $nomor_panggil)), 0, 3);
return match (true) {
$kode_utama >= 0 && $kode_utama <= 99 => match (true) {
$kode_utama <= 19 => ['x' => 12.00, 'y' => 13.00], // Rak 01
$kode_utama <= 50 => ['x' => 17.00, 'y' => 13.00], // Rak 02
default => ['x' => 22.00, 'y' => 13.00], // Rak 03-05
},
$kode_utama >= 100 && $kode_utama <= 199 => match (true) {
$kode_utama <= 150 => ['x' => 35.00, 'y' => 13.00], // Rak 06-10
default => ['x' => 35.00, 'y' => 17.00], // Rak 11-14
},
$kode_utama >= 200 && $kode_utama <= 299 => match (true) {
$kode_utama == 297 => ['x' => 14.00, 'y' => 38.00], // Rak 25-32 (Islam)
default => ['x' => 14.00, 'y' => 30.00], // Rak 15-24
},
$kode_utama >= 300 && $kode_utama <= 399 => match (true) {
$kode_utama <= 330 => ['x' => 38.00, 'y' => 23.00], // Rak 33-36
$kode_utama <= 360 => ['x' => 43.00, 'y' => 23.00], // Rak 37-40
default => ['x' => 48.00, 'y' => 23.00], // Rak 41-44
},
$kode_utama >= 400 && $kode_utama <= 499 => ['x' => 14.00, 'y' => 58.00], // Rak 45
$kode_utama >= 500 && $kode_utama <= 599 => ['x' => 38.00, 'y' => 72.00], // Rak 46-48
$kode_utama >= 600 && $kode_utama <= 699 => match (true) {
$kode_utama <= 610 => ['x' => 28.00, 'y' => 83.00], // Rak 49-53
$kode_utama <= 630 => ['x' => 36.00, 'y' => 83.00], // Rak 54-58
$kode_utama <= 650 => ['x' => 44.00, 'y' => 83.00], // Rak 59-63
default => ['x' => 52.00, 'y' => 83.00], // Rak 64-68
},
$kode_utama >= 700 && $kode_utama <= 799 => match (true) {
$kode_utama <= 739 => ['x' => 82.00, 'y' => 12.00], // Rak 71
$kode_utama <= 769 => ['x' => 86.00, 'y' => 12.00], // Rak 72
$kode_utama <= 789 => ['x' => 82.00, 'y' => 16.00], // Rak 73
default => ['x' => 86.00, 'y' => 16.00], // Rak 74
},
$kode_utama >= 800 && $kode_utama <= 899 => ['x' => 82.00, 'y' => 25.00], // Rak 77-79
$kode_utama >= 900 && $kode_utama <= 999 => match (true) {
$kode_utama <= 919 => ['x' => 82.00, 'y' => 30.00], // Rak 69-70
default => ['x' => 82.00, 'y' => 35.00], // Rak 80-84
},
default => ['x' => null, 'y' => null],
};
}
} }

View File

@ -6,10 +6,8 @@
use App\Models\Buku; use App\Models\Buku;
use App\Models\Anggota; use App\Models\Anggota;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class AdminPeminjamanController extends Controller class AdminPeminjamanController extends Controller
{ {
@ -35,14 +33,14 @@ public function index(Request $request)
$peminjaman = $query->paginate(15)->appends(['search' => $search]); $peminjaman = $query->paginate(15)->appends(['search' => $search]);
$buku = Buku::orderBy('judul', 'asc')->get(); $buku = Buku::orderBy('judul', 'asc')->get();
$anggota = Anggota::orderBy('nama', 'asc')->get(); $anggota = Anggota::latest()->get();
return view('admin.peminjaman.index', compact('peminjaman', 'buku', 'anggota', 'search')); return view('admin.peminjaman.index', compact('peminjaman', 'buku', 'anggota', 'search'));
} }
public function create() public function create()
{ {
$buku = Buku::where('eksemplar', '>', 0)->orderBy('judul', 'asc')->get(); $buku = Buku::where('eksemplar', '>', 0)->orderBy('judul', 'asc')->get();
$anggota = Anggota::orderBy('nama', 'asc')->get(); $anggota = Anggota::latest()->get();
return view('admin.peminjaman.create', compact('buku', 'anggota')); return view('admin.peminjaman.create', compact('buku', 'anggota'));
} }
@ -50,7 +48,7 @@ public function edit($id)
{ {
$peminjaman = Peminjaman::findOrFail($id); $peminjaman = Peminjaman::findOrFail($id);
$buku = Buku::all(); $buku = Buku::all();
$anggota = Anggota::orderBy('nama', 'asc')->get(); $anggota = Anggota::latest()->get();
return view('admin.peminjaman.edit', compact('peminjaman', 'buku', 'anggota')); return view('admin.peminjaman.edit', compact('peminjaman', 'buku', 'anggota'));
} }
@ -60,84 +58,49 @@ public function store(Request $request)
$validated = $request->validate([ $validated = $request->validate([
'id_anggota' => 'required|exists:anggotas,id', 'id_anggota' => 'required|exists:anggotas,id',
'id_buku' => 'required|exists:buku,id_buku', 'id_buku' => 'required|exists:buku,id_buku',
'tanggal_pinjam' => 'required|date', 'id_buku_2' => 'nullable|exists:buku,id_buku|different:id_buku',
'tanggal_kembali' => 'required|date|after_or_equal:tanggal_pinjam', 'tanggal_pinjam' => 'required|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2100-12-31',
'tanggal_kembali' => 'required|date_format:Y-m-d|after_or_equal:tanggal_pinjam|before_or_equal:2100-12-31',
]); ]);
$validated['status_peminjaman'] = 'Dipinjam'; $status = 'Dipinjam';
// 2. Kurangi Stok Buku // 2. Kurangi Stok Buku Pertama & Simpan
$buku = Buku::findOrFail($validated['id_buku']); $buku1 = Buku::findOrFail($validated['id_buku']);
$buku->decrement('eksemplar'); $buku1->decrement('eksemplar');
// 3. Simpan ke Database $peminjaman1 = Peminjaman::create([
$peminjaman = Peminjaman::create($validated); 'id_anggota' => $validated['id_anggota'],
$peminjaman->load(['buku', 'anggota']); 'id_buku' => $validated['id_buku'],
'tanggal_pinjam' => $validated['tanggal_pinjam'],
'tanggal_kembali' => $validated['tanggal_kembali'],
'status_peminjaman' => $status,
]);
$peminjaman1->load(['buku', 'anggota']);
$waSuccess = false; $waSuccess = $this->kirimWaPeminjaman($peminjaman1, false);
// 4. Proses Kirim WA (Format Struk Teks Resmi) // 3. Proses Buku Kedua (Opsional)
try { if (!empty($validated['id_buku_2'])) {
$targetNum = $peminjaman->anggota->no_hp ?? ''; $buku2 = Buku::findOrFail($validated['id_buku_2']);
$fonnteToken = 'vpzqxF2ZGgTGz9F5UbUS'; // Token Fonnte $buku2->decrement('eksemplar');
// Standarisasi Format Nomor ke awalan 62 $peminjaman2 = Peminjaman::create([
if (!empty($targetNum)) { 'id_anggota' => $validated['id_anggota'],
$targetNum = preg_replace('/^0/', '62', trim($targetNum)); 'id_buku' => $validated['id_buku_2'],
'tanggal_pinjam' => $validated['tanggal_pinjam'],
'tanggal_kembali' => $validated['tanggal_kembali'],
'status_peminjaman' => $status,
]);
$peminjaman2->load(['buku', 'anggota']);
$wa2Success = $this->kirimWaPeminjaman($peminjaman2, false);
if ($wa2Success) {
$waSuccess = true;
} }
if (!empty($targetNum) && !empty($fonnteToken)) {
// Merangkai Teks Menyerupai Struk Kertas
$pesanStruk = "🏢 *PERPUSTAKAAN DAERAH JEMBER*\n";
$pesanStruk .= "Jl. Mastrip No. 1, Kabupaten Jember\n";
$pesanStruk .= "===============================\n\n";
$pesanStruk .= "📄 *BUKTI PEMINJAMAN BUKU*\n";
$pesanStruk .= "No. Transaksi : PMJ-{$peminjaman->id_peminjaman}\n";
$pesanStruk .= "Tanggal Cetak : " . \Carbon\Carbon::now()->format('d-m-Y H:i') . "\n\n";
$pesanStruk .= "*DATA PEMINJAM*\n";
$pesanStruk .= "Nama : {$peminjaman->anggota->nama}\n";
$pesanStruk .= "No. HP : {$peminjaman->anggota->no_hp}\n\n";
$pesanStruk .= "*DETAIL BUKU*\n";
$pesanStruk .= "Judul : {$peminjaman->buku->judul}\n";
$pesanStruk .= "Kode Pengembalian : {$peminjaman->buku->bibid}\n";
$pesanStruk .= "Pinjam : " . \Carbon\Carbon::parse($peminjaman->tanggal_pinjam)->format('d F Y') . "\n";
$pesanStruk .= "Kembali: *" . \Carbon\Carbon::parse($peminjaman->tanggal_kembali)->format('d F Y') . "*\n\n";
$pesanStruk .= "===============================\n";
$pesanStruk .= "⚠️ *Catatan:*\n";
$pesanStruk .= "Tunjukkan Kode Pengembalian ke Admin saat pengembalian buku.\n";
$pesanStruk .= "Harap kembalikan buku tepat waktu.\n";
$pesanStruk .= "Denda keterlambatan: Rp 1.000/hari.\n\n";
$pesanStruk .= "Terima kasih atas kunjungan Anda!\n";
$pesanStruk .= "_Sistem Sarakata - TA 2026_";
// Eksekusi Pengiriman via Http Laravel
$response = \Illuminate\Support\Facades\Http::withoutVerifying()->timeout(15)->withHeaders([
'Authorization' => $fonnteToken,
])->post('https://api.fonnte.com/send', [
'target' => $targetNum,
'message' => $pesanStruk,
]);
if ($response->successful() && ($response->json('status') == true)) {
$waSuccess = true;
} else {
\Illuminate\Support\Facades\Log::warning("Fonnte Log: Gagal Terkirim", [
'target' => $targetNum,
'http_status' => $response->status(),
'body' => $response->body()
]);
}
} else {
\Illuminate\Support\Facades\Log::warning('Fonnte Log: Token / No HP kosong', [
'target' => $targetNum ?? 'kosong',
'token_set' => !empty($fonnteToken),
]);
}
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error("Error WA Pengiriman: " . $e->getMessage());
} }
// 5. Notifikasi Kembali ke Layar Admin // 4. Notifikasi Kembali ke Layar Admin
$msg = 'Transaksi peminjaman berhasil dicatat.'; $msg = 'Transaksi peminjaman berhasil dicatat.';
if ($waSuccess) { if ($waSuccess) {
$msg .= ' Struk WA berhasil terkirim kepada Anggota.'; $msg .= ' Struk WA berhasil terkirim kepada Anggota.';
@ -197,42 +160,42 @@ private function prediksiLokasiRakUntukAdmin($nomor_panggil)
return match (true) { return match (true) {
$kode_utama >= 0 && $kode_utama <= 99 => match (true) { $kode_utama >= 0 && $kode_utama <= 99 => match (true) {
$kode_utama <= 19 => ['rak' => 'Rak 01', 'area' => 'Karya Umum'], $kode_utama <= 19 => ['rak' => 'Rak 01', 'area' => 'Karya Umum'],
$kode_utama <= 50 => ['rak' => 'Rak 02', 'area' => 'Karya Umum'], $kode_utama <= 50 => ['rak' => 'Rak 02', 'area' => 'Karya Umum'],
default => ['rak' => 'Rak 03-05', 'area' => 'Karya Umum Lainnya'], default => ['rak' => 'Rak 03-05', 'area' => 'Karya Umum Lainnya'],
}, },
$kode_utama >= 100 && $kode_utama <= 199 => match (true) { $kode_utama >= 100 && $kode_utama <= 199 => match (true) {
$kode_utama <= 150 => ['rak' => 'Rak 06-10', 'area' => 'Filsafat'], $kode_utama <= 150 => ['rak' => 'Rak 06-10', 'area' => 'Filsafat'],
default => ['rak' => 'Rak 11-14', 'area' => 'Psikologi'], default => ['rak' => 'Rak 11-14', 'area' => 'Psikologi'],
}, },
$kode_utama >= 200 && $kode_utama <= 299 => match (true) { $kode_utama >= 200 && $kode_utama <= 299 => match (true) {
$kode_utama == 297 => ['rak' => 'Rak 25-32', 'area' => 'Agama Islam'], $kode_utama == 297 => ['rak' => 'Rak 25-32', 'area' => 'Agama Islam'],
default => ['rak' => 'Rak 15-24', 'area' => 'Agama Umum'], default => ['rak' => 'Rak 15-24', 'area' => 'Agama Umum'],
}, },
$kode_utama >= 300 && $kode_utama <= 399 => match (true) { $kode_utama >= 300 && $kode_utama <= 399 => match (true) {
$kode_utama <= 330 => ['rak' => 'Rak 33-36', 'area' => 'Sosiologi & Politik'], $kode_utama <= 330 => ['rak' => 'Rak 33-36', 'area' => 'Sosiologi & Politik'],
$kode_utama <= 360 => ['rak' => 'Rak 37-40', 'area' => 'Ekonomi & Hukum'], $kode_utama <= 360 => ['rak' => 'Rak 37-40', 'area' => 'Ekonomi & Hukum'],
default => ['rak' => 'Rak 41-44', 'area' => 'Pendidikan & Adat'], default => ['rak' => 'Rak 41-44', 'area' => 'Pendidikan & Adat'],
}, },
$kode_utama >= 400 && $kode_utama <= 499 => ['rak' => 'Rak 45', 'area' => 'Bahasa'], $kode_utama >= 400 && $kode_utama <= 499 => ['rak' => 'Rak 45', 'area' => 'Bahasa'],
$kode_utama >= 500 && $kode_utama <= 599 => ['rak' => 'Rak 46-48', 'area' => 'Ilmu Murni'], $kode_utama >= 500 && $kode_utama <= 599 => ['rak' => 'Rak 46-48', 'area' => 'Ilmu Murni'],
$kode_utama >= 600 && $kode_utama <= 699 => match (true) { $kode_utama >= 600 && $kode_utama <= 699 => match (true) {
$kode_utama <= 610 => ['rak' => 'Rak 49-53', 'area' => 'Kedokteran'], $kode_utama <= 610 => ['rak' => 'Rak 49-53', 'area' => 'Kedokteran'],
$kode_utama <= 630 => ['rak' => 'Rak 54-58', 'area' => 'Teknik'], $kode_utama <= 630 => ['rak' => 'Rak 54-58', 'area' => 'Teknik'],
$kode_utama <= 650 => ['rak' => 'Rak 59-63', 'area' => 'Pertanian'], $kode_utama <= 650 => ['rak' => 'Rak 59-63', 'area' => 'Pertanian'],
default => ['rak' => 'Rak 64-68', 'area' => 'Manajemen Bisnis'], default => ['rak' => 'Rak 64-68', 'area' => 'Manajemen Bisnis'],
}, },
$kode_utama >= 700 && $kode_utama <= 799 => match (true) { $kode_utama >= 700 && $kode_utama <= 799 => match (true) {
$kode_utama <= 739 => ['rak' => 'Rak 71', 'area' => 'Kesenian'], $kode_utama <= 739 => ['rak' => 'Rak 71', 'area' => 'Kesenian'],
$kode_utama <= 769 => ['rak' => 'Rak 72', 'area' => 'Seni Rupa'], $kode_utama <= 769 => ['rak' => 'Rak 72', 'area' => 'Seni Rupa'],
$kode_utama <= 789 => ['rak' => 'Rak 73', 'area' => 'Fotografi/Musik'], $kode_utama <= 789 => ['rak' => 'Rak 73', 'area' => 'Fotografi/Musik'],
default => ['rak' => 'Rak 74', 'area' => 'Olahraga'], default => ['rak' => 'Rak 74', 'area' => 'Olahraga'],
}, },
$kode_utama >= 800 && $kode_utama <= 899 => ['rak' => 'Rak 77-79', 'area' => 'Sastra'], $kode_utama >= 800 && $kode_utama <= 899 => ['rak' => 'Rak 77-79', 'area' => 'Sastra'],
$kode_utama >= 900 && $kode_utama <= 999 => match (true) { $kode_utama >= 900 && $kode_utama <= 999 => match (true) {
$kode_utama <= 919 => ['rak' => 'Rak 69, 70', 'area' => 'Geografi'], $kode_utama <= 919 => ['rak' => 'Rak 69, 70', 'area' => 'Geografi'],
default => ['rak' => 'Rak 80-84', 'area' => 'Sejarah Umum'], default => ['rak' => 'Rak 80-84', 'area' => 'Sejarah Umum'],
}, },
default => ['rak' => 'Rak 75-76', 'area' => 'Koleksi Terbaru'], default => ['rak' => 'Rak 75-76', 'area' => 'Koleksi Terbaru'],
}; };
} }
@ -242,12 +205,12 @@ public function update(Request $request, $id)
$validated = $request->validate([ $validated = $request->validate([
'id_anggota' => 'required|exists:anggotas,id', 'id_anggota' => 'required|exists:anggotas,id',
'id_buku' => 'required|exists:buku,id_buku', 'id_buku' => 'required|exists:buku,id_buku',
'tanggal_pinjam' => 'required|date', 'tanggal_pinjam' => 'required|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2100-12-31',
'tanggal_kembali' => 'required|date|after_or_equal:tanggal_pinjam', 'tanggal_kembali' => 'required|date_format:Y-m-d|after_or_equal:tanggal_pinjam|before_or_equal:2100-12-31',
]); ]);
$peminjaman = Peminjaman::findOrFail($id); $peminjaman = Peminjaman::findOrFail($id);
// If the book changed, adjust stock // If the book changed, adjust stock
if ($peminjaman->id_buku != $validated['id_buku']) { if ($peminjaman->id_buku != $validated['id_buku']) {
if ($peminjaman->status_peminjaman == 'Dipinjam') { if ($peminjaman->status_peminjaman == 'Dipinjam') {
@ -267,7 +230,7 @@ public function update(Request $request, $id)
public function destroy($id) public function destroy($id)
{ {
$peminjaman = Peminjaman::findOrFail($id); $peminjaman = Peminjaman::findOrFail($id);
if ($peminjaman->status_peminjaman == 'Dipinjam' && $peminjaman->buku) { if ($peminjaman->status_peminjaman == 'Dipinjam' && $peminjaman->buku) {
$peminjaman->buku->increment('eksemplar'); $peminjaman->buku->increment('eksemplar');
} }
@ -280,7 +243,7 @@ public function destroy($id)
public function kembalikan($id) public function kembalikan($id)
{ {
$peminjaman = Peminjaman::findOrFail($id); $peminjaman = Peminjaman::findOrFail($id);
$tglTenggat = \Carbon\Carbon::parse($peminjaman->tanggal_kembali)->startOfDay(); $tglTenggat = \Carbon\Carbon::parse($peminjaman->tanggal_kembali)->startOfDay();
$tglSekarang = \Carbon\Carbon::now()->startOfDay(); $tglSekarang = \Carbon\Carbon::now()->startOfDay();
@ -307,4 +270,74 @@ public function kembalikan($id)
return redirect()->back()->with('success', $pesan); return redirect()->back()->with('success', $pesan);
} }
public function resendWa($id)
{
$peminjaman = Peminjaman::with(['buku', 'anggota'])->findOrFail($id);
$success = $this->kirimWaPeminjaman($peminjaman, true);
if ($success) {
return back()->with('success', 'Struk WhatsApp berhasil dikirim ulang ke nomor ' . $peminjaman->anggota->no_hp);
} else {
return back()->with('error', 'Gagal mengirim ulang WA. Silakan cek log Fonnte.');
}
}
private function kirimWaPeminjaman($peminjaman, $isSalinan = false)
{
try {
$targetNum = $peminjaman->anggota->no_hp ?? '';
$fonnteToken = env('FONNTE_TOKEN', 'vpzqxF2ZGgTGz9F5UbUS');
if (!empty($targetNum)) {
$targetNum = preg_replace('/^0/', '62', trim($targetNum));
}
if (!empty($targetNum) && !empty($fonnteToken)) {
$tipeStruk = $isSalinan ? "SALINAN BUKTI PEMINJAMAN" : "BUKTI PEMINJAMAN BUKU";
$pesanStruk = "🏢 *PERPUSTAKAAN DAERAH JEMBER*\n";
$pesanStruk .= "Jl. Mastrip No. 1, Kabupaten Jember\n";
$pesanStruk .= "===============================\n\n";
$pesanStruk .= "📄 *{$tipeStruk}*\n";
$pesanStruk .= "No. Transaksi : PMJ-{$peminjaman->id_peminjaman}\n";
$pesanStruk .= "Tanggal Cetak : " . \Carbon\Carbon::now()->format('d-m-Y H:i') . "\n\n";
$pesanStruk .= "*DATA PEMINJAM*\n";
$pesanStruk .= "Nama : {$peminjaman->anggota->nama}\n";
$pesanStruk .= "No. HP : {$peminjaman->anggota->no_hp}\n\n";
$pesanStruk .= "*DETAIL BUKU*\n";
$pesanStruk .= "Judul : {$peminjaman->buku->judul}\n";
$pesanStruk .= "Kode Pengembalian : {$peminjaman->buku->bibid}\n";
$pesanStruk .= "Pinjam : " . \Carbon\Carbon::parse($peminjaman->tanggal_pinjam)->format('d F Y') . "\n";
$pesanStruk .= "Kembali: *" . \Carbon\Carbon::parse($peminjaman->tanggal_kembali)->format('d F Y') . "*\n\n";
$pesanStruk .= "===============================\n";
$pesanStruk .= "⚠️ *Catatan:*\n";
$pesanStruk .= "Tunjukkan Kode Pengembalian ke Admin saat pengembalian buku.\n";
$pesanStruk .= "Harap kembalikan buku tepat waktu.\n";
$pesanStruk .= "Denda keterlambatan: Rp 1.000/hari.\n\n";
$pesanStruk .= "Terima kasih atas kunjungan Anda!\n";
$pesanStruk .= "_Sistem Sarakata - TA 2026_";
$response = \Illuminate\Support\Facades\Http::withoutVerifying()->timeout(15)->withHeaders([
'Authorization' => $fonnteToken,
])->post('https://api.fonnte.com/send', [
'target' => $targetNum,
'message' => $pesanStruk,
]);
if ($response->successful() && ($response->json('status') == true)) {
return true;
} else {
\Illuminate\Support\Facades\Log::warning("Fonnte Log: Gagal Terkirim", [
'target' => $targetNum,
'http_status' => $response->status(),
'body' => $response->body()
]);
}
}
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error("Error WA Pengiriman: " . $e->getMessage());
}
return false;
}
} }

View File

@ -33,12 +33,12 @@ public function store(Request $request)
'nama' => 'required|string|max:255', 'nama' => 'required|string|max:255',
'jenis_anggota' => 'required|in:Mahasiswa,Siswa,Dosen,Umum', 'jenis_anggota' => 'required|in:Mahasiswa,Siswa,Dosen,Umum',
'no_identitas' => 'required|unique:anggotas,no_identitas', 'no_identitas' => 'required|unique:anggotas,no_identitas',
'no_ktp' => 'required|numeric|digits_between:10,16', 'no_ktp' => 'required|numeric|digits_between:1,16|unique:anggotas,no_ktp',
'prodi' => 'nullable|string|max:255', 'prodi' => 'required_unless:jenis_anggota,Umum|nullable|string|max:255',
'no_hp' => 'required|string|max:20', 'no_hp' => 'required|numeric|digits_between:10,13',
'alamat' => 'required|string', 'alamat' => 'required|string',
'nama_wali' => 'required|string|max:255', 'nama_wali' => 'required|string|max:255',
'no_hp_wali' => 'required|string|max:20', 'no_hp_wali' => 'required|numeric|digits_between:10,13',
'hubungan_wali' => 'required|in:Orang Tua,Saudara,Dosen Wali,Lainnya', 'hubungan_wali' => 'required|in:Orang Tua,Saudara,Dosen Wali,Lainnya',
]); ]);
@ -60,12 +60,12 @@ public function update(Request $request, Anggota $member)
'nama' => 'required|string|max:255', 'nama' => 'required|string|max:255',
'jenis_anggota' => 'required|in:Mahasiswa,Siswa,Dosen,Umum', 'jenis_anggota' => 'required|in:Mahasiswa,Siswa,Dosen,Umum',
'no_identitas' => 'required|unique:anggotas,no_identitas,' . $member->id, 'no_identitas' => 'required|unique:anggotas,no_identitas,' . $member->id,
'no_ktp' => 'required|numeric|digits_between:10,16', 'no_ktp' => 'required|numeric|digits_between:1,16|unique:anggotas,no_ktp,' . $member->id,
'prodi' => 'nullable|string|max:255', 'prodi' => 'required_unless:jenis_anggota,Umum|nullable|string|max:255',
'no_hp' => 'required|string|max:20', 'no_hp' => 'required|numeric|digits_between:10,13',
'alamat' => 'required|string', 'alamat' => 'required|string',
'nama_wali' => 'required|string|max:255', 'nama_wali' => 'required|string|max:255',
'no_hp_wali' => 'required|string|max:20', 'no_hp_wali' => 'required|numeric|digits_between:10,13',
'hubungan_wali' => 'required|in:Orang Tua,Saudara,Dosen Wali,Lainnya', 'hubungan_wali' => 'required|in:Orang Tua,Saudara,Dosen Wali,Lainnya',
]); ]);

View File

@ -21,7 +21,7 @@ public function index()
*/ */
public function showAdminLoginForm() public function showAdminLoginForm()
{ {
return view('auth.login'); // login admin return view('Auth.login'); // login admin
} }
/** /**
@ -53,7 +53,7 @@ public function adminLogin(Request $request)
*/ */
public function showUserLoginForm() public function showUserLoginForm()
{ {
return view('auth.login-user'); // login pengunjung return view('Auth.login-user'); // login pengunjung
} }
/** /**

View File

@ -23,10 +23,10 @@ public function store(Request $request)
'keperluan' => 'required', 'keperluan' => 'required',
]); ]);
$anggota = Anggota::where('no_identitas', $request->no_anggota)->first(); $anggota = Anggota::where('no_ktp', $request->no_anggota)->orWhere('no_identitas', $request->no_anggota)->first();
if (!$anggota) { if (!$anggota) {
return back()->withErrors(['no_anggota' => 'Nomor Anggota tidak ditemukan dalam sistem kami.'])->withInput(); return back()->withErrors(['no_anggota' => 'Nomor KTP / NIK tidak ditemukan dalam sistem kami.'])->withInput();
} }
// Cari user terkait untuk id_user // Cari user terkait untuk id_user
@ -47,10 +47,10 @@ public function store(Request $request)
// Jalur tamu: isi manual // Jalur tamu: isi manual
$request->validate([ $request->validate([
'nama_tamu' => 'required|string|max:255', 'nama_tamu' => 'required|string|max:255',
'email' => 'nullable|email|max:255', 'email' => 'required|email|max:255',
'no_hp' => 'nullable|string|max:20', 'no_hp' => 'required|digits_between:10,13',
'asal_instansi' => 'required|string|max:255', 'asal_instansi' => 'required|string|max:255',
'status' => 'nullable|string|max:255', 'status' => 'required|string|max:255',
'keperluan' => 'required', 'keperluan' => 'required',
]); ]);

View File

@ -8,23 +8,85 @@
class LaporanController extends Controller class LaporanController extends Controller
{ {
public function kehadiran() public function kehadiran(Request $request)
{ {
$bukuTamu = BukuTamu::with('user') $query = BukuTamu::with('user')->orderBy('tanggal_kunjungan', 'desc');
->orderBy('tanggal_kunjungan', 'desc')
->get(); // Filter Bulan & Tahun
if ($request->filled('bulan')) {
$query->whereMonth('tanggal_kunjungan', $request->input('bulan'));
}
if ($request->filled('tahun')) {
$query->whereYear('tanggal_kunjungan', $request->input('tahun'));
}
$bukuTamu = $query->get();
// Slice data berdasarkan rentang baris (Limit Cetak)
if ($request->filled('limit_start') || $request->filled('limit_end')) {
$start = $request->filled('limit_start') ? (int)$request->input('limit_start') : 1;
$offset = max(0, $start - 1);
if ($request->filled('limit_end')) {
$end = (int)$request->input('limit_end');
$length = max(0, $end - $offset);
$bukuTamu = $bukuTamu->slice($offset, $length);
} else {
$bukuTamu = $bukuTamu->slice($offset);
}
}
return view('laporan.kehadiran', compact('bukuTamu')); return view('laporan.kehadiran', compact('bukuTamu'));
} }
public function peminjaman(Request $request) public function peminjaman(Request $request)
{ {
$query = Peminjaman::with(['anggota', 'user', 'buku'])->orderBy('tanggal_pinjam', 'desc'); $query = Peminjaman::with(['anggota', 'user', 'buku.kategori'])->orderBy('tanggal_pinjam', 'desc');
// Optional filtering by month/year if needed (can be added later) // 1. Filter Bulan & Tahun
if ($request->filled('bulan')) {
$query->whereMonth('tanggal_pinjam', $request->input('bulan'));
}
if ($request->filled('tahun')) {
$query->whereYear('tanggal_pinjam', $request->input('tahun'));
}
// 2. Filter Kategori Buku (Berdasarkan DDC / Nomor Panggil)
if ($request->filled('id_kategori')) {
$classDigit = $request->input('id_kategori'); // '0', '1', ..., '9'
$query->whereHas('buku', function ($q) use ($classDigit) {
$q->where('nomor_panggil', 'like', "{$classDigit}%");
});
}
$peminjaman = $query->get(); $peminjaman = $query->get();
return view('laporan.peminjaman', compact('peminjaman')); // Slice data berdasarkan rentang baris (Limit Cetak)
if ($request->filled('limit_start') || $request->filled('limit_end')) {
$start = $request->filled('limit_start') ? (int)$request->input('limit_start') : 1;
$offset = max(0, $start - 1);
if ($request->filled('limit_end')) {
$end = (int)$request->input('limit_end');
$length = max(0, $end - $offset);
$peminjaman = $peminjaman->slice($offset, $length);
} else {
$peminjaman = $peminjaman->slice($offset);
}
}
// Kategori berdasarkan DDC & Lokasi Rak sesuai permintaan user
$categories = [
'0' => '000-099 : Karya Umum (Rak 01-05)',
'1' => '100-199 : Filsafat & Psikologi (Rak 06-14)',
'2' => '200-299 : Agama (Rak 15-32)',
'3' => '300-399 : Ilmu Sosial (Rak 33-44)',
'4' => '400-499 : Bahasa (Rak 45)',
'5' => '500-599 : Ilmu Murni / Sains (Rak 46-48)',
'6' => '600-699 : Ilmu Terapan (Rak 49-68)',
'7' => '700-799 : Kesenian & Olahraga (Rak 71-74)',
'8' => '800-899 : Sastra (Rak 77-79)',
'9' => '900-999 : Geografi & Sejarah (Rak 69-70, Rak 80-84)'
];
return view('laporan.peminjaman', compact('peminjaman', 'categories'));
} }
} }

View File

@ -9,7 +9,6 @@
use Barryvdh\DomPDF\Facade\Pdf; use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class PeminjamanController extends Controller class PeminjamanController extends Controller
{ {
@ -32,13 +31,13 @@ public function index(\Illuminate\Http\Request $request)
$peminjaman = $query->paginate(15)->appends(['search' => $search]); $peminjaman = $query->paginate(15)->appends(['search' => $search]);
$buku = Buku::where('eksemplar', '>', 0)->get(); $buku = Buku::where('eksemplar', '>', 0)->get();
$anggota = Anggota::all(); $anggota = Anggota::latest()->get();
return view('admin.peminjaman.index', compact('peminjaman', 'buku', 'anggota', 'search')); return view('admin.peminjaman.index', compact('peminjaman', 'buku', 'anggota', 'search'));
} }
public function create() public function create()
{ {
$anggota = Anggota::all(); $anggota = Anggota::latest()->get();
$bukus = Buku::all(); $bukus = Buku::all();
return view('admin.peminjaman.create', compact('anggota', 'bukus')); return view('admin.peminjaman.create', compact('anggota', 'bukus'));
} }
@ -110,9 +109,9 @@ public function store(Request $request)
$response = Http::withoutVerifying()->timeout(15)->withHeaders([ $response = Http::withoutVerifying()->timeout(15)->withHeaders([
'Authorization' => $fonnteToken, 'Authorization' => $fonnteToken,
])->attach('file', file_get_contents($filePath), $fileName)->post('https://api.fonnte.com/send', [ ])->attach('file', file_get_contents($filePath), $fileName)->post('https://api.fonnte.com/send', [
'target' => $targetNum, 'target' => $targetNum,
'message' => "Halo Kak *{$peminjaman->anggota->nama}*! 📚✨\n\nTerima kasih telah meminjam buku di *Perpustakaan Daerah Jember*.\n\nBersama pesan ini, kami lampirkan file e-Struk (PDF) untuk peminjaman buku:\n📖 Judul: _{$peminjaman->buku->judul}_\n📅 Batas Kembali: *{$peminjaman->tanggal_kembali}*\n\nMohon simpan dokumen PDF ini sebagai bukti transaksi yang sah. Harap kembalikan buku tepat waktu untuk menghindari denda keterlambatan.\n\nSelamat menikmati waktu membaca Anda!\n\nSalam Literasi,\n*Sistem Sarakata Jember*" 'message' => "Halo Kak *{$peminjaman->anggota->nama}*! 📚✨\n\nTerima kasih telah meminjam buku di *Perpustakaan Daerah Jember*.\n\nBersama pesan ini, kami lampirkan file e-Struk (PDF) untuk peminjaman buku:\n📖 Judul: _{$peminjaman->buku->judul}_\n📅 Batas Kembali: *{$peminjaman->tanggal_kembali}*\n\nMohon simpan dokumen PDF ini sebagai bukti transaksi yang sah. Harap kembalikan buku tepat waktu untuk menghindari denda keterlambatan.\n\nSelamat menikmati waktu membaca Anda!\n\nSalam Literasi,\n*Sistem Sarakata Jember*"
]); ]);
// Cek respon resmi fonnte API // Cek respon resmi fonnte API
if ($response->successful() && ($response->json('status') == true)) { if ($response->successful() && ($response->json('status') == true)) {
@ -128,7 +127,6 @@ public function store(Request $request)
if (file_exists($filePath)) { if (file_exists($filePath)) {
unlink($filePath); unlink($filePath);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
// Bila error (misal: memori PDF kurang, library dompdf crash, timeout dari Fonnte) // Bila error (misal: memori PDF kurang, library dompdf crash, timeout dari Fonnte)
// Error ditangkap agar sistem tidak langsung menampilkan layar error 500 ke User. // Error ditangkap agar sistem tidak langsung menampilkan layar error 500 ke User.
@ -184,4 +182,4 @@ public function indexPengembalian()
return view('admin.peminjaman.pengembalian', compact('pengembalian')); return view('admin.peminjaman.pengembalian', compact('pengembalian'));
} }
} }

View File

@ -51,21 +51,12 @@ public function index(Request $request)
$hasil = []; $hasil = [];
foreach ($semuaBuku as $b) { foreach ($semuaBuku as $b) {
// Hitung nilai Cosine Similarity untuk masing-masing atribut berdasarkan kata kunci yang sudah dibersihkan // Hitung nilai kemiripan dengan algoritma Dynamic Token-wise WTS
$judulSimilarity = $this->calculateSimilarity($processedSearch, $b->judul); $totalScore = $this->calculateDynamicWTS($processedSearch, $b);
$pengarangSimilarity = $this->calculateSimilarity($processedSearch, $b->pengarang);
// Gabungkan penerbit, deskripsi, dan kategori sebagai atribut teks tambahan
$teksTambahan = trim(($b->penerbit ?? '') . ' ' . ($b->deskripsi ?? '') . ' ' . ($b->kategori->nama_kategori ?? ''));
$tambahanSimilarity = $this->calculateSimilarity($processedSearch, $teksTambahan);
// Penyesuaian Bobot WTS (Weighting)
// Judul (70%), Pengarang (20%), Penerbit/Deskripsi/Kategori (10%)
$totalScore = ($judulSimilarity * 0.7) + ($pengarangSimilarity * 0.2) + ($tambahanSimilarity * 0.1);
// 3. Penetapan Batas Relevansi (Threshold) // 3. Penetapan Batas Relevansi (Threshold)
// Hanya tampilkan yang memiliki Total Similarity Score >= 0.3 (30%) // Hanya tampilkan yang memiliki Total Similarity Score >= 0.2 (20%)
if ($totalScore >= 0.3) { if ($totalScore >= 0.2) {
$b->similarity_score = $totalScore; $b->similarity_score = $totalScore;
$hasil[] = $b; $hasil[] = $b;
} }
@ -90,44 +81,77 @@ public function index(Request $request)
} }
/** /**
* Helper untuk menghitung nilai perbandingan kemiripan teks. * [ALGORITMA DYNAMIC TOKEN-WISE WTS]
* Menggunakan pendekatan Cosine Similarity yang dimodifikasi (Query Coverage) * Dibuat khusus untuk memecahkan masalah "Length Penalty" pada Global Search Bar.
* agar panjang judul buku tidak menurunkan skor secara drastis *
* pada pencarian kata kunci pendek. * KONTEKS MASALAH (Untuk Sidang TA):
* Algoritma Cosine Similarity standar membandingkan string secara utuh.
* Saat user menggabungkan "Judul + Pengarang" (contoh: "Aku Anak Mandiri Watiek"),
* panjang vektor query membesar. Saat dicocokkan HANYA ke kolom Judul ("Aku Anak Mandiri"),
* kata "Watiek" dianggap sebagai noise/sampah, sehingga skor kemiripan Judul anjlok.
* Akibatnya total WTS turun dan buku yang relevan malah tenggelam.
*
* SOLUSI (Tokenisasi Dinamis Lintas Atribut):
* 1. Query dipecah menjadi token (kata per kata).
* 2. Setiap token dievaluasi secara independen ke semua atribut buku (Judul, Pengarang, Kategori).
* 3. Token berhak memilih skor kemiripan TERTINGGI dari atribut manapun (Local Max Pooling).
* -> Contoh: Kata "Watiek" tidak akan menghukum skor Judul, melainkan diidentifikasi
* sebagai milik "Pengarang" dan menyumbang skor 0.3.
* 4. Agregasi: Skor semua token dijumlahkan, lalu dibagi jumlah token query.
*
* Hasilnya: "Aku Anak Mandiri Watiek" akan secara dinamis menyatukan bobot Judul (0.6)
* dan Pengarang (0.3), BUKAN saling memberikan penalti!
*
* @param string $query String pencarian dari user
* @param \App\Models\Buku $buku Objek buku dari database
* @return float Skor relevansi akhir
*/ */
private function calculateSimilarity($query, $text) private function calculateDynamicWTS($query, $buku)
{ {
if (empty(trim($text))) return 0; // 1. Ekstraksi dan Pembersihan Token Query
$queryWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $query))); $queryWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $query)));
$textWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $text))); $queryWords = array_filter($queryWords);
if (empty($queryWords)) return 0;
// Frekuensi kata // 2. Ekstraksi Token Atribut Buku
$vecA = array_count_values(array_filter($queryWords)); $judulWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $buku->judul)));
$vecB = array_count_values(array_filter($textWords)); $pengarangWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $buku->pengarang)));
$teksTambahan = trim(($buku->penerbit ?? '') . ' ' . ($buku->deskripsi ?? '') . ' ' . ($buku->kategori->nama_kategori ?? ''));
$tambahanWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $teksTambahan)));
$terms = array_unique(array_merge(array_keys($vecA), array_keys($vecB))); // Konversi ke array asosiatif (Hash Map) untuk efisiensi pencarian O(1)
$judulMap = array_flip(array_filter($judulWords));
$pengarangMap = array_flip(array_filter($pengarangWords));
$tambahanMap = array_flip(array_filter($tambahanWords));
$dotProduct = 0; // 3. Konfigurasi Bobot WTS
$normA = 0; $weights = [
$normB = 0; 'judul' => 0.6,
'pengarang' => 0.3,
'tambahan' => 0.1
];
foreach ($terms as $term) { $totalScore = 0;
$valA = $vecA[$term] ?? 0;
$valB = $vecB[$term] ?? 0;
$dotProduct += ($valA * $valB); // 4. Evaluasi Token-wise (Local Max Pooling)
$normA += pow($valA, 2); foreach ($queryWords as $qWord) {
$normB += pow($valB, 2); // Berikan bobot penuh jika kata ditemukan di atribut bersangkutan
$scoreJudul = isset($judulMap[$qWord]) ? $weights['judul'] : 0;
$scorePengarang = isset($pengarangMap[$qWord]) ? $weights['pengarang'] : 0;
$scoreTambahan = isset($tambahanMap[$qWord]) ? $weights['tambahan'] : 0;
// KUNCI SOLUSI: Kata ini akan menyumbang skor dari atribut yang paling relevan untuknya
$bestScoreForToken = max($scoreJudul, $scorePengarang, $scoreTambahan);
$totalScore += $bestScoreForToken;
} }
if ($normA == 0 || $normB == 0) return 0; // 5. Normalisasi
// Membagi total skor dengan jumlah token query agar term-frequency ternormalisasi,
// Standard Cosine Similarity: $dotProduct / (sqrt($normA) * sqrt($normB)) // namun tetap secara matematis mempertahankan keunggulan bobot absolut antar atribut.
// Kelemahannya: Jika judul buku sangat panjang (normB besar), skor akan anjlok (misal < 0.2) return $totalScore / count($queryWords);
// padahal kata pencariannya cocok 100%.
// Modifikasi menjadi Query Coverage (Pembagi dominan adalah panjang query).
return $dotProduct / $normA;
} }
public function show($id) public function show($id)

View File

@ -10,6 +10,7 @@ class BukuTamu extends Model
use HasFactory; use HasFactory;
protected $table = 'buku_tamu'; protected $table = 'buku_tamu';
protected $primaryKey = 'id_tamu';
public $timestamps = false; public $timestamps = false;
protected $fillable = [ protected $fillable = [

View File

@ -2,24 +2,24 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
/**
* Register any application services.
*/
public function register(): void public function register(): void
{ {
// //
} }
/**
* Bootstrap any application services.
*/
public function boot(): void public function boot(): void
{ {
// Paginator::useTailwind();
// Paksa URL Utama & HTTPS di Production untuk menjamin asset() & route() konsisten
if (config('app.env') !== 'local') {
\Illuminate\Support\Facades\URL::forceRootUrl(config('app.url'));
\Illuminate\Support\Facades\URL::forceScheme('https');
}
} }
} }

31
buat_link.php Normal file
View File

@ -0,0 +1,31 @@
<?php
/**
* Script untuk membuat Symlink (Storage Link) di Shared Hosting
* Jalankan file ini sekali saja melalui browser: https://ta.myhost.id/E31230887/buat_link.php
*/
$target = __DIR__ . '/storage/app/public';
$link = __DIR__ . '/public/storage';
echo "<h2>🔧 Memperbaiki Jembatan Storage (Symlink)</h2>";
echo "<p>Target: <code>$target</code></p>";
echo "<p>Link: <code>$link</code></p>";
if (file_exists($link)) {
echo "<p>⚠️ Menghapus link/folder storage lama...</p>";
if (is_link($link)) {
unlink($link);
} else {
// Jika ternyata folder biasa (bukan link), kita amankan dengan ganti nama
rename($link, $link . '_backup_' . time());
}
}
if (symlink($target, $link)) {
echo "<h3 style='color: green;'>✅ Jembatan Storage Berhasil Dibuat!</h3>";
echo "<p>Sekarang gambar buku Anda seharusnya sudah muncul di website.</p>";
echo "<a href='./'>Kembali ke Website</a>";
} else {
echo "<h3 style='color: red;'>❌ Gagal membuat jembatan symlink.</h3>";
echo "<p>Beberapa hosting membatasi fungsi symlink(). Jika ini terjadi, beri tahu asisten AI Anda untuk menggunakan metode .htaccess redirect.</p>";
}

8
check_link.php Normal file
View File

@ -0,0 +1,8 @@
<?php
$target = __DIR__ . '/public/storage';
echo "Path: $target\n";
echo "Is Link: " . (is_link($target) ? 'YES' : 'NO') . "\n";
echo "Is Dir: " . (is_dir($target) ? 'YES' : 'NO') . "\n";
if (is_link($target)) {
echo "Readlink: " . readlink($target) . "\n";
}

View File

@ -1,42 +0,0 @@
<?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()
{
Schema::create('bukus', function (Blueprint $table) {
$table->id();
$table->string('bibid')->nullable();
$table->string('judul');
$table->string('edisi')->nullable();
$table->string('pengarang');
$table->string('penerbit');
$table->year('tahun_terbit')->nullable();
$table->text('deskripsi_fisik')->nullable();
$table->string('nomor_panggil')->nullable();
$table->string('konten_digital')->nullable();
$table->integer('eksemplar')->default(1);
$table->unsignedBigInteger('id_kategori')->nullable();
$table->string('cover')->nullable();
$table->float('lokasi_x')->nullable();
$table->float('lokasi_y')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('bukus');
}
};

View File

@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
try {
DB::statement('ALTER TABLE peminjaman DROP FOREIGN KEY peminjaman_ibfk_2');
} catch (\Throwable $e) {
}
try {
DB::statement('ALTER TABLE peminjaman DROP FOREIGN KEY fk_peminjaman_buku');
} catch (\Throwable $e) {
}
try {
Schema::table('peminjaman', function (Blueprint $table) {
$table->dropForeign(['id_buku']);
});
} catch (\Throwable $e) {
}
DB::statement('ALTER TABLE peminjaman MODIFY id_buku INT UNSIGNED NOT NULL');
Schema::table('peminjaman', function (Blueprint $table) {
$table->foreign('id_buku')
->references('id_buku')
->on('buku')
->cascadeOnDelete();
});
}
public function down(): void
{
try {
Schema::table('peminjaman', function (Blueprint $table) {
$table->dropForeign(['id_buku']);
});
} catch (\Throwable $e) {
}
DB::statement('ALTER TABLE peminjaman MODIFY id_buku BIGINT UNSIGNED NOT NULL');
Schema::table('peminjaman', function (Blueprint $table) {
$table->foreign('id_buku')
->references('id')
->on('bukus')
->cascadeOnDelete();
});
}
};

View File

@ -0,0 +1,24 @@
<?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
{
\Illuminate\Support\Facades\DB::statement('ALTER TABLE buku_tamu MODIFY id_tamu INT UNSIGNED NOT NULL AUTO_INCREMENT');
}
/**
* Reverse the migrations.
*/
public function down(): void
{
\Illuminate\Support\Facades\DB::statement('ALTER TABLE buku_tamu MODIFY id_tamu INT UNSIGNED NOT NULL');
}
};

13
debug.php Normal file
View File

@ -0,0 +1,13 @@
<?php
// File debug sementara — HAPUS setelah selesai!
echo "<pre>";
echo "DOCUMENT_ROOT: " . $_SERVER['DOCUMENT_ROOT'] . "\n";
echo "REQUEST_URI: " . $_SERVER['REQUEST_URI'] . "\n";
echo "SCRIPT_FILENAME: " . $_SERVER['SCRIPT_FILENAME'] . "\n";
echo "__DIR__: " . __DIR__ . "\n";
echo "\n--- Cek keberadaan file ---\n";
echo "img/denah.webp via __DIR__: " . (file_exists(__DIR__ . '/img/denah.webp') ? '✅ ADA' : '❌ TIDAK ADA') . "\n";
echo "public/img/denah.webp via __DIR__: " . (file_exists(__DIR__ . '/public/img/denah.webp') ? '✅ ADA' : '❌ TIDAK ADA') . "\n";
echo "DOCUMENT_ROOT/img/denah.webp: " . (file_exists($_SERVER['DOCUMENT_ROOT'] . '/img/denah.webp') ? '✅ ADA' : '❌ TIDAK ADA') . "\n";
echo "DOCUMENT_ROOT/E31230887/img/denah.webp: " . (file_exists($_SERVER['DOCUMENT_ROOT'] . '/E31230887/img/denah.webp') ? '✅ ADA' : '❌ TIDAK ADA') . "\n";
echo "</pre>";

8
get_buku_52.php Normal file
View File

@ -0,0 +1,8 @@
<?php
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
$buku = App\Models\Buku::find(52);
echo json_encode($buku->toArray());

12
get_coordinates.php Normal file
View File

@ -0,0 +1,12 @@
<?php
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
$res = [];
foreach(['0','1','2','3','4','5','6','7','8','9'] as $d) {
$b = App\Models\Buku::where('nomor_panggil', 'like', $d.'%')->whereNotNull('lokasi_x')->first();
$res[$d.'00'] = $b ? ['x' => $b->lokasi_x, 'y' => $b->lokasi_y] : null;
}
echo json_encode($res);

4
index.php Normal file
View File

@ -0,0 +1,4 @@
<?php
// Bootstrap Laravel dari folder public/
require __DIR__ . '/public/index.php';

BIN
public/img/denah.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 MiB

After

Width:  |  Height:  |  Size: 6.8 MiB

View File

@ -3,7 +3,7 @@
@section('title', 'Manajemen Admin') @section('title', 'Manajemen Admin')
@section('content') @section('content')
<div x-data="{ isModalOpen: false }" x-cloak> <div x-data="{ isModalOpen: {{ $errors->any() ? 'true' : 'false' }} }" x-cloak>
<x-page-header title="Manajemen Akun Admin"> <x-page-header title="Manajemen Akun Admin">
<x-slot name="actions"> <x-slot name="actions">
<button @click="isModalOpen = true" class="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white px-5 py-2.5 rounded-lg border border-transparent shadow-[0_4px_10px_rgba(37,99,235,0.2)] hover:shadow-[0_6px_15px_rgba(37,99,235,0.3)] transition-all font-semibold flex items-center gap-2 transform hover:translate-y-[-1px]"> <button @click="isModalOpen = true" class="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white px-5 py-2.5 rounded-lg border border-transparent shadow-[0_4px_10px_rgba(37,99,235,0.2)] hover:shadow-[0_6px_15px_rgba(37,99,235,0.3)] transition-all font-semibold flex items-center gap-2 transform hover:translate-y-[-1px]">
@ -15,16 +15,6 @@
<x-alert type="success" :message="session('success')" /> <x-alert type="success" :message="session('success')" />
<x-alert type="error" :message="session('error')" /> <x-alert type="error" :message="session('error')" />
@if ($errors->any())
<div class="bg-red-50 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded-r-xl text-sm shadow-sm">
<p class="font-bold mb-1"><i class="fas fa-exclamation-circle mr-1"></i> Validasi Gagal</p>
<ul class="list-disc ml-5 space-y-1">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<x-card> <x-card>
<x-table> <x-table>
@ -95,27 +85,38 @@ class="relative z-[60] inline-block px-4 pt-5 pb-4 overflow-hidden text-left ali
</button> </button>
</div> </div>
@if ($errors->any())
<div class="bg-red-50 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded-r-xl text-sm shadow-sm">
<p class="font-bold mb-1"><i class="fas fa-exclamation-circle mr-1"></i> Validasi Gagal</p>
<ul class="list-disc ml-5 space-y-1">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<form action="{{ route('admin.akun.store') }}" method="POST" class="space-y-5"> <form action="{{ route('admin.akun.store') }}" method="POST" class="space-y-5">
@csrf @csrf
<div> <div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Nama Lengkap</label> <x-input-label for="name" value="Nama Lengkap" class="mb-1" />
<input type="text" name="name" value="{{ old('name') }}" placeholder="Masukkan nama..." class="w-full rounded-xl border-gray-300 focus:border-blue-500 focus:ring-blue-500 shadow-sm bg-gray-50/50" required> <x-text-input id="name" type="text" name="name" :value="old('name')" placeholder="Masukkan nama..." required />
</div> </div>
<div> <div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Email</label> <x-input-label for="email" value="Email" class="mb-1" />
<input type="email" name="email" value="{{ old('email') }}" placeholder="admin@domain.com" class="w-full rounded-xl border-gray-300 focus:border-blue-500 focus:ring-blue-500 shadow-sm bg-gray-50/50" required> <x-text-input id="email" type="email" name="email" :value="old('email')" placeholder="admin@domain.com" required />
</div> </div>
<div> <div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Password</label> <x-input-label for="password" value="Password" class="mb-1" />
<input type="password" name="password" placeholder="Minimal 8 karakter" class="w-full rounded-xl border-gray-300 focus:border-blue-500 focus:ring-blue-500 shadow-sm bg-gray-50/50" required> <x-text-input id="password" type="password" name="password" placeholder="Minimal 8 karakter" required />
</div> </div>
<div> <div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Konfirmasi Password</label> <x-input-label for="password_confirmation" value="Konfirmasi Password" class="mb-1" />
<input type="password" name="password_confirmation" placeholder="Ulangi password" class="w-full rounded-xl border-gray-300 focus:border-blue-500 focus:ring-blue-500 shadow-sm bg-gray-50/50" required> <x-text-input id="password_confirmation" type="password" name="password_confirmation" placeholder="Ulangi password" required />
</div> </div>
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-100 mt-6"> <div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-100 mt-6">

View File

@ -35,7 +35,7 @@
</div> </div>
<div> <div>
<x-input-label for="id_kategori" value="Kategori" /> <x-input-label for="id_kategori" value="Kategori" />
<select id="id_kategori" name="id_kategori" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"> <select id="id_kategori" name="id_kategori" class="border border-gray-200 bg-gray-50 text-gray-900 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block w-full p-3 shadow-sm hover:bg-white transition-all duration-200 outline-none">
<option value="">Pilih Kategori</option> <option value="">Pilih Kategori</option>
@foreach($kategori as $kat) @foreach($kategori as $kat)
<option value="{{ $kat->id_kategori }}" {{ old('id_kategori') == $kat->id_kategori ? 'selected' : '' }}> <option value="{{ $kat->id_kategori }}" {{ old('id_kategori') == $kat->id_kategori ? 'selected' : '' }}>
@ -65,26 +65,26 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div> <div>
<x-input-label for="penerbit" value="Penerbit" /> <x-input-label for="penerbit" value="Penerbit" />
<x-text-input id="penerbit" name="penerbit" type="text" class="mt-1 block w-full" :value="old('penerbit')" /> <x-text-input id="penerbit" name="penerbit" type="text" class="mt-1 block w-full" :value="old('penerbit')" required />
</div> </div>
<div> <div>
<x-input-label for="tahun_terbit" value="Tahun Terbit" /> <x-input-label for="tahun_terbit" value="Tahun Terbit" />
<x-text-input id="tahun_terbit" name="tahun_terbit" type="number" class="mt-1 block w-full" :value="old('tahun_terbit')" /> <x-text-input id="tahun_terbit" name="tahun_terbit" type="number" class="mt-1 block w-full" :value="old('tahun_terbit')" required />
</div> </div>
<div> <div>
<x-input-label for="edisi" value="Edisi" /> <x-input-label for="edisi" value="Edisi" />
<x-text-input id="edisi" name="edisi" type="text" class="mt-1 block w-full" :value="old('edisi')" /> <x-text-input id="edisi" name="edisi" type="number" class="mt-1 block w-full" :value="old('edisi')" required />
</div> </div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<x-input-label for="deskripsi_fisik" value="Deskripsi Fisik" /> <x-input-label for="deskripsi_fisik" value="Deskripsi Fisik" />
<x-text-input id="deskripsi_fisik" name="deskripsi_fisik" type="text" class="mt-1 block w-full" :value="old('deskripsi_fisik')" /> <x-text-input id="deskripsi_fisik" name="deskripsi_fisik" type="text" class="mt-1 block w-full" :value="old('deskripsi_fisik')" required />
</div> </div>
<div> <div>
<x-input-label for="eksemplar" value="Jumlah Eksemplar" /> <x-input-label for="eksemplar" value="Jumlah Eksemplar" />
<x-text-input id="eksemplar" name="eksemplar" type="number" class="mt-1 block w-full" :value="old('eksemplar')" /> <x-text-input id="eksemplar" name="eksemplar" type="number" class="mt-1 block w-full" :value="old('eksemplar')" required />
</div> </div>
</div> </div>

View File

@ -32,7 +32,7 @@ class="p-6 space-y-6">
<div> <div>
<x-input-label for="id_kategori" value="Kategori" /> <x-input-label for="id_kategori" value="Kategori" />
<select id="id_kategori" name="id_kategori" <select id="id_kategori" name="id_kategori"
class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"> class="border border-gray-200 bg-gray-50 text-gray-900 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block w-full p-3 shadow-sm hover:bg-white transition-all duration-200 outline-none">
<option value="">Pilih Kategori</option> <option value="">Pilih Kategori</option>
@foreach ($kategori as $kat) @foreach ($kategori as $kat)
<option value="{{ $kat->id_kategori }}" <option value="{{ $kat->id_kategori }}"
@ -66,17 +66,17 @@ class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indi
<div> <div>
<x-input-label for="penerbit" value="Penerbit" /> <x-input-label for="penerbit" value="Penerbit" />
<x-text-input id="penerbit" name="penerbit" type="text" class="mt-1 block w-full" <x-text-input id="penerbit" name="penerbit" type="text" class="mt-1 block w-full"
:value="old('penerbit', $buku->penerbit)" /> :value="old('penerbit', $buku->penerbit)" required />
</div> </div>
<div> <div>
<x-input-label for="tahun_terbit" value="Tahun Terbit" /> <x-input-label for="tahun_terbit" value="Tahun Terbit" />
<x-text-input id="tahun_terbit" name="tahun_terbit" type="number" class="mt-1 block w-full" <x-text-input id="tahun_terbit" name="tahun_terbit" type="number" class="mt-1 block w-full"
:value="old('tahun_terbit', $buku->tahun_terbit)" /> :value="old('tahun_terbit', $buku->tahun_terbit)" required />
</div> </div>
<div> <div>
<x-input-label for="edisi" value="Edisi" /> <x-input-label for="edisi" value="Edisi" />
<x-text-input id="edisi" name="edisi" type="text" class="mt-1 block w-full" <x-text-input id="edisi" name="edisi" type="number" class="mt-1 block w-full"
:value="old('edisi', $buku->edisi)" /> :value="old('edisi', $buku->edisi)" required />
</div> </div>
</div> </div>
@ -84,12 +84,12 @@ class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indi
<div> <div>
<x-input-label for="deskripsi_fisik" value="Deskripsi Fisik" /> <x-input-label for="deskripsi_fisik" value="Deskripsi Fisik" />
<x-text-input id="deskripsi_fisik" name="deskripsi_fisik" type="text" class="mt-1 block w-full" <x-text-input id="deskripsi_fisik" name="deskripsi_fisik" type="text" class="mt-1 block w-full"
:value="old('deskripsi_fisik', $buku->deskripsi_fisik)" /> :value="old('deskripsi_fisik', $buku->deskripsi_fisik)" required />
</div> </div>
<div> <div>
<x-input-label for="eksemplar" value="Jumlah Eksemplar" /> <x-input-label for="eksemplar" value="Jumlah Eksemplar" />
<x-text-input id="eksemplar" name="eksemplar" type="number" class="mt-1 block w-full" <x-text-input id="eksemplar" name="eksemplar" type="number" class="mt-1 block w-full"
:value="old('eksemplar', $buku->eksemplar)" /> :value="old('eksemplar', $buku->eksemplar)" required />
</div> </div>
</div> </div>

View File

@ -133,7 +133,7 @@ class="mt-1 block w-full bg-gray-50/50" :value="old('bibid')" required />
<div> <div>
<x-input-label for="id_kategori" value="Kategori" /> <x-input-label for="id_kategori" value="Kategori" />
<select id="id_kategori" name="id_kategori" <select id="id_kategori" name="id_kategori"
class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" class="border border-gray-200 bg-gray-50 text-gray-900 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block w-full p-3 shadow-sm hover:bg-white transition-all duration-200 outline-none"
required> required>
<option value="">Pilih Kategori</option> <option value="">Pilih Kategori</option>
@foreach ($kategori as $kat) @foreach ($kategori as $kat)
@ -169,17 +169,17 @@ class="mt-1 block w-full bg-gray-50/50" :value="old('nomor_panggil')" required /
<div> <div>
<x-input-label for="penerbit" value="Penerbit" /> <x-input-label for="penerbit" value="Penerbit" />
<x-text-input id="penerbit" name="penerbit" type="text" <x-text-input id="penerbit" name="penerbit" type="text"
class="mt-1 block w-full bg-gray-50/50" :value="old('penerbit')" /> class="mt-1 block w-full bg-gray-50/50" :value="old('penerbit')" required />
</div> </div>
<div> <div>
<x-input-label for="tahun_terbit" value="Tahun Terbit" /> <x-input-label for="tahun_terbit" value="Tahun Terbit" />
<x-text-input id="tahun_terbit" name="tahun_terbit" type="number" <x-text-input id="tahun_terbit" name="tahun_terbit" type="number"
class="mt-1 block w-full bg-gray-50/50" :value="old('tahun_terbit')" /> class="mt-1 block w-full bg-gray-50/50" :value="old('tahun_terbit')" required />
</div> </div>
<div> <div>
<x-input-label for="edisi" value="Edisi" /> <x-input-label for="edisi" value="Edisi" />
<x-text-input id="edisi" name="edisi" type="text" <x-text-input id="edisi" name="edisi" type="number"
class="mt-1 block w-full bg-gray-50/50" :value="old('edisi')" /> class="mt-1 block w-full bg-gray-50/50" :value="old('edisi')" required />
</div> </div>
</div> </div>
@ -187,7 +187,7 @@ class="mt-1 block w-full bg-gray-50/50" :value="old('edisi')" />
<div> <div>
<x-input-label for="deskripsi_fisik" value="Deskripsi Fisik" /> <x-input-label for="deskripsi_fisik" value="Deskripsi Fisik" />
<x-text-input id="deskripsi_fisik" name="deskripsi_fisik" type="text" <x-text-input id="deskripsi_fisik" name="deskripsi_fisik" type="text"
class="mt-1 block w-full bg-gray-50/50" :value="old('deskripsi_fisik')" /> class="mt-1 block w-full bg-gray-50/50" :value="old('deskripsi_fisik')" required />
</div> </div>
<div> <div>
<x-input-label for="eksemplar" value="Jumlah Eksemplar" /> <x-input-label for="eksemplar" value="Jumlah Eksemplar" />

View File

@ -1,6 +1,31 @@
@extends('layouts.app') @extends('layouts.app')
@section('content') @section('content')
<!-- CDNs for icons, TomSelect and sweetalert -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
<style>
/* TomSelect Custom Styling matching dashboard */
.ts-control {
border: 1px solid #d1d5db !important;
background-color: #ffffff !important;
border-radius: 0.5rem !important;
padding: 0.75rem 1rem !important;
font-size: 0.875rem !important;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;
}
.ts-control.focus {
border-color: #3b82f6 !important;
box-shadow: 0 0 0 1px #3b82f6 !important;
}
.ts-control input {
font-size: 0.875rem !important;
}
</style>
<div class="container mx-auto px-4 py-8 max-w-2xl"> <div class="container mx-auto px-4 py-8 max-w-2xl">
<div class="bg-white shadow-lg rounded-xl overflow-hidden border border-gray-100"> <div class="bg-white shadow-lg rounded-xl overflow-hidden border border-gray-100">
<div class="p-6 bg-blue-50 border-b border-blue-100"> <div class="p-6 bg-blue-50 border-b border-blue-100">
@ -34,30 +59,43 @@ class="w-full border-gray-300 rounded-lg shadow-sm focus:border-blue-500 focus:r
</select> </select>
</div> </div>
<div> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Pilih Aset Buku (Hanya yang <div>
tersedia)</label> <label class="block text-sm font-medium text-gray-700 mb-2">Pilih Buku Utama (Pindai Barcode / Cari Judul)</label>
<select name="id_buku" <select name="id_buku" id="select-buku-peminjaman" required>
class="w-full border-gray-300 rounded-lg shadow-sm focus:border-blue-500 focus:ring-blue-500"> <option value="">-- Pilih Buku Pertama --</option>
<option value="">-- Pilih Buku --</option> @foreach ($buku as $b)
@foreach ($buku as $b) <option value="{{ $b->id_buku }}" data-bibid="{{ $b->bibid }}" {{ old('id_buku') == $b->id_buku ? 'selected' : '' }}>
<option value="{{ $b->id }}" {{ old('id_buku') == $b->id ? 'selected' : '' }}> {{ $b->bibid }} - {{ $b->judul }} (Stok: {{ $b->eksemplar }})
{{ $b->bibid }} - {{ $b->judul }} (Stok: {{ $b->eksemplar }}) </option>
</option> @endforeach
@endforeach </select>
</select> </div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Pilih Buku Kedua (Opsional)</label>
<select name="id_buku_2" id="select-buku-peminjaman-2">
<option value="">-- Pilih Buku Kedua (Kosongkan jika hanya 1) --</option>
@foreach ($buku as $b)
<option value="{{ $b->id_buku }}" data-bibid="{{ $b->bibid }}" {{ old('id_buku_2') == $b->id_buku ? 'selected' : '' }}>
{{ $b->bibid }} - {{ $b->judul }} (Stok: {{ $b->eksemplar }})
</option>
@endforeach
</select>
</div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Tanggal Pinjam</label> <label class="block text-sm font-medium text-gray-700 mb-2">Tanggal Pinjam</label>
<input type="date" name="tanggal_pinjam" value="{{ old('tanggal_pinjam', date('Y-m-d')) }}" <input type="date" name="tanggal_pinjam" value="{{ old('tanggal_pinjam', date('Y-m-d')) }}"
min="2000-01-01" max="2100-12-31"
class="w-full border-gray-300 rounded-lg shadow-sm focus:border-blue-500 focus:ring-blue-500"> class="w-full border-gray-300 rounded-lg shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Tanggal Kembali (Tenggat)</label> <label class="block text-sm font-medium text-gray-700 mb-2">Tanggal Kembali (Tenggat)</label>
<input type="date" name="tanggal_kembali" <input type="date" name="tanggal_kembali"
value="{{ old('tanggal_kembali', date('Y-m-d', strtotime('+7 days'))) }}" value="{{ old('tanggal_kembali', date('Y-m-d', strtotime('+7 days'))) }}"
min="2000-01-01" max="2100-12-31"
class="w-full border-gray-300 rounded-lg shadow-sm focus:border-blue-500 focus:ring-blue-500"> class="w-full border-gray-300 rounded-lg shadow-sm focus:border-blue-500 focus:ring-blue-500">
</div> </div>
</div> </div>
@ -81,5 +119,131 @@ class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-lg s
btn.innerHTML = 'Memproses...'; btn.innerHTML = 'Memproses...';
btn.classList.add('opacity-50', 'cursor-not-allowed'); btn.classList.add('opacity-50', 'cursor-not-allowed');
}); });
let scanBuffer = "";
let lastKeyTime = Date.now();
// Global barcode scan detection
document.addEventListener('keypress', function(e) {
const target = e.target;
if (target.tagName === 'INPUT' && target.type !== 'submit' && target.type !== 'button' && target.type !== 'checkbox' && target.type !== 'radio') {
if (!target.classList.contains('ts-control') && !target.closest('.ts-wrapper')) {
return;
}
}
if (target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') {
return;
}
const currentTime = Date.now();
if (currentTime - lastKeyTime > 100) {
scanBuffer = "";
}
lastKeyTime = currentTime;
if (e.key === 'Enter') {
if (scanBuffer.length >= 2) {
e.preventDefault();
const cleanCode = scanBuffer.trim();
handleGlobalScan(cleanCode);
scanBuffer = "";
}
} else {
if (e.key.length === 1) {
scanBuffer += e.key;
}
}
});
function handleGlobalScan(bibid) {
const select1 = document.getElementById('select-buku-peminjaman');
const select2 = document.getElementById('select-buku-peminjaman-2');
if (!select1) return;
function findVal(selectEl) {
const options = selectEl.options;
for (let i = 0; i < options.length; i++) {
if (options[i].getAttribute('data-bibid') === bibid) {
return { value: options[i].value, text: options[i].text };
}
}
return null;
}
const item = findVal(select1);
if (!item) {
Swal.fire({
icon: 'error',
title: 'Buku Tidak Ditemukan',
text: 'Kode buku "' + bibid + '" tidak terdaftar atau tidak tersedia.',
timer: 2000,
showConfirmButton: false,
toast: true,
position: 'top-end'
});
return;
}
let targetSelect = null;
if (select1.tomselect && !select1.tomselect.getValue()) {
targetSelect = select1;
} else if (select2 && select2.tomselect && !select2.tomselect.getValue()) {
targetSelect = select2;
} else {
targetSelect = select1;
}
if (targetSelect && targetSelect.tomselect) {
const otherSelect = (targetSelect === select1) ? select2 : select1;
if (otherSelect && otherSelect.tomselect && otherSelect.tomselect.getValue() === item.value) {
Swal.fire({
icon: 'warning',
title: 'Buku Sudah Dipilih',
text: 'Buku ini sudah dipilih pada slot lainnya.',
timer: 2000,
showConfirmButton: false,
toast: true,
position: 'top-end'
});
return;
}
targetSelect.tomselect.setValue(item.value);
Swal.fire({
icon: 'success',
title: 'Buku Berhasil Dipilih',
text: item.text,
timer: 1500,
showConfirmButton: false,
toast: true,
position: 'top-end'
});
}
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize TomSelect for books
const ts1 = new TomSelect('#select-buku-peminjaman', { maxOptions: null });
const ts2 = new TomSelect('#select-buku-peminjaman-2', { maxOptions: null });
// Focus on first book dropdown
setTimeout(() => {
ts1.focus();
}, 100);
// Prevent Enter key from submitting form on text/select inputs
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.tagName !== 'BUTTON' && e.target.type !== 'submit') {
e.preventDefault();
}
});
});
});
</script> </script>
@endsection @endsection

View File

@ -16,6 +16,16 @@
tanggal_pinjam: '{{ old('tanggal_pinjam') ?? '' }}', tanggal_pinjam: '{{ old('tanggal_pinjam') ?? '' }}',
tanggal_kembali: '{{ old('tanggal_kembali') ?? '' }}' tanggal_kembali: '{{ old('tanggal_kembali') ?? '' }}'
}, },
init() {
this.$watch('isModalPeminjamanOpen', value => {
if (value) {
setTimeout(() => {
const ts1 = document.getElementById('select-buku-peminjaman');
if (ts1 && ts1.tomselect) ts1.tomselect.focus();
}, 200);
}
});
},
openEditModal(id, anggotaId, buku, pinjam, kembali) { openEditModal(id, anggotaId, buku, pinjam, kembali) {
this.editData = { id: id, id_anggota: anggotaId, id_buku: buku, tanggal_pinjam: pinjam, tanggal_kembali: kembali }; this.editData = { id: id, id_anggota: anggotaId, id_buku: buku, tanggal_pinjam: pinjam, tanggal_kembali: kembali };
this.isModalEditOpen = true; this.isModalEditOpen = true;
@ -153,6 +163,14 @@ class="text-green-600 font-bold px-3 py-1.5 text-xs bg-green-50 rounded border b
<a href="{{ route('admin.peminjaman.struk', $item->id_peminjaman) }}" target="_blank" <a href="{{ route('admin.peminjaman.struk', $item->id_peminjaman) }}" target="_blank"
class="bg-gray-800 text-white hover:bg-gray-900 px-3 py-1.5 rounded text-xs font-bold transition flex items-center">Cetak class="bg-gray-800 text-white hover:bg-gray-900 px-3 py-1.5 rounded text-xs font-bold transition flex items-center">Cetak
Struk</a> Struk</a>
<form action="{{ route('admin.peminjaman.resend_wa', $item->id_peminjaman) }}" method="POST" class="m-0 p-0 flex items-center">
@csrf
<button type="submit"
class="bg-green-500 text-white hover:bg-green-600 px-2 py-1.5 rounded text-xs font-bold transition flex items-center"
title="Kirim Struk ke WhatsApp">
<i class="fab fa-whatsapp"></i>
</button>
</form>
@if ($item->status_peminjaman == 'Dipinjam') @if ($item->status_peminjaman == 'Dipinjam')
<button type="button" <button type="button"
@click="openEditModal('{{ $item->id_peminjaman }}', '{{ $item->id_anggota ?? $item->id_user }}', '{{ $item->id_buku }}', '{{ \Carbon\Carbon::parse($item->tanggal_pinjam)->format('Y-m-d') }}', '{{ \Carbon\Carbon::parse($item->tanggal_kembali)->format('Y-m-d') }}')" @click="openEditModal('{{ $item->id_peminjaman }}', '{{ $item->id_anggota ?? $item->id_user }}', '{{ $item->id_buku }}', '{{ \Carbon\Carbon::parse($item->tanggal_pinjam)->format('Y-m-d') }}', '{{ \Carbon\Carbon::parse($item->tanggal_kembali)->format('Y-m-d') }}')"
@ -232,7 +250,7 @@ class="w-10 h-10 rounded-full bg-gray-50 hover:bg-red-50 text-gray-400 hover:tex
<div> <div>
<x-input-label value="Pilih Anggota / Member" /> <x-input-label value="Pilih Anggota / Member" />
<select name="id_anggota" x-init="new TomSelect($el, { maxOptions: null })" <select name="id_anggota" x-init="new TomSelect($el, { maxOptions: null })"
class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" class="mt-1 block w-full border-gray-200 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50 p-3"
required> required>
<option value="">-- Pilih Member --</option> <option value="">-- Pilih Member --</option>
@foreach ($anggota as $a) @foreach ($anggota as $a)
@ -244,18 +262,34 @@ class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indi
</select> </select>
</div> </div>
<div> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<x-input-label value="Pilih Buku (Hanya yang tersedia)" /> <div>
<select name="id_buku" x-init="new TomSelect($el, { maxOptions: null })" <x-input-label value="Pilih Buku Utama (Pindai Barcode / Cari Judul)" />
class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" <select name="id_buku" id="select-buku-peminjaman" x-init="new TomSelect($el, { maxOptions: null })"
required> class="mt-1 block w-full border-gray-200 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50 p-3"
<option value="">-- Pilih Buku --</option> required>
@foreach ($buku as $b) <option value="">-- Pilih Buku Pertama --</option>
<option value="{{ $b->id }}" {{ old('id_buku') == $b->id ? 'selected' : '' }}> @foreach ($buku as $b)
{{ $b->bibid }} - {{ $b->judul }} (Stok: {{ $b->eksemplar }}) <option value="{{ $b->id_buku }}" data-bibid="{{ $b->bibid }}"
</option> {{ old('id_buku') == $b->id_buku ? 'selected' : '' }}>
@endforeach {{ $b->bibid }} - {{ $b->judul }} (Stok: {{ $b->eksemplar }})
</select> </option>
@endforeach
</select>
</div>
<div>
<x-input-label value="Pilih Buku Kedua (Opsional)" />
<select name="id_buku_2" id="select-buku-peminjaman-2" x-init="new TomSelect($el, { maxOptions: null })"
class="mt-1 block w-full border-gray-200 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50 p-3">
<option value="">-- Pilih Buku Kedua (Kosongkan jika hanya 1) --</option>
@foreach ($buku as $b)
<option value="{{ $b->id_buku }}" data-bibid="{{ $b->bibid }}"
{{ old('id_buku_2') == $b->id_buku ? 'selected' : '' }}>
{{ $b->bibid }} - {{ $b->judul }} (Stok: {{ $b->eksemplar }})
</option>
@endforeach
</select>
</div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@ -263,12 +297,14 @@ class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indi
<x-input-label value="Tanggal Pinjam" /> <x-input-label value="Tanggal Pinjam" />
<x-text-input type="date" name="tanggal_pinjam" <x-text-input type="date" name="tanggal_pinjam"
value="{{ old('tanggal_pinjam', date('Y-m-d')) }}" value="{{ old('tanggal_pinjam', date('Y-m-d')) }}"
min="2000-01-01" max="2100-12-31"
class="mt-1 block w-full bg-gray-50/50" required /> class="mt-1 block w-full bg-gray-50/50" required />
</div> </div>
<div> <div>
<x-input-label value="Tanggal Kembali (Tenggat)" /> <x-input-label value="Tanggal Kembali (Tenggat)" />
<x-text-input type="date" name="tanggal_kembali" <x-text-input type="date" name="tanggal_kembali"
value="{{ old('tanggal_kembali', date('Y-m-d', strtotime('+7 days'))) }}" value="{{ old('tanggal_kembali', date('Y-m-d', strtotime('+7 days'))) }}"
min="2000-01-01" max="2100-12-31"
class="mt-1 block w-full bg-gray-50/50" required /> class="mt-1 block w-full bg-gray-50/50" required />
</div> </div>
</div> </div>
@ -365,7 +401,7 @@ class="bg-green-50 text-green-700 p-3 rounded-lg text-center font-bold text-xs m
</div> </div>
</template> </template>
<form :action="'/admin/peminjaman/' + kembaliData.id + '/kembali'" method="POST"> <form :action="'{{ url('admin/peminjaman') }}/' + kembaliData.id + '/kembali'" method="POST">
@csrf @csrf
@method('PUT') @method('PUT')
<div class="flex items-center justify-end gap-2 pt-3 border-t border-gray-100"> <div class="flex items-center justify-end gap-2 pt-3 border-t border-gray-100">
@ -413,7 +449,7 @@ class="w-10 h-10 rounded-full bg-gray-50 hover:bg-red-50 text-gray-400 hover:tex
</button> </button>
</div> </div>
<form :action="'/admin/peminjaman/' + editData.id" method="POST" class="space-y-6"> <form :action="'{{ url('admin/peminjaman') }}/' + editData.id" method="POST" class="space-y-6">
@csrf @csrf
@method('PUT') @method('PUT')
<input type="hidden" name="peminjaman_id_edit" :value="editData.id"> <input type="hidden" name="peminjaman_id_edit" :value="editData.id">
@ -422,7 +458,7 @@ class="w-10 h-10 rounded-full bg-gray-50 hover:bg-red-50 text-gray-400 hover:tex
<x-input-label value="Pilih Anggota / Member" /> <x-input-label value="Pilih Anggota / Member" />
<select name="id_anggota" x-model="editData.id_anggota" x-init="let tsA = null; <select name="id_anggota" x-model="editData.id_anggota" x-init="let tsA = null;
$watch('isModalEditOpen', value => { if (value && !tsA && $el.offsetHeight) setTimeout(() => { try { tsA = new TomSelect($el, { maxOptions: null }); } catch (e) {} }, 100) })" $watch('isModalEditOpen', value => { if (value && !tsA && $el.offsetHeight) setTimeout(() => { try { tsA = new TomSelect($el, { maxOptions: null }); } catch (e) {} }, 100) })"
class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" class="mt-1 block w-full border-gray-200 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50 p-3"
required> required>
<option value="">-- Pilih Member --</option> <option value="">-- Pilih Member --</option>
@foreach ($anggota as $a) @foreach ($anggota as $a)
@ -436,11 +472,11 @@ class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indi
<x-input-label value="Pilih Buku" /> <x-input-label value="Pilih Buku" />
<select name="id_buku" x-model="editData.id_buku" x-init="let tsB = null; <select name="id_buku" x-model="editData.id_buku" x-init="let tsB = null;
$watch('isModalEditOpen', value => { if (value && !tsB && $el.offsetHeight) setTimeout(() => { try { tsB = new TomSelect($el, { maxOptions: null }); } catch (e) {} }, 100) })" $watch('isModalEditOpen', value => { if (value && !tsB && $el.offsetHeight) setTimeout(() => { try { tsB = new TomSelect($el, { maxOptions: null }); } catch (e) {} }, 100) })"
class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" class="mt-1 block w-full border-gray-200 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50 p-3"
required> required>
<option value="">-- Pilih Buku --</option> <option value="">-- Pilih Buku --</option>
@foreach ($buku as $b) @foreach ($buku as $b)
<option value="{{ $b->id }}">{{ $b->bibid }} - {{ $b->judul }} <option value="{{ $b->id_buku }}">{{ $b->bibid }} - {{ $b->judul }}
</option> </option>
@endforeach @endforeach
</select> </select>
@ -450,11 +486,13 @@ class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indi
<div> <div>
<x-input-label value="Tanggal Pinjam" /> <x-input-label value="Tanggal Pinjam" />
<x-text-input type="date" name="tanggal_pinjam" x-model="editData.tanggal_pinjam" <x-text-input type="date" name="tanggal_pinjam" x-model="editData.tanggal_pinjam"
min="2000-01-01" max="2100-12-31"
class="mt-1 block w-full bg-gray-50/50" required /> class="mt-1 block w-full bg-gray-50/50" required />
</div> </div>
<div> <div>
<x-input-label value="Tanggal Kembali (Tenggat)" /> <x-input-label value="Tanggal Kembali (Tenggat)" />
<x-text-input type="date" name="tanggal_kembali" x-model="editData.tanggal_kembali" <x-text-input type="date" name="tanggal_kembali" x-model="editData.tanggal_kembali"
min="2000-01-01" max="2100-12-31"
class="mt-1 block w-full bg-gray-50/50" required /> class="mt-1 block w-full bg-gray-50/50" required />
</div> </div>
</div> </div>
@ -504,7 +542,7 @@ class="text-gray-400 hover:text-gray-600 transition-colors">
buku akan otomatis dikembalikan jika status masih dipinjam.</p> buku akan otomatis dikembalikan jika status masih dipinjam.</p>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<form :action="'/admin/peminjaman/' + deleteId" method="POST" class="w-full m-0"> <form :action="'{{ url('admin/peminjaman') }}/' + deleteId" method="POST" class="w-full m-0">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" <button type="submit"
@ -523,3 +561,123 @@ class="w-full py-4 bg-gray-50 hover:bg-gray-100 text-gray-600 rounded-2xl font-b
</div> </div>
</div> </div>
@endsection @endsection
@push('scripts')
<script>
let scanBuffer = "";
let lastKeyTime = Date.now();
// Global barcode scan detection
document.addEventListener('keypress', function(e) {
const target = e.target;
if (target.tagName === 'INPUT' && target.type !== 'submit' && target.type !== 'button' && target.type !== 'checkbox' && target.type !== 'radio') {
if (!target.classList.contains('ts-control') && !target.closest('.ts-wrapper')) {
return;
}
}
if (target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') {
return;
}
const currentTime = Date.now();
if (currentTime - lastKeyTime > 100) {
scanBuffer = "";
}
lastKeyTime = currentTime;
if (e.key === 'Enter') {
if (scanBuffer.length >= 2) {
e.preventDefault();
const cleanCode = scanBuffer.trim();
handleGlobalScan(cleanCode);
scanBuffer = "";
}
} else {
if (e.key.length === 1) {
scanBuffer += e.key;
}
}
});
function handleGlobalScan(bibid) {
const select1 = document.getElementById('select-buku-peminjaman');
const select2 = document.getElementById('select-buku-peminjaman-2');
if (!select1) return;
function findVal(selectEl) {
const options = selectEl.options;
for (let i = 0; i < options.length; i++) {
if (options[i].getAttribute('data-bibid') === bibid) {
return { value: options[i].value, text: options[i].text };
}
}
return null;
}
const item = findVal(select1);
if (!item) {
Swal.fire({
icon: 'error',
title: 'Buku Tidak Ditemukan',
text: 'Kode buku "' + bibid + '" tidak terdaftar atau tidak tersedia.',
timer: 2000,
showConfirmButton: false,
toast: true,
position: 'top-end'
});
return;
}
let targetSelect = null;
if (select1.tomselect && !select1.tomselect.getValue()) {
targetSelect = select1;
} else if (select2 && select2.tomselect && !select2.tomselect.getValue()) {
targetSelect = select2;
} else {
targetSelect = select1;
}
if (targetSelect && targetSelect.tomselect) {
const otherSelect = (targetSelect === select1) ? select2 : select1;
if (otherSelect && otherSelect.tomselect && otherSelect.tomselect.getValue() === item.value) {
Swal.fire({
icon: 'warning',
title: 'Buku Sudah Dipilih',
text: 'Buku ini sudah dipilih pada slot lainnya.',
timer: 2000,
showConfirmButton: false,
toast: true,
position: 'top-end'
});
return;
}
targetSelect.tomselect.setValue(item.value);
Swal.fire({
icon: 'success',
title: 'Buku Berhasil Dipilih',
text: item.text,
timer: 1500,
showConfirmButton: false,
toast: true,
position: 'top-end'
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.tagName !== 'BUTTON' && e.target.type !== 'submit') {
e.preventDefault();
}
});
});
});
</script>
@endpush

View File

@ -1,84 +1,101 @@
@extends('layouts.admin') @extends('layouts.admin')
@section('content') @section('content')
<div class="container mx-auto px-4 py-8 max-w-3xl"> <div class="container mx-auto px-4 py-8 max-w-3xl">
<div class="mb-6"> <div class="mb-6">
<a href="{{ route('admin.peminjaman.index') }}" class="text-blue-600 hover:text-blue-800 font-medium">&larr; Kembali ke Daftar Sirkulasi</a> <a href="{{ route('admin.pengembalian.index') }}" class="text-blue-600 hover:text-blue-800 font-medium">&larr;
</div> Kembali ke Daftar Pengembalian</a>
<div class="bg-white shadow-lg rounded-xl overflow-hidden border border-gray-100 mb-8">
<div class="p-8 bg-gray-900 text-center">
<h1 class="text-2xl font-extrabold text-white mb-2">Pindai Barcode Buku</h1>
<p class="text-gray-400 text-sm mb-6">Arahkan scanner ke label BIBID pada buku yang dikembalikan.</p>
<form action="{{ route('admin.peminjaman.proses_scan') }}" method="POST" class="max-w-md mx-auto relative">
@csrf
<input type="text" name="bibid" autofocus autocomplete="off" placeholder="Scan Barcode di sini..." class="w-full pl-12 pr-4 py-4 text-center text-xl font-mono border-2 border-gray-700 bg-gray-800 text-white rounded-lg shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:bg-gray-900 transition-colors">
<svg class="w-6 h-6 text-gray-500 absolute left-4 top-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm14 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"></path></svg>
</form>
</div> </div>
</div>
@if(session('error')) <div class="bg-white shadow-lg rounded-xl overflow-hidden border border-gray-100 mb-8">
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded shadow-sm" role="alert"> <div class="p-8 bg-gray-900 text-center">
<p class="font-bold">Pemindaian Gagal</p> <h1 class="text-2xl font-extrabold text-white mb-2">Pindai Barcode Buku</h1>
<p>{{ session('error') }}</p> <p class="text-gray-400 text-sm mb-6">Arahkan scanner ke label BIBID pada buku yang dikembalikan.</p>
</div>
@endif
@if(isset($peminjaman)) <form action="{{ route('admin.peminjaman.proses_scan') }}" method="POST" class="max-w-md mx-auto relative">
<div class="bg-white shadow-xl rounded-xl overflow-hidden border-2 border-green-500"> @csrf
<div class="p-6 bg-green-50 border-b border-green-100 flex justify-between items-center"> <input type="text" name="bibid" autofocus autocomplete="off" placeholder="Scan Barcode di sini..."
<h2 class="text-xl font-bold text-green-900">Data Transaksi Ditemukan</h2> class="w-full pl-12 pr-4 py-4 text-center text-xl font-mono border-2 border-gray-700 bg-gray-800 text-white rounded-lg shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:bg-gray-900 transition-colors">
<span class="bg-green-200 text-green-800 px-3 py-1 rounded-full text-xs font-bold uppercase">#{{ $peminjaman->id_peminjaman }}</span> <svg class="w-6 h-6 text-gray-500 absolute left-4 top-4" fill="none" stroke="currentColor"
</div> viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
<div class="p-6 grid grid-cols-1 md:grid-cols-2 gap-8"> d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm14 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z">
<div class="space-y-4"> </path>
<div> </svg>
<span class="block text-xs font-bold text-gray-500 uppercase">Peminjam</span> </form>
<span class="block text-lg font-bold text-gray-900">{{ $peminjaman->anggota?->nama ?? ($peminjaman->user?->name ?? 'Anonim') }}</span>
</div>
<div>
<span class="block text-xs font-bold text-gray-500 uppercase">Aset Buku</span>
<span class="block text-base font-bold text-gray-900">{{ $buku->judul }}</span>
<span class="block text-sm text-gray-600 font-mono">{{ $buku->bibid }}</span>
</div>
<div>
<span class="block text-xs font-bold text-gray-500 uppercase">Tenggat Waktu</span>
<span class="block text-sm font-medium {{ $denda > 0 ? 'text-red-600' : 'text-gray-900' }}">
{{ \Carbon\Carbon::parse($peminjaman->tanggal_kembali)->format('d F Y') }}
</span>
</div>
</div>
<div class="flex flex-col space-y-4">
<div class="bg-blue-900 rounded-xl p-5 text-center text-white shadow-inner flex-1 flex flex-col justify-center border border-blue-800">
<span class="block text-xs font-bold text-blue-300 uppercase tracking-widest mb-2">Panduan Pengembalian Rak Fisik</span>
<span class="block text-4xl font-black mb-1 drop-shadow-md">{{ $lokasi['rak'] }}</span>
<span class="block text-sm text-blue-100 font-medium bg-blue-950/50 py-1 rounded-full w-max mx-auto px-4 mt-2 border border-blue-700/50">{{ $lokasi['area'] }}</span>
</div>
@if($denda > 0)
<div class="bg-red-50 border-2 border-red-200 rounded-xl p-4 text-center">
<span class="block text-xs font-bold text-red-600 uppercase mb-1">Denda Keterlambatan</span>
<span class="block text-2xl font-black text-red-700">Rp {{ number_format($denda, 0, ',', '.') }}</span>
</div>
@endif
</div> </div>
</div> </div>
<div class="p-6 border-t border-gray-100 bg-gray-50 flex justify-end"> @if(session('error'))
<form action="{{ route('admin.peminjaman.kembalikan', $peminjaman->id_peminjaman) }}" method="POST" onsubmit="return confirm('Selesaikan transaksi dan pulihkan stok buku?');"> <div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6 rounded shadow-sm" role="alert">
@csrf <p class="font-bold">Pemindaian Gagal</p>
@method('PUT') <p>{{ session('error') }}</p>
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-4 px-8 rounded-lg shadow-md transition duration-300 w-full md:w-auto text-lg flex items-center gap-2"> </div>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg> @endif
Konfirmasi Pengembalian
</button> @if(isset($peminjaman))
</form> <div class="bg-white shadow-xl rounded-xl overflow-hidden border-2 border-green-500">
</div> <div class="p-6 bg-green-50 border-b border-green-100 flex justify-between items-center">
<h2 class="text-xl font-bold text-green-900">Data Transaksi Ditemukan</h2>
<span
class="bg-green-200 text-green-800 px-3 py-1 rounded-full text-xs font-bold uppercase">#{{ $peminjaman->id_peminjaman }}</span>
</div>
<div class="p-6 grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-4">
<div>
<span class="block text-xs font-bold text-gray-500 uppercase">Peminjam</span>
<span
class="block text-lg font-bold text-gray-900">{{ $peminjaman->anggota?->nama ?? ($peminjaman->user?->name ?? 'Anonim') }}</span>
</div>
<div>
<span class="block text-xs font-bold text-gray-500 uppercase">Aset Buku</span>
<span class="block text-base font-bold text-gray-900">{{ $buku->judul }}</span>
<span class="block text-sm text-gray-600 font-mono">{{ $buku->bibid }}</span>
</div>
<div>
<span class="block text-xs font-bold text-gray-500 uppercase">Tenggat Waktu</span>
<span class="block text-sm font-medium {{ $denda > 0 ? 'text-red-600' : 'text-gray-900' }}">
{{ \Carbon\Carbon::parse($peminjaman->tanggal_kembali)->format('d F Y') }}
</span>
</div>
</div>
<div class="flex flex-col space-y-4">
<div
class="bg-blue-900 rounded-xl p-5 text-center text-white shadow-inner flex-1 flex flex-col justify-center border border-blue-800">
<span class="block text-xs font-bold text-blue-300 uppercase tracking-widest mb-2">Panduan
Pengembalian Rak Fisik</span>
<span class="block text-4xl font-black mb-1 drop-shadow-md">{{ $lokasi['rak'] }}</span>
<span
class="block text-sm text-blue-100 font-medium bg-blue-950/50 py-1 rounded-full w-max mx-auto px-4 mt-2 border border-blue-700/50">{{ $lokasi['area'] }}</span>
</div>
@if($denda > 0)
<div class="bg-red-50 border-2 border-red-200 rounded-xl p-4 text-center">
<span class="block text-xs font-bold text-red-600 uppercase mb-1">Denda Keterlambatan</span>
<span class="block text-2xl font-black text-red-700">Rp
{{ number_format($denda, 0, ',', '.') }}</span>
</div>
@endif
</div>
</div>
<div class="p-6 border-t border-gray-100 bg-gray-50 flex justify-end">
<form action="{{ route('admin.peminjaman.kembalikan', $peminjaman->id_peminjaman) }}" method="POST"
onsubmit="return confirm('Selesaikan transaksi dan pulihkan stok buku?');">
@csrf
@method('PUT')
<button type="submit"
class="bg-green-600 hover:bg-green-700 text-white font-bold py-4 px-8 rounded-lg shadow-md transition duration-300 w-full md:w-auto text-lg flex items-center gap-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
Konfirmasi Pengembalian
</button>
</form>
</div>
</div>
@endif
</div> </div>
@endif @endsection
</div>
@endsection

View File

@ -24,7 +24,7 @@
<div> <div>
<x-input-label for="jenis_anggota" value="Jenis Anggota" /> <x-input-label for="jenis_anggota" value="Jenis Anggota" />
<select id="jenis_anggota" name="jenis_anggota" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm" required onchange="toggleProdi()"> <select id="jenis_anggota" name="jenis_anggota" x-data x-init="new TomSelect($el, { maxOptions: null })" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" required onchange="toggleProdi()">
<option value="" disabled {{ old('jenis_anggota') ? '' : 'selected' }}>-- Pilih Jenis --</option> <option value="" disabled {{ old('jenis_anggota') ? '' : 'selected' }}>-- Pilih Jenis --</option>
<option value="Mahasiswa" {{ old('jenis_anggota') == 'Mahasiswa' ? 'selected' : '' }}>Mahasiswa</option> <option value="Mahasiswa" {{ old('jenis_anggota') == 'Mahasiswa' ? 'selected' : '' }}>Mahasiswa</option>
<option value="Siswa" {{ old('jenis_anggota') == 'Siswa' ? 'selected' : '' }}>Siswa</option> <option value="Siswa" {{ old('jenis_anggota') == 'Siswa' ? 'selected' : '' }}>Siswa</option>
@ -70,7 +70,7 @@
<div class="md:col-span-2"> <div class="md:col-span-2">
<x-input-label for="alamat" value="Alamat Lengkap" /> <x-input-label for="alamat" value="Alamat Lengkap" />
<textarea id="alamat" name="alamat" rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm" placeholder="Masukkan alamat lengkap" required>{{ old('alamat') }}</textarea> <textarea id="alamat" name="alamat" rows="3" class="mt-1 block w-full border border-gray-200 bg-gray-50 text-gray-900 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 p-3 shadow-sm hover:bg-white transition-all duration-200 outline-none" placeholder="Masukkan alamat lengkap" required>{{ old('alamat') }}</textarea>
<x-input-error class="mt-2" :messages="$errors->get('alamat')" /> <x-input-error class="mt-2" :messages="$errors->get('alamat')" />
</div> </div>
</div> </div>
@ -98,7 +98,7 @@
<div> <div>
<x-input-label for="hubungan_wali" value="Hubungan" /> <x-input-label for="hubungan_wali" value="Hubungan" />
<select id="hubungan_wali" name="hubungan_wali" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm" required> <select id="hubungan_wali" name="hubungan_wali" x-data x-init="new TomSelect($el, { maxOptions: null })" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" required>
<option value="" disabled {{ old('hubungan_wali') ? '' : 'selected' }}>-- Pilih Hubungan --</option> <option value="" disabled {{ old('hubungan_wali') ? '' : 'selected' }}>-- Pilih Hubungan --</option>
<option value="Orang Tua" {{ old('hubungan_wali') == 'Orang Tua' ? 'selected' : '' }}>Orang Tua</option> <option value="Orang Tua" {{ old('hubungan_wali') == 'Orang Tua' ? 'selected' : '' }}>Orang Tua</option>
<option value="Saudara" {{ old('hubungan_wali') == 'Saudara' ? 'selected' : '' }}>Saudara</option> <option value="Saudara" {{ old('hubungan_wali') == 'Saudara' ? 'selected' : '' }}>Saudara</option>

View File

@ -25,7 +25,7 @@
<div> <div>
<x-input-label for="jenis_anggota" value="Jenis Anggota" /> <x-input-label for="jenis_anggota" value="Jenis Anggota" />
<select id="jenis_anggota" name="jenis_anggota" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm" required onchange="toggleProdi()"> <select id="jenis_anggota" name="jenis_anggota" x-data x-init="new TomSelect($el, { maxOptions: null })" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" required onchange="toggleProdi()">
<option value="" disabled>-- Pilih Jenis --</option> <option value="" disabled>-- Pilih Jenis --</option>
@foreach(['Mahasiswa', 'Siswa', 'Dosen', 'Umum'] as $jenis) @foreach(['Mahasiswa', 'Siswa', 'Dosen', 'Umum'] as $jenis)
<option value="{{ $jenis }}" {{ old('jenis_anggota', $anggota->jenis_anggota) == $jenis ? 'selected' : '' }}>{{ $jenis }}</option> <option value="{{ $jenis }}" {{ old('jenis_anggota', $anggota->jenis_anggota) == $jenis ? 'selected' : '' }}>{{ $jenis }}</option>
@ -64,13 +64,13 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<x-input-label for="no_hp" value="No. HP" /> <x-input-label for="no_hp" value="No. HP" />
<x-text-input id="no_hp" name="no_hp" type="text" class="mt-1 block w-full" :value="old('no_hp', $anggota->no_hp)" required /> <x-text-input id="no_hp" name="no_hp" type="number" class="mt-1 block w-full" :value="old('no_hp', $anggota->no_hp)" required />
<x-input-error class="mt-2" :messages="$errors->get('no_hp')" /> <x-input-error class="mt-2" :messages="$errors->get('no_hp')" />
</div> </div>
<div class="md:col-span-2"> <div class="md:col-span-2">
<x-input-label for="alamat" value="Alamat Lengkap" /> <x-input-label for="alamat" value="Alamat Lengkap" />
<textarea id="alamat" name="alamat" rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm" required>{{ old('alamat', $anggota->alamat) }}</textarea> <textarea id="alamat" name="alamat" rows="3" class="mt-1 block w-full border border-gray-200 bg-gray-50 text-gray-900 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 p-3 shadow-sm hover:bg-white transition-all duration-200 outline-none" required>{{ old('alamat', $anggota->alamat) }}</textarea>
<x-input-error class="mt-2" :messages="$errors->get('alamat')" /> <x-input-error class="mt-2" :messages="$errors->get('alamat')" />
</div> </div>
</div> </div>
@ -92,13 +92,13 @@
<div> <div>
<x-input-label for="no_hp_wali" value="No. HP Wali" /> <x-input-label for="no_hp_wali" value="No. HP Wali" />
<x-text-input id="no_hp_wali" name="no_hp_wali" type="text" class="mt-1 block w-full" :value="old('no_hp_wali', $anggota->no_hp_wali)" required /> <x-text-input id="no_hp_wali" name="no_hp_wali" type="number" class="mt-1 block w-full" :value="old('no_hp_wali', $anggota->no_hp_wali)" required />
<x-input-error class="mt-2" :messages="$errors->get('no_hp_wali')" /> <x-input-error class="mt-2" :messages="$errors->get('no_hp_wali')" />
</div> </div>
<div> <div>
<x-input-label for="hubungan_wali" value="Hubungan" /> <x-input-label for="hubungan_wali" value="Hubungan" />
<select id="hubungan_wali" name="hubungan_wali" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm" required> <select id="hubungan_wali" name="hubungan_wali" x-data x-init="new TomSelect($el, { maxOptions: null })" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" required>
<option value="" disabled>-- Pilih Hubungan --</option> <option value="" disabled>-- Pilih Hubungan --</option>
@foreach(['Orang Tua', 'Saudara', 'Dosen Wali', 'Lainnya'] as $hub) @foreach(['Orang Tua', 'Saudara', 'Dosen Wali', 'Lainnya'] as $hub)
<option value="{{ $hub }}" {{ old('hubungan_wali', $anggota->hubungan_wali) == $hub ? 'selected' : '' }}>{{ $hub }}</option> <option value="{{ $hub }}" {{ old('hubungan_wali', $anggota->hubungan_wali) == $hub ? 'selected' : '' }}>{{ $hub }}</option>
@ -131,8 +131,10 @@ function toggleProdi() {
if (jenis === 'Umum') { if (jenis === 'Umum') {
prodiField.style.display = 'none'; prodiField.style.display = 'none';
document.getElementById('prodi').value = ''; document.getElementById('prodi').value = '';
document.getElementById('prodi').removeAttribute('required');
} else { } else {
prodiField.style.display = 'block'; prodiField.style.display = 'block';
document.getElementById('prodi').setAttribute('required', 'required');
} }
// Logika Dynamic Form untuk Identitas Siswa / Pelajar // Logika Dynamic Form untuk Identitas Siswa / Pelajar

View File

@ -6,7 +6,7 @@
<div x-data="{ isModalAnggotaOpen: {{ ($errors->any() || request('add') == 'true') ? 'true' : 'false' }} }"> <div x-data="{ isModalAnggotaOpen: {{ ($errors->any() || request('add') == 'true') ? 'true' : 'false' }} }">
<x-page-header title="Data Anggota"> <x-page-header title="Data Anggota">
<x-slot name="actions"> <x-slot name="actions">
<button @click="isModalAnggotaOpen = true" class="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white px-5 py-2.5 rounded-xl shadow-lg shadow-emerald-500/30 transition-all font-semibold flex items-center gap-2 transform hover:scale-105"> <button @click="isModalAnggotaOpen = true" class="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white px-5 py-2.5 rounded-xl shadow-lg shadow-blue-500/30 transition-all font-semibold flex items-center gap-2 transform hover:scale-105">
<i class="fas fa-user-plus"></i> Tambah Anggota <i class="fas fa-user-plus"></i> Tambah Anggota
</button> </button>
</x-slot> </x-slot>
@ -164,7 +164,7 @@ class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transi
<div> <div>
<x-input-label for="jenis_anggota" value="Jenis Anggota" /> <x-input-label for="jenis_anggota" value="Jenis Anggota" />
<select id="jenis_anggota" name="jenis_anggota" x-model="jenisAnggota" class="mt-1 block w-full rounded-xl border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm bg-gray-50/50" required> <select id="jenis_anggota" name="jenis_anggota" x-model="jenisAnggota" x-init="new TomSelect($el, { maxOptions: null })" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" required>
<option value="" disabled>-- Pilih Jenis --</option> <option value="" disabled>-- Pilih Jenis --</option>
<option value="Mahasiswa">Mahasiswa</option> <option value="Mahasiswa">Mahasiswa</option>
<option value="Siswa">Siswa</option> <option value="Siswa">Siswa</option>
@ -185,7 +185,7 @@ class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transi
<div x-show="jenisAnggota !== 'Umum'" x-transition> <div x-show="jenisAnggota !== 'Umum'" x-transition>
<x-input-label for="prodi" value="Program Studi / Jurusan" /> <x-input-label for="prodi" value="Program Studi / Jurusan" />
<x-text-input id="prodi" name="prodi" type="text" class="mt-1 block w-full bg-gray-50/50" :value="old('prodi')" placeholder="Contoh: Teknik Informatika" /> <x-text-input id="prodi" name="prodi" type="text" class="mt-1 block w-full bg-gray-50/50" :value="old('prodi')" placeholder="Contoh: Teknik Informatika" x-bind:required="jenisAnggota !== 'Umum'" />
</div> </div>
</div> </div>
</div> </div>
@ -199,7 +199,7 @@ class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transi
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<x-input-label for="no_hp" value="No. HP" /> <x-input-label for="no_hp" value="No. HP" />
<x-text-input id="no_hp" name="no_hp" type="text" class="mt-1 block w-full bg-gray-50/50" :value="old('no_hp')" placeholder="08xxxxxxxxx" required /> <x-text-input id="no_hp" name="no_hp" type="number" class="mt-1 block w-full bg-gray-50/50" :value="old('no_hp')" placeholder="08xxxxxxxxx" required />
</div> </div>
<div class="md:col-span-2"> <div class="md:col-span-2">
@ -224,12 +224,12 @@ class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transi
<div> <div>
<x-input-label for="no_hp_wali" value="No. HP Wali" /> <x-input-label for="no_hp_wali" value="No. HP Wali" />
<x-text-input id="no_hp_wali" name="no_hp_wali" type="text" class="mt-1 block w-full bg-gray-50/50" :value="old('no_hp_wali')" placeholder="08xxxxxxxxx" required /> <x-text-input id="no_hp_wali" name="no_hp_wali" type="number" class="mt-1 block w-full bg-gray-50/50" :value="old('no_hp_wali')" placeholder="08xxxxxxxxx" required />
</div> </div>
<div> <div>
<x-input-label for="hubungan_wali" value="Hubungan" /> <x-input-label for="hubungan_wali" value="Hubungan" />
<select id="hubungan_wali" name="hubungan_wali" class="mt-1 block w-full rounded-xl border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm bg-gray-50/50" required> <select id="hubungan_wali" name="hubungan_wali" x-init="new TomSelect($el, { maxOptions: null })" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-xl shadow-sm bg-gray-50/50" required>
<option value="" disabled {{ old('hubungan_wali') ? '' : 'selected' }}>-- Pilih Hubungan --</option> <option value="" disabled {{ old('hubungan_wali') ? '' : 'selected' }}>-- Pilih Hubungan --</option>
<option value="Orang Tua" {{ old('hubungan_wali') == 'Orang Tua' ? 'selected' : '' }}>Orang Tua</option> <option value="Orang Tua" {{ old('hubungan_wali') == 'Orang Tua' ? 'selected' : '' }}>Orang Tua</option>
<option value="Saudara" {{ old('hubungan_wali') == 'Saudara' ? 'selected' : '' }}>Saudara</option> <option value="Saudara" {{ old('hubungan_wali') == 'Saudara' ? 'selected' : '' }}>Saudara</option>
@ -244,7 +244,7 @@ class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transi
<button type="button" @click="isModalAnggotaOpen = false" class="px-5 py-2.5 text-gray-600 bg-gray-100 hover:bg-gray-200 hover:text-gray-900 rounded-xl font-semibold transition-colors"> <button type="button" @click="isModalAnggotaOpen = false" class="px-5 py-2.5 text-gray-600 bg-gray-100 hover:bg-gray-200 hover:text-gray-900 rounded-xl font-semibold transition-colors">
Batal Batal
</button> </button>
<button type="submit" class="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white px-6 py-2.5 rounded-xl font-bold shadow-lg shadow-emerald-500/30 transition-all transform hover:scale-105"> <button type="submit" class="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white px-6 py-2.5 rounded-xl font-bold shadow-lg shadow-blue-500/30 transition-all transform hover:scale-105">
<i class="fas fa-save mr-2"></i> Simpan Anggota <i class="fas fa-save mr-2"></i> Simpan Anggota
</button> </button>
</div> </div>

View File

@ -26,6 +26,31 @@
@keyframes blob { 0% { transform: translate(0,0) scale(1); } 33% { transform: translate(30px,-50px) scale(1.1); } 66% { transform: translate(-20px,20px) scale(0.9); } 100% { transform: translate(0,0) scale(1); } } @keyframes blob { 0% { transform: translate(0,0) scale(1); } 33% { transform: translate(30px,-50px) scale(1.1); } 66% { transform: translate(-20px,20px) scale(0.9); } 100% { transform: translate(0,0) scale(1); } }
.animate-blob { animation: blob 7s infinite; } .animate-blob { animation: blob 7s infinite; }
.animation-delay-2000 { animation-delay: 2s; } .animation-delay-2000 { animation-delay: 2s; }
/* Hide spin buttons for number input */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style> </style>
</head> </head>
@ -79,8 +104,12 @@
{{-- RIGHT PANEL (Form) --}} {{-- RIGHT PANEL (Form) --}}
<div class="w-full md:w-7/12 bg-gradient-to-br from-primary-900 via-primary-800 to-primary-900 relative flex flex-col p-8 md:p-12"> <div class="w-full md:w-7/12 bg-gradient-to-br from-primary-900 via-primary-800 to-primary-900 relative flex flex-col p-8 md:p-12">
<div class="mb-6 pb-4"> @php $activeTipe = old('tipe', request('tipe', 'member')); @endphp
<h2 class="text-2xl font-bold text-white tracking-wide mb-2">Buku Tamu</h2>
<div class="mb-5 pb-3">
<h2 id="form-title" class="text-2xl font-bold text-white tracking-wide mb-1">
{{ $activeTipe == 'member' ? 'Buku Tamu Anggota' : 'Buku Tamu Pengunjung Umum' }}
</h2>
<p class="text-primary-300/60 text-sm">Isi data kunjungan Anda hari ini.</p> <p class="text-primary-300/60 text-sm">Isi data kunjungan Anda hari ini.</p>
</div> </div>
@ -96,65 +125,73 @@
</div> </div>
@endif @endif
{{-- Toggle --}} {{-- Toggle --}}
<div class="flex gap-3 mb-6"> <div class="flex gap-3 mb-6">
<label class="toggle-option flex-1 flex items-center gap-3 p-4 rounded-xl border border-white/10 active" id="toggle-member" onclick="switchMode('member')"> <label class="toggle-option flex-1 flex items-center gap-3 p-4 rounded-xl border border-white/10 {{ $activeTipe == 'member' ? 'active' : '' }}" id="toggle-member" onclick="switchMode('member')">
<input type="radio" name="mode_toggle" value="member" checked class="hidden"> <input type="radio" name="mode_toggle" value="member" {{ $activeTipe == 'member' ? 'checked' : '' }} class="hidden">
<div class="w-5 h-5 rounded-full border-2 border-white/40 flex items-center justify-center" id="radio-member"> <div class="w-5 h-5 rounded-full border-2 border-white/40 flex items-center justify-center" id="radio-member">
@if($activeTipe == 'member')
<div class="w-3 h-3 rounded-full bg-primary-400"></div> <div class="w-3 h-3 rounded-full bg-primary-400"></div>
@endif
</div> </div>
<div> <div>
<p class="text-white text-sm font-semibold">Anggota</p> <p class="text-white text-sm font-semibold">Anggota</p>
<p class="text-primary-300/50 text-[10px]">Sudah punya No. Anggota</p> <p class="text-primary-300/50 text-[10px]">Punya NIK / No. Anggota</p>
</div> </div>
</label> </label>
<label class="toggle-option flex-1 flex items-center gap-3 p-4 rounded-xl border border-white/10" id="toggle-tamu" onclick="switchMode('tamu')"> <label class="toggle-option flex-1 flex items-center gap-3 p-4 rounded-xl border border-white/10 {{ $activeTipe == 'tamu' ? 'active' : '' }}" id="toggle-tamu" onclick="switchMode('tamu')">
<input type="radio" name="mode_toggle" value="tamu" class="hidden"> <input type="radio" name="mode_toggle" value="tamu" {{ $activeTipe == 'tamu' ? 'checked' : '' }} class="hidden">
<div class="w-5 h-5 rounded-full border-2 border-white/40 flex items-center justify-center" id="radio-tamu"></div> <div class="w-5 h-5 rounded-full border-2 border-white/40 flex items-center justify-center" id="radio-tamu">
@if($activeTipe == 'tamu')
<div class="w-3 h-3 rounded-full bg-primary-400"></div>
@endif
</div>
<div> <div>
<p class="text-white text-sm font-semibold">Pengunjung</p> <p class="text-white text-sm font-semibold">Pengunjung</p>
<p class="text-primary-300/50 text-[10px]">Belum punya No. Anggota</p> <p class="text-primary-300/50 text-[10px]">Belum punya NIK / Anggota</p>
</div> </div>
</label> </label>
</div> </div>
<form method="POST" action="{{ route('buku_tamu.store') }}" class="space-y-6" id="buku-tamu-form"> <form method="POST" action="{{ route('buku_tamu.store') }}" class="space-y-6" id="buku-tamu-form">
@csrf @csrf
<input type="hidden" name="tipe" id="tipe-input" value="member"> <input type="hidden" name="tipe" id="tipe-input" value="{{ $activeTipe }}">
<div id="form-member" class="space-y-5"> <div id="form-member" class="space-y-5" style="{{ $activeTipe == 'member' ? '' : 'display: none;' }}">
<div> <div>
<label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">No. Anggota</label> <label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">NIK / No. KTP / No. Anggota</label>
<input type="text" name="no_anggota" placeholder="Masukkan Nomor Anggota" <input type="text" name="no_anggota" placeholder="Masukkan 16 digit NIK atau Nomor Anggota"
class="w-full glass-input rounded-xl px-5 py-4 text-sm" value="{{ old('no_anggota') }}"> class="w-full glass-input rounded-xl px-5 py-4 text-sm" value="{{ old('no_anggota') }}" {{ $activeTipe == 'member' ? 'required' : '' }}>
</div> </div>
</div> </div>
<div id="form-tamu" class="space-y-5" style="display: none;"> <div id="form-tamu" class="space-y-5" style="{{ $activeTipe == 'tamu' ? '' : 'display: none;' }}">
<div> <div>
<label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">Nama Lengkap</label> <label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">Nama Lengkap</label>
<input type="text" name="nama_tamu" placeholder="Isi nama lengkap anda" <input type="text" name="nama_tamu" placeholder="Isi nama lengkap anda"
class="w-full glass-input rounded-xl px-5 py-4 text-sm" value="{{ old('nama_tamu') }}"> class="w-full glass-input rounded-xl px-5 py-4 text-sm" value="{{ old('nama_tamu') }}" {{ $activeTipe == 'tamu' ? 'required' : '' }}>
</div> </div>
<div> <div>
<label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">Email</label> <label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">Email</label>
<input type="email" name="email" placeholder="Isi email anda" <input type="email" name="email" placeholder="Isi email anda"
class="w-full glass-input rounded-xl px-5 py-4 text-sm" value="{{ old('email') }}"> class="w-full glass-input rounded-xl px-5 py-4 text-sm" value="{{ old('email') }}" {{ $activeTipe == 'tamu' ? 'required' : '' }}>
</div> </div>
<div> <div>
<label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">No HP</label> <label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">No HP</label>
<input type="text" name="no_hp" placeholder="+62" <input type="number" name="no_hp" placeholder="08..."
class="w-full glass-input rounded-xl px-5 py-4 text-sm" value="{{ old('no_hp') }}"> class="w-full glass-input rounded-xl px-5 py-4 text-sm" value="{{ old('no_hp') }}" {{ $activeTipe == 'tamu' ? 'required' : '' }} oninput="if(this.value.length > 13) this.value = this.value.slice(0, 13);">
</div> </div>
<div> <div>
<label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">Asal Instansi/ Sekolah / Kampus</label> <label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">Asal Instansi/ Sekolah / Kampus</label>
<input type="text" name="asal_instansi" placeholder="Politeknik Negeri Jember" <input type="text" name="asal_instansi" placeholder="Politeknik Negeri Jember"
class="w-full glass-input rounded-xl px-5 py-4 text-sm" value="{{ old('asal_instansi') }}"> class="w-full glass-input rounded-xl px-5 py-4 text-sm" value="{{ old('asal_instansi') }}" {{ $activeTipe == 'tamu' ? 'required' : '' }}>
</div> </div>
<div> <div>
<label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">Status</label> <label class="block text-xs font-semibold text-primary-200/70 mb-2 ml-1 uppercase tracking-wider">Status</label>
<div class="relative"> <div class="relative">
<select name="status" class="w-full glass-input rounded-xl px-5 py-4 text-sm appearance-none cursor-pointer"> <select name="status" class="w-full glass-input rounded-xl px-5 py-4 text-sm appearance-none cursor-pointer" {{ $activeTipe == 'tamu' ? 'required' : '' }}>
<option value="" disabled selected>Pilih Status</option> <option value="" disabled selected>Pilih Status</option>
<option value="Siswa" {{ old('status') == 'Siswa' ? 'selected' : '' }}>Siswa</option> <option value="Siswa" {{ old('status') == 'Siswa' ? 'selected' : '' }}>Siswa</option>
<option value="Mahasiswa" {{ old('status') == 'Mahasiswa' ? 'selected' : '' }}>Mahasiswa</option> <option value="Mahasiswa" {{ old('status') == 'Mahasiswa' ? 'selected' : '' }}>Mahasiswa</option>
@ -204,14 +241,7 @@ class="group w-full py-4 bg-gradient-to-r from-primary-400 to-primary-500 hover:
</button> </button>
</div> </div>
<div class="text-center pt-2">
<p class="text-sm text-primary-200/40">
Belum punya akun anggota?
<a href="{{ route('register') }}" class="text-white font-semibold hover:text-primary-300 transition underline decoration-primary-400/30 underline-offset-4">
Daftar Sekarang
</a>
</p>
</div>
</form> </form>
</div> </div>
</div> </div>
@ -226,6 +256,10 @@ function switchMode(mode) {
const radioMember = document.getElementById('radio-member'); const radioMember = document.getElementById('radio-member');
const radioTamu = document.getElementById('radio-tamu'); const radioTamu = document.getElementById('radio-tamu');
const tipeInput = document.getElementById('tipe-input'); const tipeInput = document.getElementById('tipe-input');
const formTitle = document.getElementById('form-title');
const reqMember = formMember.querySelectorAll('input, select');
const reqTamu = formTamu.querySelectorAll('input, select');
if (mode === 'member') { if (mode === 'member') {
formMember.style.display = 'block'; formMember.style.display = 'block';
@ -235,6 +269,10 @@ function switchMode(mode) {
radioMember.innerHTML = '<div class="w-3 h-3 rounded-full bg-primary-400"></div>'; radioMember.innerHTML = '<div class="w-3 h-3 rounded-full bg-primary-400"></div>';
radioTamu.innerHTML = ''; radioTamu.innerHTML = '';
tipeInput.value = 'member'; tipeInput.value = 'member';
if(formTitle) formTitle.innerText = 'Buku Tamu Anggota';
reqMember.forEach(el => el.setAttribute('required', 'required'));
reqTamu.forEach(el => el.removeAttribute('required'));
} else { } else {
formMember.style.display = 'none'; formMember.style.display = 'none';
formTamu.style.display = 'block'; formTamu.style.display = 'block';
@ -243,6 +281,10 @@ function switchMode(mode) {
radioMember.innerHTML = ''; radioMember.innerHTML = '';
radioTamu.innerHTML = '<div class="w-3 h-3 rounded-full bg-primary-400"></div>'; radioTamu.innerHTML = '<div class="w-3 h-3 rounded-full bg-primary-400"></div>';
tipeInput.value = 'tamu'; tipeInput.value = 'tamu';
if(formTitle) formTitle.innerText = 'Buku Tamu Pengunjung Umum';
reqMember.forEach(el => el.removeAttribute('required'));
reqTamu.forEach(el => el.setAttribute('required', 'required'));
} }
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<div {{ $attributes->merge(['class' => 'bg-white overflow-hidden shadow-sm sm:rounded-lg']) }}> <div {{ $attributes->merge(['class' => 'bg-white overflow-hidden shadow-[0_8px_30px_rgb(0,0,0,0.04)] sm:rounded-2xl border border-gray-100']) }}>
<div class="p-6 text-gray-900"> <div class="p-8 text-gray-800">
{{ $slot }} {{ $slot }}
</div> </div>
</div> </div>

View File

@ -1,3 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}> <button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex justify-center items-center px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 border border-transparent rounded-xl font-bold text-xs text-white uppercase tracking-widest shadow-md shadow-blue-500/30 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200']) }}>
{{ $slot }} {{ $slot }}
</button> </button>

View File

@ -1,3 +1,3 @@
@props(['disabled' => false]) @props(['disabled' => false])
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}> <input @disabled($disabled) {{ $attributes->merge(['class' => 'border border-gray-200 bg-gray-50 text-gray-900 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block w-full p-3 shadow-sm hover:bg-white transition-all duration-200 outline-none']) }}>

View File

@ -5,14 +5,78 @@
@section('content') @section('content')
<x-page-header title="Laporan Kehadiran" /> <x-page-header title="Laporan Kehadiran" />
<x-card> <x-card>
<div class="mb-4 flex flex-col md:flex-row justify-between items-center gap-4"> <!-- Filter Form (Sembunyikan saat cetak) -->
<p class="text-gray-500 text-sm">Berikut adalah daftar rekapitulasi kehadiran pengunjung dan anggota perpustakaan.</p> <form method="GET" action="{{ route('admin.laporan.kehadiran') }}" class="mb-6 bg-gray-50 p-5 rounded-2xl border border-gray-200/60 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4 items-end print:hidden">
<div>
<label class="block text-xs font-bold text-gray-700 mb-1.5 uppercase tracking-wide">Bulan</label>
<select name="bulan" class="w-full bg-white border border-gray-200 text-gray-800 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block p-3 shadow-sm outline-none">
<option value="">-- Semua Bulan --</option>
@foreach([
'1' => 'Januari', '2' => 'Februari', '3' => 'Maret', '4' => 'April',
'5' => 'Mei', '6' => 'Juni', '7' => 'Juli', '8' => 'Agustus',
'9' => 'September', '10' => 'Oktober', '11' => 'November', '12' => 'Desember'
] as $num => $name)
<option value="{{ $num }}" {{ request('bulan') == $num ? 'selected' : '' }}>{{ $name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-700 mb-1.5 uppercase tracking-wide">Tahun</label>
<select name="tahun" class="w-full bg-white border border-gray-200 text-gray-800 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block p-3 shadow-sm outline-none">
<option value="">-- Semua Tahun --</option>
@for($y = date('Y') + 1; $y >= date('Y') - 4; $y--)
<option value="{{ $y }}" {{ request('tahun', date('Y')) == $y ? 'selected' : '' }}>{{ $y }}</option>
@endfor
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-700 mb-1.5 uppercase tracking-wide">Dari Baris</label>
<input type="number" name="limit_start" value="{{ request('limit_start') }}" min="1" placeholder="Contoh: 1" class="w-full bg-white border border-gray-200 text-gray-800 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block p-3 shadow-sm outline-none">
</div>
<div>
<label class="block text-xs font-bold text-gray-700 mb-1.5 uppercase tracking-wide">Sampai Baris</label>
<input type="number" name="limit_end" value="{{ request('limit_end') }}" min="1" placeholder="Contoh: 20" class="w-full bg-white border border-gray-200 text-gray-800 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block p-3 shadow-sm outline-none">
</div>
<div class="flex gap-2">
<button type="submit" class="flex-grow bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-xl shadow-md transition duration-300 text-sm flex items-center justify-center gap-2">
<i class="fas fa-filter"></i> Filter
</button>
@if(request()->anyFilled(['bulan', 'tahun', 'limit_start', 'limit_end']))
<a href="{{ route('admin.laporan.kehadiran') }}" class="bg-gray-100 hover:bg-red-50 border border-gray-200 text-gray-500 hover:text-red-500 p-3 rounded-xl shadow-sm transition duration-300 flex items-center justify-center" title="Reset Filter">
<i class="fas fa-times"></i>
</a>
@endif
</div>
</form>
<div class="mb-4 flex flex-col md:flex-row justify-between items-center gap-4 print:hidden">
<p class="text-gray-500 text-sm">Berikut adalah daftar rekapitulasi kehadiran pengunjung yang disaring.</p>
<button onclick="window.print()" class="bg-gray-800 hover:bg-gray-900 text-white px-4 py-2 rounded-lg text-sm font-bold shadow-sm transition-all flex items-center gap-2"> <button onclick="window.print()" class="bg-gray-800 hover:bg-gray-900 text-white px-4 py-2 rounded-lg text-sm font-bold shadow-sm transition-all flex items-center gap-2">
<i class="fas fa-print"></i> Cetak PDF / Print <i class="fas fa-print"></i> Cetak PDF / Print
</button> </button>
</div> </div>
<div class="overflow-x-auto print:overflow-visible"> <div class="overflow-x-auto print:overflow-visible">
<!-- Print Header (Hanya terlihat saat cetak/print) -->
<div class="hidden print:block mb-6 border-b-2 border-gray-800 pb-4 text-center">
<h1 class="text-2xl font-black uppercase tracking-wide text-gray-900">Laporan Rekapitulasi Kehadiran</h1>
<h2 class="text-sm font-bold text-gray-700 mt-1">PERPUSTAKAAN DAERAH JEMBER</h2>
<p class="text-xs text-gray-500 mt-0.5">Jl. Mastrip No. 1, Kabupaten Jember</p>
<div class="text-[11px] text-gray-600 mt-3 flex flex-wrap justify-center gap-x-4 gap-y-1 font-medium border-t border-gray-100 pt-3">
@if(request('bulan') || request('tahun'))
<span>Periode:
{{ request('bulan') ? Carbon\Carbon::create()->month((int) request('bulan'))->translatedFormat('F') : 'Semua Bulan' }}
{{ request('tahun') ?? '' }}
</span>
@endif
<span>Dicetak Pada: {{ \Carbon\Carbon::now()->translatedFormat('d F Y H:i') }}</span>
</div>
</div>
<x-table> <x-table>
<x-slot name="head"> <x-slot name="head">
<x-th>No</x-th> <x-th>No</x-th>
@ -24,7 +88,7 @@
@forelse($bukuTamu as $item) @forelse($bukuTamu as $item)
<tr class="hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-0"> <tr class="hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-0">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $loop->iteration }}</td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ (request('limit_start') ? (int)request('limit_start') : 1) + $loop->index }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
@if($item->user) @if($item->user)
{{-- Member path: data dari users --}} {{-- Member path: data dari users --}}
@ -59,22 +123,68 @@
<style> <style>
@media print { @media print {
body * { /* Hide layout elements not needed for print */
visibility: hidden; header, aside, .sidebar, nav, footer, button, .print\:hidden, form, .mb-4 {
}
.sidebar, header, nav, footer, button, .mb-4 {
display: none !important; display: none !important;
} }
.print\:overflow-visible, .print\:overflow-visible * {
visibility: visible !important; /* Reset body, main, and containers to allow natural multi-page flow */
html, body {
height: auto !important;
min-height: auto !important;
overflow: visible !important;
background-color: #fff !important;
color: #000 !important;
} }
.print\:overflow-visible {
position: absolute; /* Force the relative layout container to overflow naturally */
left: 0; div.flex.flex-1.overflow-hidden.relative {
top: 0; display: block !important;
width: 100%; height: auto !important;
margin: 0 !important; min-height: auto !important;
overflow: visible !important;
position: static !important;
}
main {
padding: 0 !important; padding: 0 !important;
margin: 0 !important;
height: auto !important;
min-height: auto !important;
overflow: visible !important;
position: static !important;
display: block !important;
width: 100% !important;
}
/* Reset card and table parent wrappers */
.bg-white, .shadow-xl, .rounded-3xl, .p-6, .p-8 {
background: transparent !important;
box-shadow: none !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
height: auto !important;
min-height: auto !important;
overflow: visible !important;
}
/* Adjust table styles for print */
table {
width: 100% !important;
border-collapse: collapse !important;
}
th, td {
padding: 8px 12px !important;
border: 1px solid #ddd !important;
font-size: 11px !important;
color: #000 !important;
}
/* Avoid page breaks inside table rows */
tr {
page-break-inside: avoid !important;
} }
} }
</style> </style>

View File

@ -5,14 +5,91 @@
@section('content') @section('content')
<x-page-header title="Laporan Peminjaman" /> <x-page-header title="Laporan Peminjaman" />
<x-card> <x-card>
<div class="mb-4 flex flex-col md:flex-row justify-between items-center gap-4"> <!-- Filter Form (Sembunyikan saat cetak) -->
<p class="text-gray-500 text-sm">Berikut adalah seluruh rekap data peminjaman buku perpustakaan.</p> <form method="GET" action="{{ route('admin.laporan.peminjaman') }}" class="mb-6 bg-gray-50 p-5 rounded-2xl border border-gray-200/60 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4 items-end print:hidden">
<div>
<label class="block text-xs font-bold text-gray-700 mb-1.5 uppercase tracking-wide">Bulan</label>
<select name="bulan" class="w-full bg-white border border-gray-200 text-gray-800 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block p-3 shadow-sm outline-none">
<option value="">-- Semua Bulan --</option>
@foreach([
'1' => 'Januari', '2' => 'Februari', '3' => 'Maret', '4' => 'April',
'5' => 'Mei', '6' => 'Juni', '7' => 'Juli', '8' => 'Agustus',
'9' => 'September', '10' => 'Oktober', '11' => 'November', '12' => 'Desember'
] as $num => $name)
<option value="{{ $num }}" {{ request('bulan') == $num ? 'selected' : '' }}>{{ $name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-700 mb-1.5 uppercase tracking-wide">Tahun</label>
<select name="tahun" class="w-full bg-white border border-gray-200 text-gray-800 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block p-3 shadow-sm outline-none">
<option value="">-- Semua Tahun --</option>
@for($y = date('Y') + 1; $y >= date('Y') - 4; $y--)
<option value="{{ $y }}" {{ request('tahun', date('Y')) == $y ? 'selected' : '' }}>{{ $y }}</option>
@endfor
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-700 mb-1.5 uppercase tracking-wide">Kategori & Lokasi Buku</label>
<select name="id_kategori" class="w-full bg-white border border-gray-200 text-gray-800 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block p-3 shadow-sm outline-none">
<option value="">-- Semua Kategori --</option>
@foreach($categories as $key => $name)
<option value="{{ $key }}" {{ request('id_kategori') === (string)$key ? 'selected' : '' }}>{{ $name }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-700 mb-1.5 uppercase tracking-wide">Dari Baris</label>
<input type="number" name="limit_start" value="{{ request('limit_start') }}" min="1" placeholder="Contoh: 1" class="w-full bg-white border border-gray-200 text-gray-800 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block p-3 shadow-sm outline-none">
</div>
<div>
<label class="block text-xs font-bold text-gray-700 mb-1.5 uppercase tracking-wide">Sampai Baris</label>
<input type="number" name="limit_end" value="{{ request('limit_end') }}" min="1" placeholder="Contoh: 20" class="w-full bg-white border border-gray-200 text-gray-800 text-sm rounded-xl focus:ring-blue-500 focus:border-blue-500 block p-3 shadow-sm outline-none">
</div>
<div class="flex gap-2">
<button type="submit" class="flex-grow bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-xl shadow-md transition duration-300 text-sm flex items-center justify-center gap-2">
<i class="fas fa-filter"></i> Filter
</button>
@if(request()->anyFilled(['bulan', 'tahun', 'id_kategori', 'limit_start', 'limit_end']))
<a href="{{ route('admin.laporan.peminjaman') }}" class="bg-gray-100 hover:bg-red-50 border border-gray-200 text-gray-500 hover:text-red-500 p-3 rounded-xl shadow-sm transition duration-300 flex items-center justify-center" title="Reset Filter">
<i class="fas fa-times"></i>
</a>
@endif
</div>
</form>
<div class="mb-4 flex flex-col md:flex-row justify-between items-center gap-4 print:hidden">
<p class="text-gray-500 text-sm">Berikut adalah rekap data peminjaman buku perpustakaan yang disaring.</p>
<button onclick="window.print()" class="bg-gray-800 hover:bg-gray-900 text-white px-4 py-2 rounded-lg text-sm font-bold shadow-sm transition-all flex items-center gap-2"> <button onclick="window.print()" class="bg-gray-800 hover:bg-gray-900 text-white px-4 py-2 rounded-lg text-sm font-bold shadow-sm transition-all flex items-center gap-2">
<i class="fas fa-print"></i> Cetak Laporan <i class="fas fa-print"></i> Cetak Laporan
</button> </button>
</div> </div>
<div class="overflow-x-auto print:overflow-visible"> <div class="overflow-x-auto print:overflow-visible">
<!-- Print Header (Hanya terlihat saat cetak/print) -->
<div class="hidden print:block mb-6 border-b-2 border-gray-800 pb-4 text-center">
<h1 class="text-2xl font-black uppercase tracking-wide text-gray-900">Laporan Rekapitulasi Peminjaman</h1>
<h2 class="text-sm font-bold text-gray-700 mt-1">PERPUSTAKAAN DAERAH JEMBER</h2>
<p class="text-xs text-gray-500 mt-0.5">Jl. Mastrip No. 1, Kabupaten Jember</p>
<div class="text-[11px] text-gray-600 mt-3 flex flex-wrap justify-center gap-x-4 gap-y-1 font-medium border-t border-gray-100 pt-3">
@if(request('bulan') || request('tahun'))
<span>Periode:
{{ request('bulan') ? Carbon\Carbon::create()->month((int) request('bulan'))->translatedFormat('F') : 'Semua Bulan' }}
{{ request('tahun') ?? '' }}
</span>
@endif
@if(request('id_kategori') !== null && isset($categories[request('id_kategori')]))
<span>Kategori: {{ $categories[request('id_kategori')] }}</span>
@endif
<span>Dicetak Pada: {{ \Carbon\Carbon::now()->translatedFormat('d F Y H:i') }}</span>
</div>
</div>
<x-table> <x-table>
<x-slot name="head"> <x-slot name="head">
<x-th>No</x-th> <x-th>No</x-th>
@ -27,7 +104,7 @@
@forelse($peminjaman as $index => $item) @forelse($peminjaman as $index => $item)
<tr class="hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-0"> <tr class="hover:bg-gray-50 transition-colors border-b border-gray-100 last:border-0">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $index + 1 }}</td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ (request('limit_start') ? (int)request('limit_start') : 1) + $loop->index }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
@if($item->anggota) @if($item->anggota)
<div class="text-sm font-bold text-gray-900">{{ $item->anggota->nama }}</div> <div class="text-sm font-bold text-gray-900">{{ $item->anggota->nama }}</div>
@ -80,22 +157,68 @@
<style> <style>
@media print { @media print {
body * { /* Hide layout elements not needed for print */
visibility: hidden; header, aside, .sidebar, nav, footer, button, .print\:hidden, form, .mb-4 {
}
.sidebar, header, nav, footer, button, .mb-4 {
display: none !important; display: none !important;
} }
.print\:overflow-visible, .print\:overflow-visible * {
visibility: visible !important; /* Reset body, main, and containers to allow natural multi-page flow */
html, body {
height: auto !important;
min-height: auto !important;
overflow: visible !important;
background-color: #fff !important;
color: #000 !important;
} }
.print\:overflow-visible {
position: absolute; /* Force the relative layout container to overflow naturally */
left: 0; div.flex.flex-1.overflow-hidden.relative {
top: 0; display: block !important;
width: 100%; height: auto !important;
margin: 0 !important; min-height: auto !important;
overflow: visible !important;
position: static !important;
}
main {
padding: 0 !important; padding: 0 !important;
margin: 0 !important;
height: auto !important;
min-height: auto !important;
overflow: visible !important;
position: static !important;
display: block !important;
width: 100% !important;
}
/* Reset card and table parent wrappers */
.bg-white, .shadow-xl, .rounded-3xl, .p-6, .p-8 {
background: transparent !important;
box-shadow: none !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
height: auto !important;
min-height: auto !important;
overflow: visible !important;
}
/* Adjust table styles for print */
table {
width: 100% !important;
border-collapse: collapse !important;
}
th, td {
padding: 8px 12px !important;
border: 1px solid #ddd !important;
font-size: 11px !important;
color: #000 !important;
}
/* Avoid page breaks inside table rows */
tr {
page-break-inside: avoid !important;
} }
} }
</style> </style>

View File

@ -1,11 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="id"> <html lang="id">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Admin Dashboard') - SARAKATA</title> <title>@yield('title', 'Admin Dashboard') - SARAKATA</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
@ -16,64 +18,108 @@
extend: { extend: {
fontFamily: { sans: ['Inter', 'sans-serif'] }, fontFamily: { sans: ['Inter', 'sans-serif'] },
colors: { colors: {
primary: { 50:'#eef2ff',100:'#e0e7ff',200:'#c7d2fe',300:'#a5b4fc',400:'#818cf8',500:'#6366f1',600:'#4f46e5',700:'#4338ca',800:'#3730a3',900:'#312e81' }, primary: { 50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc', 400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca', 800: '#3730a3', 900: '#312e81' },
} }
} }
} }
} }
</script> </script>
<style> <style>
.gradient-text { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #2563eb 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .gradient-text {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 50%, #2563eb 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* TomSelect Custom Styling */
.ts-control {
border: 1px solid #e5e7eb !important;
background-color: #f9fafb !important;
border-radius: 0.75rem !important;
padding: 0.75rem 1rem !important;
font-size: 0.875rem !important;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;
}
.ts-control.focus {
border-color: #6366f1 !important;
box-shadow: 0 0 0 1px #6366f1 !important;
}
.ts-control input {
font-size: 0.875rem !important;
}
</style> </style>
</head> </head>
<body class="bg-gray-50 font-sans antialiased flex flex-col h-screen overflow-hidden">
<body class="bg-gray-50 font-sans antialiased flex flex-col h-screen overflow-hidden" x-data="{ sidebarOpen: false }">
{{-- TOP HEADER --}} {{-- TOP HEADER --}}
<header class="bg-gradient-to-r from-primary-600 via-primary-700 to-primary-800 text-white h-16 flex items-center justify-between px-6 shadow-lg z-20"> <header
class="bg-gradient-to-r from-primary-600 via-primary-700 to-primary-800 text-white h-16 flex items-center justify-between px-6 shadow-lg z-20">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-9 h-9 bg-white/15 rounded-xl flex items-center justify-center backdrop-blur-sm"> <!-- Mobile Hamburger -->
<button @click="sidebarOpen = true"
class="md:hidden w-9 h-9 bg-white/15 hover:bg-white/25 rounded-xl flex items-center justify-center backdrop-blur-sm transition focus:outline-none">
<i class="fas fa-bars text-white"></i>
</button>
<div class="hidden md:flex w-9 h-9 bg-white/15 rounded-xl items-center justify-center backdrop-blur-sm">
<i class="fas fa-book-open text-lg"></i> <i class="fas fa-book-open text-lg"></i>
</div> </div>
<h1 class="text-base font-bold tracking-wide">SARAKATA <span class="font-normal text-primary-200 hidden sm:inline"> Sistem Informasi Perpustakaan</span></h1> <h1 class="text-base font-bold tracking-wide">SARAKATA <span
class="font-normal text-primary-200 hidden sm:inline"> Sistem Informasi Perpustakaan</span></h1>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<form method="POST" action="{{ route('logout') }}"> <form method="POST" action="{{ route('logout') }}">
@csrf @csrf
<button class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/20 transition text-sm font-medium"> <button
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/10 hover:bg-white/20 transition text-sm font-medium">
<i class="fas fa-sign-out-alt text-xs"></i> Logout <i class="fas fa-sign-out-alt text-xs"></i> Logout
</button> </button>
</form> </form>
<div class="w-9 h-9 rounded-xl overflow-hidden border-2 border-white/30"> <div class="w-9 h-9 rounded-xl overflow-hidden border-2 border-white/30">
<img src="https://ui-avatars.com/api/?name={{ auth()->user()->name ?? 'Admin' }}&background=6366f1&color=fff" class="w-full h-full object-cover"> <img src="https://ui-avatars.com/api/?name={{ auth()->user()->name ?? 'Admin' }}&background=6366f1&color=fff"
class="w-full h-full object-cover">
</div> </div>
</div> </div>
</header> </header>
<div class="flex flex-1 overflow-hidden"> <div class="flex flex-1 overflow-hidden relative">
<!-- Overlay Khusus Mobile -->
<div x-show="sidebarOpen" x-transition.opacity
class="fixed inset-0 bg-gray-900/60 z-40 md:hidden backdrop-blur-sm" @click="sidebarOpen = false"
style="display: none;"></div>
{{-- SIDEBAR --}} {{-- SIDEBAR --}}
<aside class="w-64 bg-white shadow-xl flex flex-col overflow-y-auto z-10 border-r border-gray-100"> <aside :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
class="w-64 bg-white shadow-2xl flex flex-col overflow-y-auto z-50 border-r border-gray-100 absolute inset-y-0 left-0 transform md:relative md:translate-x-0 transition-transform duration-300 ease-in-out h-full">
{{-- Profile --}} {{-- Profile --}}
<div class="flex flex-col items-center py-8 border-b border-gray-100 px-4"> <div class="flex flex-col items-center py-8 border-b border-gray-100 px-4">
<div class="w-20 h-20 rounded-2xl overflow-hidden border-4 border-primary-100 shadow-lg shadow-primary-100"> <div
<img src="https://ui-avatars.com/api/?name={{ auth()->user()->name ?? 'Admin' }}&background=6366f1&color=fff&size=128" class="w-full h-full object-cover"> class="w-20 h-20 rounded-2xl overflow-hidden border-4 border-primary-100 shadow-lg shadow-primary-100">
<img src="https://ui-avatars.com/api/?name={{ auth()->user()->name ?? 'Admin' }}&background=6366f1&color=fff&size=128"
class="w-full h-full object-cover">
</div> </div>
<h2 class="mt-4 font-bold text-gray-800 text-lg">{{ auth()->user()->name ?? 'Admin' }}</h2> <h2 class="mt-4 font-bold text-gray-800 text-lg">{{ auth()->user()->name ?? 'Admin' }}</h2>
<span class="px-3 py-1 mt-1.5 text-[10px] font-bold text-primary-700 bg-primary-50 rounded-full uppercase tracking-wider">Administrator</span> <span
class="px-3 py-1 mt-1.5 text-[10px] font-bold text-primary-700 bg-primary-50 rounded-full uppercase tracking-wider">Administrator</span>
</div> </div>
{{-- Navigation --}} {{-- Navigation --}}
<nav class="flex-1 py-6 space-y-1 px-3"> <nav class="flex-1 py-6 space-y-1 px-3">
<a href="{{ route('admin.dashboard') }}" <a href="{{ route('admin.dashboard') }}"
class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 mb-1 class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 mb-1
{{ request()->routeIs('admin.dashboard') ? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-200' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}"> {{ request()->routeIs('admin.dashboard') ? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-200' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}">
<i class="fas fa-th-large w-5 text-center"></i> <i class="fas fa-th-large w-5 text-center"></i>
<span class="font-semibold text-sm">Dashboard</span> <span class="font-semibold text-sm">Dashboard</span>
</a> </a>
<a href="{{ route('admin.buku.index') }}" <a href="{{ route('admin.buku.index') }}"
class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 mb-1 class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 mb-1
{{ request()->routeIs('admin.buku.*') ? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-200' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}"> {{ request()->routeIs('admin.buku.*') ? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-200' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}">
<i class="fas fa-book w-5 text-center"></i> <i class="fas fa-book w-5 text-center"></i>
<span class="font-semibold text-sm">Data Buku</span> <span class="font-semibold text-sm">Data Buku</span>
@ -82,30 +128,35 @@ class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300
{{-- Anggota Dropdown --}} {{-- Anggota Dropdown --}}
<div x-data="{ open: {{ request()->routeIs('admin.anggota.*') ? 'true' : 'false' }} }"> <div x-data="{ open: {{ request()->routeIs('admin.anggota.*') ? 'true' : 'false' }} }">
<button @click="open = !open" <button @click="open = !open"
class="w-full flex items-center justify-between px-4 py-3 rounded-xl transition-all duration-300 mb-1 class="w-full flex items-center justify-between px-4 py-3 rounded-xl transition-all duration-300 mb-1
{{ request()->routeIs('admin.anggota.*') ? 'bg-primary-50 text-primary-600' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}"> {{ request()->routeIs('admin.anggota.*') ? 'bg-primary-50 text-primary-600' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<i class="fas fa-users w-5 text-center"></i> <i class="fas fa-users w-5 text-center"></i>
<span class="font-semibold text-sm">Data Anggota</span> <span class="font-semibold text-sm">Data Anggota</span>
</div> </div>
<i :class="open ? 'rotate-180' : ''" class="fas fa-chevron-down text-[10px] transition-transform duration-200"></i> <i :class="open ? 'rotate-180' : ''"
class="fas fa-chevron-down text-[10px] transition-transform duration-200"></i>
</button> </button>
<div x-show="open" x-transition class="pl-12 pr-3 py-1 space-y-1"> <div x-show="open" x-transition class="pl-12 pr-3 py-1 space-y-1">
<a href="{{ route('admin.anggota.tamu') }}" class="block px-3 py-2 text-xs font-medium text-gray-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition">Buku Tamu</a> <a href="{{ route('admin.anggota.tamu') }}"
<a href="{{ route('admin.anggota.member.index') }}" class="block px-3 py-2 text-xs font-medium text-gray-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition">Data Member</a> class="block px-3 py-2 text-xs font-medium text-gray-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition">Buku
Tamu</a>
<a href="{{ route('admin.anggota.member.index') }}"
class="block px-3 py-2 text-xs font-medium text-gray-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition">Data
Member</a>
</div> </div>
</div> </div>
<a href="{{ route('admin.peminjaman.index') }}" <a href="{{ route('admin.peminjaman.index') }}"
class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 mb-1 class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 mb-1
{{ request()->routeIs('admin.peminjaman.*') ? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-200' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}"> {{ request()->routeIs('admin.peminjaman.*') && !request()->routeIs('admin.peminjaman.scan') ? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-200' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}">
<i class="fas fa-file-export w-5 text-center"></i> <i class="fas fa-file-export w-5 text-center"></i>
<span class="font-semibold text-sm">Data Peminjaman</span> <span class="font-semibold text-sm">Data Peminjaman</span>
</a> </a>
<a href="{{ route('admin.pengembalian.index') }}" <a href="{{ route('admin.pengembalian.index') }}"
class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 mb-1 class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 mb-1
{{ request()->routeIs('admin.pengembalian.*') ? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-200' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}"> {{ request()->routeIs('admin.pengembalian.*') || request()->routeIs('admin.peminjaman.scan') ? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-200' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}">
<i class="fas fa-file-import w-5 text-center"></i> <i class="fas fa-file-import w-5 text-center"></i>
<span class="font-semibold text-sm">Data Pengembalian</span> <span class="font-semibold text-sm">Data Pengembalian</span>
</a> </a>
@ -113,23 +164,28 @@ class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300
{{-- Laporan Dropdown --}} {{-- Laporan Dropdown --}}
<div x-data="{ open: {{ request()->routeIs('admin.laporan.*') ? 'true' : 'false' }} }"> <div x-data="{ open: {{ request()->routeIs('admin.laporan.*') ? 'true' : 'false' }} }">
<button @click="open = !open" <button @click="open = !open"
class="w-full flex items-center justify-between px-4 py-3 rounded-xl transition-all duration-300 mb-1 class="w-full flex items-center justify-between px-4 py-3 rounded-xl transition-all duration-300 mb-1
{{ request()->routeIs('admin.laporan.*') ? 'bg-primary-50 text-primary-600' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}"> {{ request()->routeIs('admin.laporan.*') ? 'bg-primary-50 text-primary-600' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<i class="fas fa-chart-bar w-5 text-center"></i> <i class="fas fa-chart-bar w-5 text-center"></i>
<span class="font-semibold text-sm">Laporan</span> <span class="font-semibold text-sm">Laporan</span>
</div> </div>
<i :class="open ? 'rotate-180' : ''" class="fas fa-chevron-down text-[10px] transition-transform duration-200"></i> <i :class="open ? 'rotate-180' : ''"
class="fas fa-chevron-down text-[10px] transition-transform duration-200"></i>
</button> </button>
<div x-show="open" x-transition class="pl-12 pr-3 py-1 space-y-1"> <div x-show="open" x-transition class="pl-12 pr-3 py-1 space-y-1">
<a href="{{ route('admin.laporan.kehadiran') }}" class="block px-3 py-2 text-xs font-medium text-gray-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition">Lap. Kehadiran</a> <a href="{{ route('admin.laporan.kehadiran') }}"
<a href="{{ route('admin.laporan.peminjaman') }}" class="block px-3 py-2 text-xs font-medium text-gray-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition">Lap. Peminjaman</a> class="block px-3 py-2 text-xs font-medium text-gray-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition">Lap.
Kehadiran</a>
<a href="{{ route('admin.laporan.peminjaman') }}"
class="block px-3 py-2 text-xs font-medium text-gray-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition">Lap.
Peminjaman</a>
</div> </div>
</div> </div>
{{-- Akun Admin Menu --}} {{-- Akun Admin Menu --}}
<a href="{{ route('admin.akun.index') }}" <a href="{{ route('admin.akun.index') }}"
class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 mb-1 mt-4 border-t border-gray-100 pt-5 class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300 mb-1 mt-4 border-t border-gray-100 pt-5
{{ request()->routeIs('admin.akun.*') ? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-200' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}"> {{ request()->routeIs('admin.akun.*') ? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white shadow-lg shadow-primary-200' : 'text-gray-500 hover:bg-primary-50 hover:text-primary-600' }}">
<i class="fas fa-user-shield w-5 text-center"></i> <i class="fas fa-user-shield w-5 text-center"></i>
<span class="font-semibold text-sm">Kelola Admin</span> <span class="font-semibold text-sm">Kelola Admin</span>
@ -144,5 +200,53 @@ class="flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-300
</main> </main>
</div> </div>
{{-- SweetAlert2 for Global Delete Confirmation --}}
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Intercept all forms that have a confirm() in their onsubmit attribute
const deleteForms = document.querySelectorAll('form[onsubmit*="return confirm"]');
deleteForms.forEach(form => {
// Extract the message from the confirm('Message') call
const originalOnsubmit = form.getAttribute('onsubmit');
const messageMatch = originalOnsubmit.match(/confirm\(['"](.*?)['"]\)/);
const message = messageMatch ? messageMatch[1] : 'Apakah Anda yakin ingin menghapus data ini?';
// Remove the default onsubmit to prevent browser confirm
form.removeAttribute('onsubmit');
// Add the custom SweetAlert event listener
form.addEventListener('submit', function (e) {
e.preventDefault();
Swal.fire({
title: 'Konfirmasi Hapus',
text: message,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#6b7280',
confirmButtonText: '<i class="fas fa-trash-alt mr-2"></i> Ya, Hapus',
cancelButtonText: 'Batal',
reverseButtons: true,
customClass: {
popup: 'rounded-3xl shadow-2xl',
title: 'text-2xl font-bold text-gray-800',
htmlContainer: 'text-gray-500',
confirmButton: 'rounded-xl px-6 py-2.5 font-bold shadow-lg shadow-red-500/30 transition-all ml-3',
cancelButton: 'rounded-xl px-6 py-2.5 font-bold bg-gray-100 text-gray-600 hover:bg-gray-200 transition-all'
},
buttonsStyling: false
}).then((result) => {
if (result.isConfirmed) {
form.submit();
}
});
});
});
});
</script>
@stack('scripts')
</body> </body>
</html> </html>

View File

@ -6,6 +6,7 @@
<title>@yield('title', 'Sarakata — Perpustakaan Digital')</title> <title>@yield('title', 'Sarakata — Perpustakaan Digital')</title>
<meta name="description" content="Sarakata - Sistem Informasi Perpustakaan Digital untuk Generasi Modern"> <meta name="description" content="Sarakata - Sistem Informasi Perpustakaan Digital untuk Generasi Modern">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ asset('favicon.ico') }}" type="image/x-icon">
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"
rel="stylesheet"> rel="stylesheet">
@ -107,11 +108,11 @@
@stack('styles') @stack('styles')
</head> </head>
<body class="bg-white text-gray-800 font-sans antialiased"> <body class="bg-white text-gray-800 font-sans antialiased overflow-x-hidden w-full">
{{-- NAVBAR --}} {{-- NAVBAR --}}
<nav class="glass-nav fixed w-full top-0 z-50 border-b border-gray-100/50"> <nav class="glass-nav fixed w-full top-0 z-50 border-b border-gray-100/50">
<div class="max-w-7xl mx-auto px-6 py-4 flex justify-between items-center"> <div class="max-w-7xl mx-auto px-6 py-4 flex justify-between items-center relative">
<a href="{{ route('home') }}" class="flex items-center gap-3 group"> <a href="{{ route('home') }}" class="flex items-center gap-3 group">
<div <div
class="w-10 h-10 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl flex items-center justify-center shadow-lg shadow-primary-200 group-hover:shadow-primary-300 transition-shadow"> class="w-10 h-10 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl flex items-center justify-center shadow-lg shadow-primary-200 group-hover:shadow-primary-300 transition-shadow">
@ -126,20 +127,27 @@ class="text-[10px] font-bold text-gray-500 uppercase tracking-widest mt-1 hidden
</div> </div>
</a> </a>
<div class="flex items-center gap-8 text-sm font-medium"> <!-- Hamburger Button for Mobile -->
<button id="mobile-menu-btn"
class="md:hidden text-gray-600 hover:text-primary-600 focus:outline-none p-2 rounded-lg bg-gray-50">
<i class="fas fa-bars text-xl"></i>
</button>
<!-- Desktop Menu -->
<div class="hidden md:flex items-center gap-8 text-sm font-medium">
<a href="{{ route('home') }}" <a href="{{ route('home') }}"
class="relative transition-colors {{ request()->routeIs('home') ? 'text-primary-600 font-semibold' : 'text-gray-600 hover:text-primary-600' }} after:absolute after:bottom-[-4px] after:left-0 {{ request()->routeIs('home') ? 'after:w-full' : 'after:w-0 hover:after:w-full' }} after:h-0.5 after:bg-primary-500 after:transition-all">Beranda</a> class="relative transition-colors {{ request()->routeIs('home') ? 'text-primary-600 font-semibold' : 'text-gray-600 hover:text-primary-600' }} after:absolute after:bottom-[-4px] after:left-0 {{ request()->routeIs('home') ? 'after:w-full' : 'after:w-0 hover:after:w-full' }} after:h-0.5 after:bg-primary-500 after:transition-all">Beranda</a>
<a href="{{ route('katalog.index') }}" <a href="{{ route('katalog.index') }}"
class="relative transition-colors {{ request()->routeIs('katalog.*') ? 'text-primary-600 font-semibold' : 'text-gray-600 hover:text-primary-600' }} after:absolute after:bottom-[-4px] after:left-0 {{ request()->routeIs('katalog.*') ? 'after:w-full' : 'after:w-0 hover:after:w-full' }} after:h-0.5 after:bg-primary-500 after:transition-all">Katalog class="relative transition-colors {{ request()->routeIs('katalog.*') ? 'text-primary-600 font-semibold' : 'text-gray-600 hover:text-primary-600' }} after:absolute after:bottom-[-4px] after:left-0 {{ request()->routeIs('katalog.*') ? 'after:w-full' : 'after:w-0 hover:after:w-full' }} after:h-0.5 after:bg-primary-500 after:transition-all">Katalog
Buku</a> Buku</a>
<a href="{{ route('home') }}#fitur" <a href="{{ route('home') }}#fitur"
class="relative transition-colors {{ request()->routeIs('home') && request()->getQueryString() == '' ? 'text-primary-600 font-semibold' : 'text-gray-600 hover:text-primary-600' }} after:absolute after:bottom-[-4px] after:left-0 after:w-0 after:h-0.5 after:bg-primary-500 after:transition-all hover:after:w-full">Fitur</a> class="relative transition-colors text-gray-600 hover:text-primary-600 after:absolute after:bottom-[-4px] after:left-0 after:w-0 after:h-0.5 after:bg-primary-500 after:transition-all hover:after:w-full">Fitur</a>
<a href="{{ route('home') }}#rekomendasi" <a href="{{ route('home') }}#rekomendasi"
class="relative transition-colors {{ request()->routeIs('home') && request()->getQueryString() == '' ? 'text-primary-600 font-semibold' : 'text-gray-600 hover:text-primary-600' }} after:absolute after:bottom-[-4px] after:left-0 after:w-0 after:h-0.5 after:bg-primary-500 after:transition-all hover:after:w-full">Koleksi</a> class="relative transition-colors text-gray-600 hover:text-primary-600 after:absolute after:bottom-[-4px] after:left-0 after:w-0 after:h-0.5 after:bg-primary-500 after:transition-all hover:after:w-full">Koleksi</a>
@guest @guest
<button onclick="openGuestModal()" <button onclick="openGuestModal()"
class="px-5 py-2.5 bg-primary-50 text-primary-600 rounded-xl font-semibold hover:bg-primary-100 transition-colors cursor-pointer"> class="px-5 py-2.5 bg-primary-50 text-primary-600 rounded-xl font-semibold hover:bg-primary-100 transition-colors cursor-pointer flex items-center justify-center">
<i class="fas fa-book-reader mr-1.5"></i> Buku Tamu <i class="fas fa-book-reader mr-1.5"></i> Buku Tamu
</button> </button>
<a href="{{ route('login') }}" <a href="{{ route('login') }}"
@ -163,6 +171,47 @@ class="px-5 py-2.5 bg-gradient-to-r from-green-600 to-green-700 text-white round
@endauth @endauth
</div> </div>
</div> </div>
<!-- Mobile Menu -->
<div id="mobile-menu"
class="hidden md:hidden bg-white/95 backdrop-blur-xl border-t border-gray-100 shadow-xl absolute w-full left-0 top-full flex-col gap-4 px-6 py-6 transition-all">
<a href="{{ route('home') }}"
class="block font-bold text-gray-700 hover:text-primary-600 py-2 border-b border-gray-50">Beranda</a>
<a href="{{ route('katalog.index') }}"
class="block font-bold text-gray-700 hover:text-primary-600 py-2 border-b border-gray-50">Katalog
Buku</a>
<a href="{{ route('home') }}#fitur"
class="block font-bold text-gray-700 hover:text-primary-600 py-2 border-b border-gray-50">Fitur</a>
<a href="{{ route('home') }}#rekomendasi"
class="block font-bold text-gray-700 hover:text-primary-600 py-2 border-b border-gray-50">Koleksi</a>
<div class="flex flex-col gap-3 mt-4">
@guest
<button onclick="openGuestModal()"
class="w-full px-5 py-3 bg-primary-50 text-primary-600 rounded-xl font-bold hover:bg-primary-100 transition-colors text-center flex items-center justify-center">
<i class="fas fa-book-reader mr-1.5"></i> Buku Tamu
</button>
<a href="{{ route('login') }}"
class="w-full px-5 py-3 bg-gradient-to-r from-primary-600 to-primary-700 text-white rounded-xl font-bold shadow-lg shadow-primary-200 text-center">
<i class="fas fa-sign-in-alt mr-1.5"></i> Login
</a>
@endguest
@auth
@if (auth()->user()->role === 'admin')
<a href="{{ route('admin.dashboard') }}"
class="w-full px-5 py-3 bg-gradient-to-r from-primary-600 to-primary-700 text-white rounded-xl font-bold shadow-lg text-center">
<i class="fas fa-tachometer-alt mr-1.5"></i> Dashboard Admin
</a>
@else
<a href="{{ route('user.dashboard') }}"
class="w-full px-5 py-3 bg-gradient-to-r from-green-600 to-green-700 text-white rounded-xl font-bold shadow-lg text-center">
<i class="fas fa-user mr-1.5"></i> Dashboard Anggota
</a>
@endif
@endauth
</div>
</div>
</nav> </nav>
{{-- CONTENT --}} {{-- CONTENT --}}
@ -198,8 +247,7 @@ class="w-10 h-10 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl fl
Buku</a></li> Buku</a></li>
<li><a href="{{ route('home') }}#fitur" class="hover:text-white transition-colors">Fitur</a> <li><a href="{{ route('home') }}#fitur" class="hover:text-white transition-colors">Fitur</a>
</li> </li>
<li><a href="{{ route('home') }}#rekomendasi" <li><a href="{{ route('home') }}#rekomendasi" class="hover:text-white transition-colors">Koleksi
class="hover:text-white transition-colors">Koleksi
Buku</a></li> Buku</a></li>
<li><a href="{{ route('buku_tamu.index') }}" class="hover:text-white transition-colors">Buku <li><a href="{{ route('buku_tamu.index') }}" class="hover:text-white transition-colors">Buku
Tamu</a></li> Tamu</a></li>
@ -238,7 +286,93 @@ class="fab fa-twitter text-sm"></i></a>
</div> </div>
</footer> </footer>
<!-- Global Guest Modal -->
<div id="guestModal"
class="fixed inset-0 bg-gray-900/60 backdrop-blur-sm hidden z-[100] flex items-center justify-center p-4 transition-opacity duration-300">
<div class="bg-white rounded-[2rem] shadow-2xl max-w-md w-full overflow-hidden transform transition-all">
<div class="p-8 text-center relative">
<!-- Close Button -->
<button onclick="closeGuestModal()"
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors w-8 h-8 rounded-full hover:bg-gray-100 flex items-center justify-center">
<i class="fas fa-times"></i>
</button>
<!-- Icon Box -->
<div
class="w-16 h-16 bg-gradient-to-br from-primary-500 to-primary-600 text-white rounded-2xl flex items-center justify-center mx-auto mb-5 text-2xl shadow-lg shadow-primary-200">
<i class="fas fa-user-check"></i>
</div>
<h3 class="text-2xl font-black text-gray-900 tracking-tight">Selamat Datang!</h3>
<p class="text-gray-500 text-sm mt-2 leading-relaxed">Apakah Anda sudah terdaftar sebagai anggota
Sarakata?</p>
<div class="grid grid-cols-1 gap-3 mt-8">
<!-- Member Option -->
<a href="{{ route('buku_tamu.index') }}"
class="flex items-center gap-4 p-4 border-2 border-primary-100 rounded-2xl hover:border-primary-300 hover:bg-primary-50/50 transition-all group cursor-pointer shadow-sm hover:shadow-md">
<div
class="w-12 h-12 bg-primary-100 rounded-xl flex items-center justify-center text-primary-600 group-hover:bg-gradient-to-br group-hover:from-primary-500 group-hover:to-primary-600 group-hover:text-white transition-all shadow-sm group-hover:shadow-primary-300">
<i class="fas fa-id-card"></i>
</div>
<div class="text-left flex-1 border-gray-100">
<div class="font-bold text-gray-900 text-sm">Saya Anggota</div>
<div class="text-xs text-gray-400">Punya No. Anggota</div>
</div>
<i
class="fas fa-chevron-right text-primary-300 group-hover:text-primary-500 transition-colors"></i>
</a>
<!-- Visitor Option -->
<a href="{{ route('buku_tamu.index', ['tipe' => 'tamu']) }}"
class="flex items-center gap-4 p-4 border-2 border-gray-100 rounded-2xl hover:border-gray-300 hover:bg-gray-50 transition-all group cursor-pointer shadow-sm hover:shadow-md">
<div
class="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center text-gray-500 group-hover:bg-gray-800 group-hover:text-white transition-all shadow-sm">
<i class="fas fa-user-friends"></i>
</div>
<div class="text-left flex-1 border-gray-100">
<div class="font-bold text-gray-900 text-sm">Pengunjung Umum</div>
<div class="text-xs text-gray-400">Belum punya member</div>
</div>
<i class="fas fa-chevron-right text-gray-300 group-hover:text-gray-600 transition-colors"></i>
</a>
</div>
</div>
</div>
</div>
@stack('scripts') @stack('scripts')
<script>
document.addEventListener('DOMContentLoaded', function () {
const btn = document.getElementById('mobile-menu-btn');
const menu = document.getElementById('mobile-menu');
const icon = btn.querySelector('i');
btn.addEventListener('click', () => {
menu.classList.toggle('hidden');
menu.classList.toggle('flex');
// Toggle icon
if (menu.classList.contains('flex')) {
icon.classList.remove('fa-bars');
icon.classList.add('fa-times');
} else {
icon.classList.remove('fa-times');
icon.classList.add('fa-bars');
}
});
});
// Global Modal Scripts
function openGuestModal() {
document.getElementById('guestModal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function closeGuestModal() {
document.getElementById('guestModal').classList.add('hidden');
document.body.style.overflow = 'auto';
}
</script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,46 @@
@if ($paginator->hasPages())
<nav>
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
<span class="page-link" aria-hidden="true">&lsaquo;</span>
</li>
@else
<li class="page-item">
<a class="page-link" href="{{ $paginator->previousPageUrl() }}" rel="prev" aria-label="@lang('pagination.previous')">&lsaquo;</a>
</li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="page-item disabled" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="page-item active" aria-current="page"><span class="page-link">{{ $page }}</span></li>
@else
<li class="page-item"><a class="page-link" href="{{ $url }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<a class="page-link" href="{{ $paginator->nextPageUrl() }}" rel="next" aria-label="@lang('pagination.next')">&rsaquo;</a>
</li>
@else
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
<span class="page-link" aria-hidden="true">&rsaquo;</span>
</li>
@endif
</ul>
</nav>
@endif

View File

@ -0,0 +1,88 @@
@if ($paginator->hasPages())
<nav class="d-flex justify-items-center justify-content-between">
<div class="d-flex justify-content-between flex-fill d-sm-none">
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">@lang('pagination.previous')</span>
</li>
@else
<li class="page-item">
<a class="page-link" href="{{ $paginator->previousPageUrl() }}" rel="prev">@lang('pagination.previous')</a>
</li>
@endif
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<a class="page-link" href="{{ $paginator->nextPageUrl() }}" rel="next">@lang('pagination.next')</a>
</li>
@else
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">@lang('pagination.next')</span>
</li>
@endif
</ul>
</div>
<div class="d-none flex-sm-fill d-sm-flex align-items-sm-center justify-content-sm-between">
<div>
<p class="small text-muted">
{!! __('Showing') !!}
<span class="fw-semibold">{{ $paginator->firstItem() }}</span>
{!! __('to') !!}
<span class="fw-semibold">{{ $paginator->lastItem() }}</span>
{!! __('of') !!}
<span class="fw-semibold">{{ $paginator->total() }}</span>
{!! __('results') !!}
</p>
</div>
<div>
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
<span class="page-link" aria-hidden="true">&lsaquo;</span>
</li>
@else
<li class="page-item">
<a class="page-link" href="{{ $paginator->previousPageUrl() }}" rel="prev" aria-label="@lang('pagination.previous')">&lsaquo;</a>
</li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="page-item disabled" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="page-item active" aria-current="page"><span class="page-link">{{ $page }}</span></li>
@else
<li class="page-item"><a class="page-link" href="{{ $url }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<a class="page-link" href="{{ $paginator->nextPageUrl() }}" rel="next" aria-label="@lang('pagination.next')">&rsaquo;</a>
</li>
@else
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
<span class="page-link" aria-hidden="true">&rsaquo;</span>
</li>
@endif
</ul>
</div>
</div>
</nav>
@endif

View File

@ -0,0 +1,46 @@
@if ($paginator->hasPages())
<nav>
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
<span aria-hidden="true">&lsaquo;</span>
</li>
@else
<li>
<a href="{{ $paginator->previousPageUrl() }}" rel="prev" aria-label="@lang('pagination.previous')">&lsaquo;</a>
</li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="disabled" aria-disabled="true"><span>{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="active" aria-current="page"><span>{{ $page }}</span></li>
@else
<li><a href="{{ $url }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li>
<a href="{{ $paginator->nextPageUrl() }}" rel="next" aria-label="@lang('pagination.next')">&rsaquo;</a>
</li>
@else
<li class="disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
<span aria-hidden="true">&rsaquo;</span>
</li>
@endif
</ul>
</nav>
@endif

View File

@ -0,0 +1,36 @@
@if ($paginator->hasPages())
<div class="ui pagination menu" role="navigation">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<a class="icon item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')"> <i class="left chevron icon"></i> </a>
@else
<a class="icon item" href="{{ $paginator->previousPageUrl() }}" rel="prev" aria-label="@lang('pagination.previous')"> <i class="left chevron icon"></i> </a>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<a class="icon item disabled" aria-disabled="true">{{ $element }}</a>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<a class="item active" href="{{ $url }}" aria-current="page">{{ $page }}</a>
@else
<a class="item" href="{{ $url }}">{{ $page }}</a>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<a class="icon item" href="{{ $paginator->nextPageUrl() }}" rel="next" aria-label="@lang('pagination.next')"> <i class="right chevron icon"></i> </a>
@else
<a class="icon item disabled" aria-disabled="true" aria-label="@lang('pagination.next')"> <i class="right chevron icon"></i> </a>
@endif
</div>
@endif

View File

@ -0,0 +1,27 @@
@if ($paginator->hasPages())
<nav>
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">@lang('pagination.previous')</span>
</li>
@else
<li class="page-item">
<a class="page-link" href="{{ $paginator->previousPageUrl() }}" rel="prev">@lang('pagination.previous')</a>
</li>
@endif
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<a class="page-link" href="{{ $paginator->nextPageUrl() }}" rel="next">@lang('pagination.next')</a>
</li>
@else
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">@lang('pagination.next')</span>
</li>
@endif
</ul>
</nav>
@endif

View File

@ -0,0 +1,29 @@
@if ($paginator->hasPages())
<nav role="navigation" aria-label="{!! __('Pagination Navigation') !!}">
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">{!! __('pagination.previous') !!}</span>
</li>
@else
<li class="page-item">
<a class="page-link" href="{{ $paginator->previousPageUrl() }}" rel="prev">
{!! __('pagination.previous') !!}
</a>
</li>
@endif
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<a class="page-link" href="{{ $paginator->nextPageUrl() }}" rel="next">{!! __('pagination.next') !!}</a>
</li>
@else
<li class="page-item disabled" aria-disabled="true">
<span class="page-link">{!! __('pagination.next') !!}</span>
</li>
@endif
</ul>
</nav>
@endif

View File

@ -0,0 +1,19 @@
@if ($paginator->hasPages())
<nav>
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="disabled" aria-disabled="true"><span>@lang('pagination.previous')</span></li>
@else
<li><a href="{{ $paginator->previousPageUrl() }}" rel="prev">@lang('pagination.previous')</a></li>
@endif
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li><a href="{{ $paginator->nextPageUrl() }}" rel="next">@lang('pagination.next')</a></li>
@else
<li class="disabled" aria-disabled="true"><span>@lang('pagination.next')</span></li>
@endif
</ul>
</nav>
@endif

View File

@ -0,0 +1,25 @@
@if ($paginator->hasPages())
<nav role="navigation" aria-label="{!! __('Pagination Navigation') !!}" class="flex justify-between">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
{!! __('pagination.previous') !!}
</span>
@else
<a href="{{ $paginator->previousPageUrl() }}" rel="prev" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
{!! __('pagination.previous') !!}
</a>
@endif
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" rel="next" class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
{!! __('pagination.next') !!}
</a>
@else
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
{!! __('pagination.next') !!}
</span>
@endif
</nav>
@endif

View File

@ -0,0 +1,85 @@
@if ($paginator->hasPages())
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" class="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6">
{{-- Info text --}}
<div>
<p class="text-sm text-gray-500 leading-5">
Menampilkan
@if ($paginator->firstItem())
<span class="font-semibold text-gray-700">{{ $paginator->firstItem() }}</span>
sampai
<span class="font-semibold text-gray-700">{{ $paginator->lastItem() }}</span>
@else
{{ $paginator->count() }}
@endif
dari
<span class="font-semibold text-gray-700">{{ $paginator->total() }}</span>
data
</p>
</div>
{{-- Pagination Buttons --}}
<div class="flex items-center gap-1.5">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<span class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-gray-300 cursor-not-allowed" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/>
</svg>
</span>
@else
<a href="{{ $paginator->previousPageUrl() }}" rel="prev"
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-gray-500 bg-white border border-gray-200 shadow-sm hover:bg-indigo-50 hover:text-indigo-600 hover:border-indigo-200 transition-all duration-200"
aria-label="{{ __('pagination.previous') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/>
</svg>
</a>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<span class="inline-flex items-center justify-center w-9 h-9 text-sm text-gray-400 select-none" aria-disabled="true">
{{ $element }}
</span>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<span aria-current="page"
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-sm font-bold text-white bg-gradient-to-br from-indigo-500 to-blue-600 shadow-md shadow-indigo-200 cursor-default transition-all duration-200">
{{ $page }}
</span>
@else
<a href="{{ $url }}"
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-sm font-medium text-gray-600 bg-white border border-gray-200 shadow-sm hover:bg-indigo-50 hover:text-indigo-600 hover:border-indigo-200 hover:shadow-md transition-all duration-200"
aria-label="{{ __('Go to page :page', ['page' => $page]) }}">
{{ $page }}
</a>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" rel="next"
class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-gray-500 bg-white border border-gray-200 shadow-sm hover:bg-indigo-50 hover:text-indigo-600 hover:border-indigo-200 transition-all duration-200"
aria-label="{{ __('pagination.next') }}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
</svg>
</a>
@else
<span class="inline-flex items-center justify-center w-9 h-9 rounded-lg text-gray-300 cursor-not-allowed" aria-disabled="true">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
</svg>
</span>
@endif
</div>
</nav>
@endif

View File

@ -3,7 +3,7 @@
@section('content') @section('content')
<!-- Hero Section --> <!-- Hero Section -->
<div <div
class="relative bg-gradient-to-br from-blue-900 via-indigo-800 to-blue-900 py-16 sm:py-24 overflow-hidden shadow-2xl mb-12"> class="relative bg-gradient-to-br from-blue-900 via-indigo-800 to-blue-900 py-12 sm:py-24 overflow-hidden shadow-2xl mb-8 sm:mb-12">
<!-- Decorative background elements --> <!-- Decorative background elements -->
<div <div
class="absolute inset-0 opacity-10 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] mix-blend-overlay"> class="absolute inset-0 opacity-10 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] mix-blend-overlay">
@ -17,29 +17,31 @@ class="absolute bottom-0 left-0 -ml-20 -mb-20 w-72 h-72 rounded-full bg-indigo-3
<div class="container mx-auto px-4 relative z-10"> <div class="container mx-auto px-4 relative z-10">
<div class="text-center max-w-3xl mx-auto"> <div class="text-center max-w-3xl mx-auto">
<h1 class="text-4xl sm:text-5xl font-extrabold text-white mb-6 tracking-tight"> <h1
Eksplorasi <span class="text-transparent bg-clip-text bg-gradient-to-r from-blue-200 to-cyan-200">Katalog class="text-3xl sm:text-4xl md:text-5xl font-extrabold text-white mb-4 sm:mb-6 tracking-tight leading-tight">
Eksplorasi <span
class="text-transparent bg-clip-text bg-gradient-to-r from-blue-200 to-cyan-200">Katalog
Perpustakaan</span> Perpustakaan</span>
</h1> </h1>
<p class="text-blue-100 text-lg sm:text-xl mb-10 font-light"> <p class="text-blue-100 text-sm sm:text-lg md:text-xl mb-8 sm:mb-10 font-light px-2 sm:px-0">
Temukan ribuan koleksi buku, jurnal, dan referensi akademik untuk menginspirasi perjalanan literasi Temukan ribuan koleksi buku, jurnal, dan referensi akademik untuk menginspirasi perjalanan literasi
Anda. Anda.
</p> </p>
<!-- Search Bar --> <!-- Search Bar -->
<form action="{{ route('katalog.index') }}" method="GET" class="relative group max-w-2xl mx-auto"> <form action="{{ route('katalog.index') }}" method="GET"
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> class="relative group max-w-2xl mx-auto px-2 sm:px-0">
<svg class="h-6 w-6 text-gray-400 group-focus-within:text-blue-500 transition-colors" <div class="absolute inset-y-0 left-2 sm:left-0 pl-4 flex items-center pointer-events-none">
<svg class="h-5 w-5 sm:h-6 sm:w-6 text-gray-400 group-focus-within:text-blue-500 transition-colors"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
</div> </div>
<input type="text" name="search" value="{{ $search }}" <input type="text" name="search" value="{{ $search }}" placeholder="Cari judul buku, pengarang..."
placeholder="Cari judul buku, pengarang, atau penerbit..." class="block w-full pl-10 pr-24 sm:pl-12 sm:pr-32 py-3.5 sm:py-4 bg-white/95 backdrop-blur-sm border-0 rounded-xl sm:rounded-2xl shadow-xl focus:ring-4 focus:ring-blue-500/30 text-gray-800 text-sm sm:text-lg transition-all placeholder-gray-400">
class="block w-full pl-12 pr-32 py-4 bg-white/95 backdrop-blur-sm border-0 rounded-2xl shadow-xl focus:ring-4 focus:ring-blue-500/30 text-gray-800 text-lg transition-all placeholder-gray-400">
<button type="submit" <button type="submit"
class="absolute right-2 top-2 bottom-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold px-6 lg:px-8 rounded-xl shadow-md transition-all transform hover:scale-[1.02] active:scale-[0.98]"> class="absolute right-3.5 sm:right-2 top-1.5 bottom-1.5 sm:top-2 sm:bottom-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold px-5 sm:px-6 lg:px-8 rounded-lg sm:rounded-xl shadow-md transition-all transform hover:scale-[1.02] active:scale-[0.98] text-sm sm:text-base">
Cari Cari
</button> </button>
</form> </form>
@ -65,19 +67,14 @@ class="absolute right-2 top-2 bottom-2 bg-gradient-to-r from-blue-600 to-indigo-
class="group bg-white rounded-2xl shadow-sm hover:shadow-2xl border border-gray-100 overflow-hidden transition-all duration-300 transform {{ $habis ? 'grayscale opacity-75 cursor-not-allowed' : 'hover:-translate-y-1' }} flex flex-col h-full"> class="group bg-white rounded-2xl shadow-sm hover:shadow-2xl border border-gray-100 overflow-hidden transition-all duration-300 transform {{ $habis ? 'grayscale opacity-75 cursor-not-allowed' : 'hover:-translate-y-1' }} flex flex-col h-full">
<!-- Book Cover --> <!-- Book Cover -->
<div class="relative w-full pt-[140%] overflow-hidden bg-gray-50 flex-shrink-0"> <div class="relative w-full pt-[140%] overflow-hidden bg-gray-50 flex-shrink-0">
@if ($item->cover) @php
<img src="{{ asset('storage/' . $item->cover) }}" alt="{{ $item->judul }}" $coverPath = $item->cover ?? $item->sampul;
$finalPath = $coverPath ? (Str::contains($coverPath, '/') ? $coverPath : 'covers/' . $coverPath) : null;
@endphp
@if ($finalPath)
<img src="{{ asset('storage/' . $finalPath) }}" alt="{{ $item->judul }}"
class="absolute inset-0 w-full h-full object-cover {{ !$habis ? 'group-hover:scale-110' : '' }} transition-transform duration-700 ease-in-out" class="absolute inset-0 w-full h-full object-cover {{ !$habis ? 'group-hover:scale-110' : '' }} transition-transform duration-700 ease-in-out"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"> onerror="this.src='https://ui-avatars.com/api/?name={{ urlencode($item->judul) }}&background=F3F4F6&color=6366F1&size=512&bold=true';">
<div
class="hidden absolute inset-0 flex flex-col items-center justify-center text-gray-300 bg-gray-100 {{ !$habis ? 'group-hover:bg-gray-200' : '' }} transition-colors duration-300">
<svg class="w-12 h-12 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253">
</path>
</svg>
<span class="text-xs font-medium">Cover Tidak Tersedia</span>
</div>
@else @else
<div <div
class="absolute inset-0 flex flex-col items-center justify-center text-gray-300 bg-gray-100 {{ !$habis ? 'group-hover:bg-gray-200' : '' }} transition-colors duration-300"> class="absolute inset-0 flex flex-col items-center justify-center text-gray-300 bg-gray-100 {{ !$habis ? 'group-hover:bg-gray-200' : '' }} transition-colors duration-300">
@ -92,8 +89,7 @@ class="absolute inset-0 flex flex-col items-center justify-center text-gray-300
@if ($habis) @if ($habis)
<!-- Overlay Dipinjam Semua --> <!-- Overlay Dipinjam Semua -->
<div <div class="absolute inset-0 bg-red-900/30 backdrop-blur-[1px] flex items-center justify-center z-10">
class="absolute inset-0 bg-red-900/30 backdrop-blur-[1px] flex items-center justify-center z-10">
<div <div
class="bg-red-600/90 text-white font-black text-sm md:text-base px-6 py-2 border-y-2 border-white transform -rotate-12 shadow-2xl tracking-widest uppercase pointer-events-none"> class="bg-red-600/90 text-white font-black text-sm md:text-base px-6 py-2 border-y-2 border-white transform -rotate-12 shadow-2xl tracking-widest uppercase pointer-events-none">
TIDAK TERSEDIA TIDAK TERSEDIA
@ -229,4 +225,4 @@ class="inline-flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-
</div> </div>
@endif @endif
</div> </div>
@endsection @endsection

View File

@ -202,9 +202,14 @@ class="absolute right-1 top-1 bottom-1 w-10 flex items-center justify-center bg-
<div class="md:col-span-12 lg:col-span-3"> <div class="md:col-span-12 lg:col-span-3">
<div <div
class="bg-white rounded-3xl shadow-[0_10px_40px_-10px_rgba(0,0,0,0.08)] border border-gray-50 p-5 flex flex-col items-center text-center overflow-hidden transition-transform duration-500 hover:-translate-y-2"> class="bg-white rounded-3xl shadow-[0_10px_40px_-10px_rgba(0,0,0,0.08)] border border-gray-50 p-5 flex flex-col items-center text-center overflow-hidden transition-transform duration-500 hover:-translate-y-2">
@if ($buku->cover) @php
<img src="{{ asset('storage/' . $buku->cover) }}" alt="{{ $buku->judul }}" $coverPath = $buku->cover ?? $buku->sampul;
class="w-full aspect-[3/4] object-cover rounded-2xl mb-6 shadow-md border border-gray-100"> $finalPath = $coverPath ? (Str::contains($coverPath, '/') ? $coverPath : 'covers/' . $coverPath) : null;
@endphp
@if ($finalPath)
<img src="{{ asset('storage/' . $finalPath) }}" alt="{{ $buku->judul }}"
class="w-full aspect-[3/4] object-cover rounded-2xl mb-6 shadow-md border border-gray-100"
onerror="this.src='https://ui-avatars.com/api/?name={{ urlencode($buku->judul) }}&background=F3F4F6&color=6366F1&size=512&bold=true';">
@else @else
<div <div
class="w-full aspect-[3/4] bg-gradient-to-br from-indigo-50 to-blue-50 rounded-2xl flex items-center justify-center mb-6 overflow-hidden relative shadow-inner border border-blue-100/50"> class="w-full aspect-[3/4] bg-gradient-to-br from-indigo-50 to-blue-50 rounded-2xl flex items-center justify-center mb-6 overflow-hidden relative shadow-inner border border-blue-100/50">
@ -279,21 +284,32 @@ class="text-blue-600 font-bold text-sm">{{ $buku->kategori->nama_kategori ?? $lo
<div class="w-full lg:w-96 flex-shrink-0 flex flex-col gap-6 mx-auto relative"> <div class="w-full lg:w-96 flex-shrink-0 flex flex-col gap-6 mx-auto relative">
<!-- MAP CONTAINER --> <!-- MAP CONTAINER RESPONSIVE -->
<div class="map-container shadow-sm border-2 border-slate-300 select-none font-sans min-h-[300px]" <div class="relative inline-block w-full max-w-4xl shadow-sm border-2 border-slate-300 select-none font-sans overflow-hidden rounded-lg">
id="mapContainer">
<!-- Background Image Map --> <!-- Background Image Map -->
<img src="{{ asset('img/img 2.png') }}" alt="Denah Perpustakaan" id="mapImage"> <img src="{{ asset('img/denah.webp') }}" loading="lazy" alt="Denah Perpustakaan" class="w-full h-auto block">
<!-- Pin Container (JavaScript akan merender pin di sini) --> @if ($buku->lokasi_x && $buku->lokasi_y)
<div id="pinContainer"></div> <!-- Pin Marker (Render via Blade) -->
<div class="absolute -translate-x-1/2 -translate-y-full flex flex-col items-center"
style="top: {{ $buku->lokasi_y }}%; left: {{ $buku->lokasi_x }}%; margin-top: 4px;">
<!-- Tooltip / Label -->
<div class="bg-blue-700 text-white text-[10px] sm:text-xs font-bold px-2 py-1 rounded shadow-md mb-1 whitespace-nowrap">
{{ $lokasi['rak'] }} {{ $lokasi['area'] }}
</div>
@if (!$buku->lokasi_x || !$buku->lokasi_y) <!-- Ikon Pin Merah -->
<div class="relative flex flex-col items-center animate-bounce">
<i class="fas fa-map-marker-alt text-red-600 text-3xl sm:text-4xl drop-shadow-md"></i>
</div>
</div>
@else
<!-- Overlay Not Found --> <!-- Overlay Not Found -->
<div class="map-overlay-notfound"> <div class="absolute inset-0 flex flex-col items-center justify-center bg-white/70 backdrop-blur-sm z-10 text-center p-4">
<i class="fas fa-exclamation-triangle text-red-500 text-3xl mb-2 animate-pulse"></i> <i class="fas fa-exclamation-triangle text-red-500 text-3xl sm:text-4xl mb-2 animate-pulse"></i>
<h4 class="font-bold text-red-600 text-sm sm:text-base">Lokasi Rak Belum Dipetakan</h4> <h4 class="font-bold text-red-600 text-sm sm:text-lg">Lokasi Rak Belum Dipetakan</h4>
<p class="text-xs text-gray-500 mt-1">Silakan hubungi petugas perpustakaan untuk bantuan.</p> <p class="text-xs sm:text-sm text-gray-700 mt-1">Silakan hubungi petugas perpustakaan untuk bantuan.</p>
</div> </div>
@endif @endif
</div> </div>
@ -419,69 +435,5 @@ class="text-sm font-semibold text-blue-600 hover:text-blue-800 flex items-center
@endsection @endsection
@push('scripts') @push('scripts')
@php <!-- Skrip lama pembuat pin JS sudah dihapus dan digantikan full Tailwind CSS di HTML -->
$pinDataJson = [
'x' => $buku->lokasi_x,
'y' => $buku->lokasi_y,
'label' => $lokasi['rak'] . ' — ' . $lokasi['area'],
];
@endphp
<script>
document.addEventListener('DOMContentLoaded', function() {
// Data koordinat dari Laravel
const pinData = @json($pinDataJson);
const container = document.getElementById('pinContainer');
if (!container || !pinData.x || !pinData.y) return;
/**
* Membuat elemen pin pointer dan menambahkannya ke container.
* @param {number} x - Posisi X dalam persentase (0-100)
* @param {number} y - Posisi Y dalam persentase (0-100)
* @param {string} label - Label teks untuk pin
* @param {boolean} isActive - Apakah pin ini aktif (bounce + label visible)
*/
function renderPin(x, y, label, isActive) {
// Wrapper pin
const pin = document.createElement('div');
pin.className = 'map-pin' + (isActive ? ' active' : '');
pin.style.left = x + '%';
pin.style.top = y + '%';
// Ikon pin (circle + tail)
const icon = document.createElement('div');
icon.className = 'map-pin__icon';
const circle = document.createElement('div');
circle.className = 'map-pin__circle';
circle.innerHTML = '<i class="fas fa-map-marker-alt"></i>';
const tail = document.createElement('div');
tail.className = 'map-pin__tail';
icon.appendChild(circle);
icon.appendChild(tail);
pin.appendChild(icon);
// Pulse effect
const pulse = document.createElement('div');
pulse.className = 'map-pin__pulse';
pin.appendChild(pulse);
// Label tooltip
if (label) {
const labelEl = document.createElement('span');
labelEl.className = 'map-pin__label';
labelEl.textContent = label;
pin.appendChild(labelEl);
}
container.appendChild(pin);
return pin;
}
// Render pin utama buku ini
renderPin(pinData.x, pinData.y, pinData.label, true);
});
</script>
@endpush @endpush

View File

@ -11,61 +11,4 @@
@include('welcome.rekomendasi') @include('welcome.rekomendasi')
</section> </section>
<div id="guestModal" class="fixed inset-0 bg-gray-900/60 backdrop-blur-sm hidden z-[100] flex items-center justify-center p-4 transition-opacity duration-300"> @endsection
<div class="bg-white rounded-[2rem] shadow-2xl max-w-md w-full overflow-hidden transform transition-all">
<div class="p-8 text-center relative">
<!-- Close Button -->
<button onclick="closeGuestModal()" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors w-8 h-8 rounded-full hover:bg-gray-100 flex items-center justify-center">
<i class="fas fa-times"></i>
</button>
<!-- Icon Box -->
<div class="w-16 h-16 bg-gradient-to-br from-primary-500 to-primary-600 text-white rounded-2xl flex items-center justify-center mx-auto mb-5 text-2xl shadow-lg shadow-primary-200">
<i class="fas fa-user-check"></i>
</div>
<h3 class="text-2xl font-black text-gray-900 tracking-tight">Selamat Datang!</h3>
<p class="text-gray-500 text-sm mt-2 leading-relaxed">Apakah Anda sudah terdaftar sebagai anggota Sarakata?</p>
<div class="grid grid-cols-1 gap-3 mt-8">
<!-- Member Option -->
<a href="{{ route('buku_tamu.index') }}" class="flex items-center gap-4 p-4 border-2 border-primary-100 rounded-2xl hover:border-primary-300 hover:bg-primary-50/50 transition-all group cursor-pointer shadow-sm hover:shadow-md">
<div class="w-12 h-12 bg-primary-100 rounded-xl flex items-center justify-center text-primary-600 group-hover:bg-gradient-to-br group-hover:from-primary-500 group-hover:to-primary-600 group-hover:text-white transition-all shadow-sm group-hover:shadow-primary-300">
<i class="fas fa-id-card"></i>
</div>
<div class="text-left flex-1 border-gray-100">
<div class="font-bold text-gray-900 text-sm">Saya Anggota</div>
<div class="text-xs text-gray-400">Punya No. Anggota</div>
</div>
<i class="fas fa-chevron-right text-primary-300 group-hover:text-primary-500 transition-colors"></i>
</a>
<!-- Visitor Option -->
<a href="{{ route('buku_tamu.index') }}" class="flex items-center gap-4 p-4 border-2 border-gray-100 rounded-2xl hover:border-gray-300 hover:bg-gray-50 transition-all group cursor-pointer shadow-sm hover:shadow-md">
<div class="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center text-gray-500 group-hover:bg-gray-800 group-hover:text-white transition-all shadow-sm">
<i class="fas fa-user-friends"></i>
</div>
<div class="text-left flex-1 border-gray-100">
<div class="font-bold text-gray-900 text-sm">Pengunjung Umum</div>
<div class="text-xs text-gray-400">Belum punya member</div>
</div>
<i class="fas fa-chevron-right text-gray-300 group-hover:text-gray-600 transition-colors"></i>
</a>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
function openGuestModal() {
document.getElementById('guestModal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function closeGuestModal() {
document.getElementById('guestModal').classList.add('hidden');
document.body.style.overflow = 'auto';
}
</script>
@endpush

View File

@ -5,58 +5,128 @@
<div class="absolute bottom-10 right-10 w-96 h-96 bg-purple-200/20 blob blur-3xl"></div> <div class="absolute bottom-10 right-10 w-96 h-96 bg-purple-200/20 blob blur-3xl"></div>
<div class="absolute top-40 right-1/3 w-48 h-48 bg-blue-200/20 rounded-full blur-2xl"></div> <div class="absolute top-40 right-1/3 w-48 h-48 bg-blue-200/20 rounded-full blur-2xl"></div>
<div class="max-w-7xl mx-auto px-6 py-24 md:py-32 grid md:grid-cols-2 gap-16 items-center relative z-10"> <div
class="max-w-7xl mx-auto px-4 sm:px-6 py-14 sm:py-20 md:py-26
grid md:grid-cols-2 gap-10 md:gap-16 items-center relative z-10">
<div class="fade-up"> <div class="fade-up">
{{-- Badge --}} {{-- Badge --}}
<div class="inline-flex items-center gap-2 px-4 py-2 bg-white/80 rounded-full shadow-sm border border-primary-100 mb-8"> <div
class="inline-flex items-center gap-2 px-3 py-2 bg-white/80
rounded-full shadow-sm border border-primary-100 mb-6">
<span class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span> <span class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
<span class="text-xs font-semibold text-primary-700 tracking-wide">Sistem Informasi Perpustakaan</span>
<span class="text-[11px] sm:text-xs font-semibold text-primary-700 tracking-wide">
Sistem Informasi Perpustakaan
</span>
</div> </div>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-black leading-[1.1] text-gray-900 tracking-tight"> <h1
class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl
font-black leading-tight text-gray-900 tracking-tight">
Sistem Informasi Perpustakaan Sistem Informasi Perpustakaan
<span class="gradient-text block mt-2">Daerah Jember</span>
<span class="gradient-text block mt-1 sm:mt-2">
Daerah Jember
</span>
</h1> </h1>
<p class="mt-6 text-lg text-gray-500 max-w-lg leading-relaxed"> <p
Akses ribuan koleksi buku, jurnal, dan referensi akademik dalam satu platform terintegrasi kapan saja, di mana saja. class="mt-5 text-sm sm:text-base md:text-lg text-gray-500
max-w-lg leading-relaxed">
Akses ribuan koleksi buku, jurnal, dan referensi akademik
dalam satu platform terintegrasi kapan saja, di mana saja.
</p> </p>
<div class="mt-10 flex flex-col sm:flex-row gap-4 flex-wrap"> {{-- BUTTONS --}}
<a href="{{ route('katalog.index') }}" class="px-8 py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-2xl font-bold text-sm shadow-xl shadow-blue-200 hover:shadow-blue-300 hover:from-blue-700 hover:to-blue-800 transition-all inline-flex items-center gap-2 justify-center"> <div class="mt-8 flex flex-col gap-3 sm:flex-row sm:flex-wrap">
<i class="fas fa-search"></i> Cari Katalog Buku
<a href="{{ route('katalog.index') }}"
class="w-full sm:w-auto px-6 py-3.5
bg-gradient-to-r from-blue-600 to-blue-700
text-white rounded-xl sm:rounded-2xl
font-bold text-sm shadow-xl shadow-blue-200
hover:from-blue-700 hover:to-blue-800
transition-all inline-flex items-center
gap-2 justify-center">
<i class="fas fa-search"></i>
Cari Katalog Buku
</a> </a>
<a href="{{ route('buku_tamu.index') }}" class="px-8 py-4 bg-gradient-to-r from-primary-600 to-primary-700 text-white rounded-2xl font-bold text-sm shadow-xl shadow-primary-200 hover:shadow-primary-300 hover:from-primary-700 hover:to-primary-800 transition-all inline-flex items-center gap-2 justify-center">
<i class="fas fa-book-reader"></i> Mulai Kunjungan <a href="{{ route('buku_tamu.index') }}"
class="w-full sm:w-auto px-6 py-3.5
bg-gradient-to-r from-primary-600 to-primary-700
text-white rounded-xl sm:rounded-2xl
font-bold text-sm shadow-xl shadow-primary-200
hover:from-primary-700 hover:to-primary-800
transition-all inline-flex items-center
gap-2 justify-center">
<i class="fas fa-book-reader"></i>
Mulai Kunjungan
</a> </a>
<a href="#fitur" class="px-8 py-4 bg-white text-gray-700 rounded-2xl font-bold text-sm shadow-sm border border-gray-200 hover:border-primary-200 hover:text-primary-600 transition-all inline-flex items-center gap-2 justify-center">
<i class="fas fa-info-circle"></i> Pelajari Lebih Lanjut <a href="#fitur"
class="w-full sm:w-auto px-6 py-3.5
bg-white text-gray-700 rounded-xl sm:rounded-2xl
font-bold text-sm shadow-sm border border-gray-200
hover:border-primary-200 hover:text-primary-600
transition-all inline-flex items-center
gap-2 justify-center">
<i class="fas fa-info-circle"></i>
Pelajari Lebih Lanjut
</a> </a>
</div> </div>
{{-- Stats --}} {{-- STATS --}}
<div class="mt-14 flex gap-10"> <div class="mt-10 grid grid-cols-3 gap-3 sm:gap-6">
<div>
<div class="text-3xl font-black text-gray-900">1000+</div> <div class="bg-white/50 rounded-xl p-3 sm:p-4 text-center">
<div class="text-xs text-gray-400 font-medium mt-1">Koleksi Buku</div> <div class="text-xl sm:text-3xl font-black text-gray-900">
1000+
</div>
<div class="text-[10px] sm:text-xs text-gray-400 font-medium mt-1">
Koleksi Buku
</div>
</div> </div>
<div class="border-l border-gray-200 pl-10">
<div class="text-3xl font-black text-gray-900">500+</div> <div class="bg-white/50 rounded-xl p-3 sm:p-4 text-center">
<div class="text-xs text-gray-400 font-medium mt-1">Anggota Aktif</div> <div class="text-xl sm:text-3xl font-black text-gray-900">
500+
</div>
<div class="text-[10px] sm:text-xs text-gray-400 font-medium mt-1">
Anggota Aktif
</div>
</div> </div>
<div class="border-l border-gray-200 pl-10">
<div class="text-3xl font-black text-gray-900">24/7</div> <div class="bg-white/50 rounded-xl p-3 sm:p-4 text-center">
<div class="text-xs text-gray-400 font-medium mt-1">Akses Digital</div> <div class="text-xl sm:text-3xl font-black text-gray-900">
24/7
</div>
<div class="text-[10px] sm:text-xs text-gray-400 font-medium mt-1">
Akses Digital
</div>
</div> </div>
</div> </div>
</div> </div>
{{-- IMAGE --}}
<div class="hidden md:flex justify-center relative items-center"> <div class="hidden md:flex justify-center relative items-center">
<div class="absolute inset-0 bg-gradient-to-tr from-primary-400/10 to-purple-400/10 rounded-[3rem] blur-2xl"></div> <div
class="absolute inset-0 bg-gradient-to-tr
from-primary-400/10 to-purple-400/10
rounded-[3rem] blur-2xl">
</div>
<img src="{{ asset('img/buku sampul awal.png') }}" <img src="{{ asset('img/buku sampul awal.png') }}"
class="w-[600px] object-contain drop-shadow-2xl relative z-10 rounded-3xl" class="w-[500px] lg:w-[600px] object-contain
alt="Animasi Perpustakaan Digital"> drop-shadow-2xl relative z-10 rounded-3xl"
alt="Animasi Perpustakaan Digital">
</div> </div>
</div> </div>

View File

@ -6,8 +6,7 @@
<div <div
class="flex flex-col md:flex-row justify-between items-start md:items-center mb-10 gap-4 text-center md:text-left"> class="flex flex-col md:flex-row justify-between items-start md:items-center mb-10 gap-4 text-center md:text-left">
<div class="w-full md:w-auto"> <div class="w-full md:w-auto">
<div <div class="inline-flex items-center gap-2 px-4 py-2 bg-blue-100 rounded-full mb-4 mx-auto md:mx-0">
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-100 rounded-full mb-4 mx-auto md:mx-0">
<i class="fas fa-fire text-red-500 text-xs"></i> <i class="fas fa-fire text-red-500 text-xs"></i>
<span class="text-xs font-semibold text-blue-800 tracking-wide">Paling Laris</span> <span class="text-xs font-semibold text-blue-800 tracking-wide">Paling Laris</span>
</div> </div>
@ -39,15 +38,18 @@ class="bg-blue-600/90 backdrop-blur-md text-white text-[10px] sm:text-xs font-bo
<!-- Book Cover --> <!-- Book Cover -->
<div class="relative w-full pt-[140%] overflow-hidden bg-gray-50 flex-shrink-0"> <div class="relative w-full pt-[140%] overflow-hidden bg-gray-50 flex-shrink-0">
@if ($item->cover ?? $item->sampul) @php
<img src="{{ asset('storage/' . ($item->cover ?? $item->sampul)) }}" $coverPath = $item->cover ?? $item->sampul;
alt="{{ $item->judul }}" $finalPath = $coverPath ? (Str::contains($coverPath, '/') ? $coverPath : 'covers/' . $coverPath) : null;
class="absolute inset-0 w-full h-full object-cover {{ !$habis ? 'group-hover:scale-110' : '' }} transition-transform duration-700 ease-in-out"> @endphp
@if ($finalPath)
<img src="{{ asset('storage/' . $finalPath) }}" alt="{{ $item->judul }}"
class="absolute inset-0 w-full h-full object-cover {{ !$habis ? 'group-hover:scale-110' : '' }} transition-transform duration-700 ease-in-out"
onerror="this.src='https://ui-avatars.com/api/?name={{ urlencode($item->judul) }}&background=F3F4F6&color=6366F1&size=512&bold=true';">
@else @else
<div <div
class="absolute inset-0 flex flex-col items-center justify-center text-gray-300 bg-gray-100 {{ !$habis ? 'group-hover:bg-gray-200' : '' }} transition-colors duration-300"> class="absolute inset-0 flex flex-col items-center justify-center text-gray-300 bg-gray-100 {{ !$habis ? 'group-hover:bg-gray-200' : '' }} transition-colors duration-300">
<svg class="w-12 h-12 mb-2" fill="none" stroke="currentColor" <svg class="w-12 h-12 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"> d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253">
</path> </path>
@ -113,4 +115,4 @@ class="block w-full text-center bg-gray-50 hover:bg-blue-600 text-gray-700 hover
</div> </div>
@endif @endif
</div> </div>
</section> </section>

View File

@ -42,15 +42,19 @@ class="bg-gradient-to-r from-yellow-400 to-orange-500 text-white text-[9px] sm:t
<!-- Book Cover --> <!-- Book Cover -->
<div class="relative w-full pt-[140%] overflow-hidden bg-gray-50 flex-shrink-0"> <div class="relative w-full pt-[140%] overflow-hidden bg-gray-50 flex-shrink-0">
@if ($item->cover ?? $item->sampul) @php
<img src="{{ asset('storage/' . ($item->cover ?? $item->sampul)) }}" $coverPath = $item->cover ?? $item->sampul;
alt="{{ $item->judul }}" // Cek apakah path sudah mengandung folder, jika tidak default ke 'covers/'
class="absolute inset-0 w-full h-full object-cover {{ !$habis ? 'group-hover:scale-110' : '' }} transition-transform duration-700 ease-in-out"> $finalPath = $coverPath ? (Str::contains($coverPath, '/') ? $coverPath : 'covers/' . $coverPath) : null;
@endphp
@if ($finalPath)
<img src="{{ asset('storage/' . $finalPath) }}" alt="{{ $item->judul }}"
class="absolute inset-0 w-full h-full object-cover {{ !$habis ? 'group-hover:scale-110' : '' }} transition-transform duration-700 ease-in-out"
onerror="this.src='https://ui-avatars.com/api/?name={{ urlencode($item->judul) }}&background=EBF4FF&color=7F9CF5&size=512&bold=true';">
@else @else
<div <div
class="absolute inset-0 flex flex-col items-center justify-center text-gray-300 bg-gray-100 {{ !$habis ? 'group-hover:bg-gray-200' : '' }} transition-colors duration-300"> class="absolute inset-0 flex flex-col items-center justify-center text-gray-300 bg-gray-100 {{ !$habis ? 'group-hover:bg-gray-200' : '' }} transition-colors duration-300">
<svg class="w-12 h-12 mb-2" fill="none" stroke="currentColor" <svg class="w-12 h-12 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"> d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253">
</path> </path>
@ -124,4 +128,4 @@ class="block w-full text-center bg-gray-50 hover:bg-blue-600 text-gray-700 hover
</div> </div>
@endif @endif
</div> </div>
</section> </section>

View File

@ -129,4 +129,5 @@
Route::get('/admin/buku/{id}/edit', [AdminBukuController::class, 'edit'])->name('admin.buku.edit'); Route::get('/admin/buku/{id}/edit', [AdminBukuController::class, 'edit'])->name('admin.buku.edit');
Route::put('/admin/buku/{id}', [AdminBukuController::class, 'update'])->name('admin.buku.update'); Route::put('/admin/buku/{id}', [AdminBukuController::class, 'update'])->name('admin.buku.update');
Route::delete('/admin/buku/{id}', [AdminBukuController::class, 'destroy'])->name('admin.buku.destroy'); Route::delete('/admin/buku/{id}', [AdminBukuController::class, 'destroy'])->name('admin.buku.destroy');
Route::get('/admin/peminjaman/{id}/struk', [AdminPeminjamanController::class, 'cetakStruk'])->name('admin.peminjaman.struk'); Route::get('/admin/peminjaman/{id}/struk', [AdminPeminjamanController::class, 'cetakStruk'])->name('admin.peminjaman.struk');
Route::post('/admin/peminjaman/{id}/resend-wa', [AdminPeminjamanController::class, 'resendWa'])->name('admin.peminjaman.resend_wa');

64
update_lokasi.php Normal file
View File

@ -0,0 +1,64 @@
<?php
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
$bukus = App\Models\Buku::all();
$count = 0;
foreach ($bukus as $buku) {
if (empty($buku->nomor_panggil)) continue;
$kode_utama = (int) substr(trim(preg_replace('/[^0-9]/', '', $buku->nomor_panggil)), 0, 3);
$koordinat = match (true) {
$kode_utama >= 0 && $kode_utama <= 99 => match (true) {
$kode_utama <= 19 => ['x' => 12.00, 'y' => 13.00], // Rak 01
$kode_utama <= 50 => ['x' => 17.00, 'y' => 13.00], // Rak 02
default => ['x' => 22.00, 'y' => 13.00], // Rak 03-05
},
$kode_utama >= 100 && $kode_utama <= 199 => match (true) {
$kode_utama <= 150 => ['x' => 35.00, 'y' => 13.00], // Rak 06-10
default => ['x' => 35.00, 'y' => 17.00], // Rak 11-14
},
$kode_utama >= 200 && $kode_utama <= 299 => match (true) {
$kode_utama == 297 => ['x' => 14.00, 'y' => 38.00], // Rak 25-32 (Islam)
default => ['x' => 14.00, 'y' => 30.00], // Rak 15-24
},
$kode_utama >= 300 && $kode_utama <= 399 => match (true) {
$kode_utama <= 330 => ['x' => 38.00, 'y' => 23.00], // Rak 33-36
$kode_utama <= 360 => ['x' => 43.00, 'y' => 23.00], // Rak 37-40
default => ['x' => 48.00, 'y' => 23.00], // Rak 41-44
},
$kode_utama >= 400 && $kode_utama <= 499 => ['x' => 14.00, 'y' => 58.00], // Rak 45
$kode_utama >= 500 && $kode_utama <= 599 => ['x' => 38.00, 'y' => 72.00], // Rak 46-48
$kode_utama >= 600 && $kode_utama <= 699 => match (true) {
$kode_utama <= 610 => ['x' => 28.00, 'y' => 83.00], // Rak 49-53
$kode_utama <= 630 => ['x' => 36.00, 'y' => 83.00], // Rak 54-58
$kode_utama <= 650 => ['x' => 44.00, 'y' => 83.00], // Rak 59-63
default => ['x' => 52.00, 'y' => 83.00], // Rak 64-68
},
$kode_utama >= 700 && $kode_utama <= 799 => match (true) {
$kode_utama <= 739 => ['x' => 82.00, 'y' => 12.00], // Rak 71
$kode_utama <= 769 => ['x' => 86.00, 'y' => 12.00], // Rak 72
$kode_utama <= 789 => ['x' => 82.00, 'y' => 16.00], // Rak 73
default => ['x' => 86.00, 'y' => 16.00], // Rak 74
},
$kode_utama >= 800 && $kode_utama <= 899 => ['x' => 82.00, 'y' => 25.00], // Rak 77-79
$kode_utama >= 900 && $kode_utama <= 999 => match (true) {
$kode_utama <= 919 => ['x' => 82.00, 'y' => 30.00], // Rak 69-70
default => ['x' => 82.00, 'y' => 35.00], // Rak 80-84
},
default => ['x' => null, 'y' => null],
};
if ($koordinat['x'] !== null) {
$buku->lokasi_x = $koordinat['x'];
$buku->lokasi_y = $koordinat['y'];
$buku->save();
$count++;
}
}
echo "Berhasil update $count buku presisi.";