feat: Implement custom borrow/due dates for loans, update profile statistics, and refine book management forms.

This commit is contained in:
cukiprit 2026-03-09 01:19:08 +07:00
parent b0c7a8b2cf
commit 3fb658fe9c
11 changed files with 65 additions and 56 deletions

View File

@ -28,6 +28,7 @@ public function index(Request $request)
'id_peminjaman' => 'PIN-ADM-'.sprintf('%03d', $userId),
'user_id' => $userId,
'peminjam' => $user->nama_lengkap ?? 'Unknown',
'email' => $user->email,
'nomor_hp' => $user->phone ?? '-',
'tanggal_pinjam' => $firstLoan->borrowed_at,
'tenggat_kembali' => $firstLoan->due_at,

View File

@ -24,7 +24,7 @@ public function index()
$bukuPinjamOffline = $loans->map(function ($loan) {
$dueAt = Carbon::parse($loan->due_at);
$sisaHari = (int) now()->diffInDays($dueAt, false);
$sisaHari = (int) now()->startOfDay()->diffInDays($dueAt->startOfDay(), false);
return [
'id' => $loan->book->id,

View File

@ -98,12 +98,23 @@ public function store(Request $request)
{
$request->validate([
'buku_ids' => 'required|array|min:1|max:3',
'buku_ids.*' => 'exists:books,id'
'buku_ids.*' => 'exists:books,id',
'tanggal_pinjam' => 'required',
'tanggal_kembali' => 'required',
]);
$bukuIds = $request->input('buku_ids');
// Parse dates from format "Y-m-d" (standard from flatpickr)
try {
$borrowedAt = Carbon::parse($request->tanggal_pinjam);
$dueAt = Carbon::parse($request->tanggal_kembali);
} catch (\Exception $e) {
$borrowedAt = now();
$dueAt = now()->addDays(7);
}
DB::transaction(function () use ($bukuIds) {
DB::transaction(function () use ($bukuIds, $borrowedAt, $dueAt) {
foreach ($bukuIds as $bukuId) {
$book = Book::lockForUpdate()->find($bukuId);
@ -115,8 +126,8 @@ public function store(Request $request)
'user_id' => Auth::id(),
'book_id' => $bukuId,
'loan_code' => 'PIN-' . date('Ym') . '-' . strtoupper(Str::random(4)) . '-' . $bukuId,
'borrowed_at' => now(),
'due_at' => now()->addDays(7),
'borrowed_at' => $borrowedAt,
'due_at' => $dueAt,
'status' => 'Dipinjam',
]);

View File

@ -99,9 +99,9 @@ public function index(Request $request): \Illuminate\View\View|\Illuminate\Http\
]);
$viewData['statistik'] = [
['label' => 'Buku dipinjam', 'value' => $loans->count(), 'icon' => 'bi-book-half', 'color' => 'primary'],
['label' => 'Tenggat Waktu', 'value' => $viewData['bukuOffline']->where('sisa_hari', '<=', 3)->where('sisa_hari', '>=', 0)->count(), 'icon' => 'bi-clock-history', 'color' => 'danger'],
['label' => 'Buku dikembalikan', 'value' => Loan::where('user_id', $user->id)->where('status', 'Dikembalikan')->count(), 'icon' => 'bi-check-circle', 'color' => 'success'],
['label' => 'Buku yang dipinjam', 'value' => $loans->count(), 'icon' => 'bi-journal-bookmark-fill', 'color' => 'primary'],
['label' => 'Tenggat Waktu', 'value' => $viewData['bukuOffline']->where('sisa_hari', '<=', 3)->count(), 'icon' => 'bi-clock-fill', 'color' => 'danger'],
['label' => 'Buku dikembalikan', 'value' => Loan::where('user_id', $user->id)->where('status', 'Dikembalikan')->count(), 'icon' => 'bi-check-circle-fill', 'color' => 'success'],
['label' => 'History Baca', 'value' => Loan::where('user_id', $user->id)->count(), 'icon' => 'bi-hourglass-split', 'color' => 'warning'],
];
@ -142,11 +142,11 @@ public function edit(Request $request): View
/**
* Update data profil ke Database MySQL.
*/
public function update(Request $request): RedirectResponse
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$user = $request->user();
$user->fill($request->validated());
$user->name = $user->nama_lengkap; // Sync for compatibility
$user->nama_lengkap = $user->name; // Sync for compatibility
if ($user->isDirty('email')) {
$user->email_verified_at = null;

View File

@ -16,7 +16,7 @@ class ProfileUpdateRequest extends FormRequest
public function rules(): array
{
return [
'nama_lengkap' => ['required', 'string', 'max:255'],
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',

View File

@ -54,7 +54,7 @@
<div class="col-md-3 mb-3">
<label for="stok" class="form-label">Stok</label>
<input type="text" name="stok" class="form-control" id="stok"
value="1" min="0" required>
value="1" min="0" required oninput="this.value = this.value.replace(/[^0-9]/g, '')">
</div>
</div>
</div>

View File

@ -51,8 +51,8 @@
</div>
<div class="col-md-4 mb-3">
<label for="stok" class="form-label">Stok</label>
<input type="number" name="stok" class="form-control" id="stok"
value="{{ old('stok', $buku->stok ?? 1) }}" min="0" required>
<input type="text" name="stok" class="form-control" id="stok"
value="{{ old('stok', $buku->stok ?? 1) }}" required oninput="this.value = this.value.replace(/[^0-9]/g, '')">
</div>
</div>

View File

@ -212,6 +212,7 @@
<button type="button" class="btn btn-primary btn-konfirmasi-kembali"
data-nama-peminjam="{{ $transaksi['peminjam'] }}"
data-nomor-hp="{{ $transaksi['nomor_hp'] }}"
data-email="{{ $transaksi['email'] }}"
data-user-id="{{ $transaksi['user_id'] }}">
Konfirmasi & Selesai
</button>
@ -346,12 +347,15 @@ function hitungTotalDenda(modal) {
});
const userId = modalEl.find('.btn-konfirmasi-kembali').data('user-id');
const userEmail = modalEl.find('.btn-konfirmasi-kembali').data('email');
const totalDenda = dendaOverdueTotal + totalDendaRusak;
// Construct WA Message
let waLink = null;
if (isWaChecked && hp && hp !== '-') {
let phone = hp;
// Clean phone number from non-numeric characters
phone = phone.replace(/[^0-9]/g, '');
if (phone.startsWith('0')) phone = '62' + phone.substring(1);
let message = `*BUKTI PENGEMBALIAN BUKU*\n\n`;
@ -371,10 +375,9 @@ function hitungTotalDenda(modal) {
// Loading Kirim Email flow
if (isEmailChecked) {
const dummyEmail = nama.replace(/\s+/g, '.').toLowerCase() + '@sekolah.sch.id';
modernSwal.fire({
title: 'Mengirim Email...',
html: `Mengirim nota ke: <b>${dummyEmail}</b>`,
html: `Mengirim nota ke: <b>${userEmail}</b>`,
timer: 2000,
timerProgressBar: true,
didOpen: () => Swal.showLoading()

View File

@ -296,7 +296,7 @@ class="bi bi-check-circle me-1"></i>Tersedia</span>
<h6 class="fw-bold text-dark mb-1">PENTING: Aturan Peminjaman!</h6>
<p class="mb-0 text-muted small">
Sesuai peraturan perpustakaan, durasi peminjaman buku maksimal adalah
<strong class="text-dark bg-warning-subtle px-2 py-1 rounded">2 HARI</strong>.
<strong class="text-dark bg-warning-subtle px-2 py-1 rounded">7 HARI</strong>.
<br>
Mohon kembalikan tepat waktu untuk menghindari denda (Rp 1.000/hari).
</p>
@ -320,20 +320,18 @@ class="bi bi-check-circle me-1"></i>Tersedia</span>
<script>
document.addEventListener("DOMContentLoaded", function() {
// (Default Hari Ini + 2 Hari)
const fpKembali = flatpickr("#tanggalKembali", {
dateFormat: "d F Y",
dateFormat: "Y-m-d",
altInput: true,
altFormat: "d F Y",
defaultDate: new Date().fp_incr(2),
defaultDate: new Date().fp_incr(7),
locale: "id",
minDate: new Date().fp_incr(1),
maxDate: new Date().fp_incr(2)
});
// Inisialisasi Tanggal Pinjam
flatpickr("#tanggalPinjam", {
dateFormat: "d F Y",
dateFormat: "Y-m-d",
altInput: true,
altFormat: "d F Y",
defaultDate: "today",
@ -343,12 +341,11 @@ class="bi bi-check-circle me-1"></i>Tersedia</span>
onChange: function(selectedDates, dateStr) {
if (selectedDates.length > 0) {
const tglMulai = selectedDates[0];
const maxDateBaru = new Date(tglMulai).fp_incr(2);
const defaultReturnDate = new Date(tglMulai).fp_incr(7);
const minDateBaru = new Date(tglMulai).fp_incr(1);
fpKembali.set("minDate", minDateBaru);
fpKembali.set("maxDate", maxDateBaru);
fpKembali.setDate(maxDateBaru);
fpKembali.setDate(defaultReturnDate);
}
}
});

View File

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

View File

@ -89,6 +89,7 @@
Route::get('/buku/{id}', [AdminBookController::class, 'show'])->name('buku.show');
Route::post('/buku', [AdminBookController::class, 'store'])->name('buku.store');
Route::get('/buku/{id}/edit', [AdminBookController::class, 'edit'])->name('buku.edit');
Route::put('/buku/{id}', [AdminBookController::class, 'update'])->name('buku.update');
Route::post('/buku/arsip', [AdminBookController::class, 'arsip'])->name('buku.arsip');
Route::post('/buku/pulihkan', [AdminBookController::class, 'pulihkan'])->name('buku.pulihkan');