feat: Implement custom borrow/due dates for loans, update profile statistics, and refine book management forms.
This commit is contained in:
parent
b0c7a8b2cf
commit
3fb658fe9c
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue