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),
|
'id_peminjaman' => 'PIN-ADM-'.sprintf('%03d', $userId),
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'peminjam' => $user->nama_lengkap ?? 'Unknown',
|
'peminjam' => $user->nama_lengkap ?? 'Unknown',
|
||||||
|
'email' => $user->email,
|
||||||
'nomor_hp' => $user->phone ?? '-',
|
'nomor_hp' => $user->phone ?? '-',
|
||||||
'tanggal_pinjam' => $firstLoan->borrowed_at,
|
'tanggal_pinjam' => $firstLoan->borrowed_at,
|
||||||
'tenggat_kembali' => $firstLoan->due_at,
|
'tenggat_kembali' => $firstLoan->due_at,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ public function index()
|
||||||
|
|
||||||
$bukuPinjamOffline = $loans->map(function ($loan) {
|
$bukuPinjamOffline = $loans->map(function ($loan) {
|
||||||
$dueAt = Carbon::parse($loan->due_at);
|
$dueAt = Carbon::parse($loan->due_at);
|
||||||
$sisaHari = (int) now()->diffInDays($dueAt, false);
|
$sisaHari = (int) now()->startOfDay()->diffInDays($dueAt->startOfDay(), false);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => $loan->book->id,
|
'id' => $loan->book->id,
|
||||||
|
|
|
||||||
|
|
@ -98,12 +98,23 @@ public function store(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'buku_ids' => 'required|array|min:1|max:3',
|
'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');
|
$bukuIds = $request->input('buku_ids');
|
||||||
|
|
||||||
DB::transaction(function () use ($bukuIds) {
|
// 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, $borrowedAt, $dueAt) {
|
||||||
foreach ($bukuIds as $bukuId) {
|
foreach ($bukuIds as $bukuId) {
|
||||||
$book = Book::lockForUpdate()->find($bukuId);
|
$book = Book::lockForUpdate()->find($bukuId);
|
||||||
|
|
||||||
|
|
@ -115,8 +126,8 @@ public function store(Request $request)
|
||||||
'user_id' => Auth::id(),
|
'user_id' => Auth::id(),
|
||||||
'book_id' => $bukuId,
|
'book_id' => $bukuId,
|
||||||
'loan_code' => 'PIN-' . date('Ym') . '-' . strtoupper(Str::random(4)) . '-' . $bukuId,
|
'loan_code' => 'PIN-' . date('Ym') . '-' . strtoupper(Str::random(4)) . '-' . $bukuId,
|
||||||
'borrowed_at' => now(),
|
'borrowed_at' => $borrowedAt,
|
||||||
'due_at' => now()->addDays(7),
|
'due_at' => $dueAt,
|
||||||
'status' => 'Dipinjam',
|
'status' => 'Dipinjam',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,9 @@ public function index(Request $request): \Illuminate\View\View|\Illuminate\Http\
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$viewData['statistik'] = [
|
$viewData['statistik'] = [
|
||||||
['label' => 'Buku dipinjam', 'value' => $loans->count(), 'icon' => 'bi-book-half', 'color' => 'primary'],
|
['label' => 'Buku yang dipinjam', 'value' => $loans->count(), 'icon' => 'bi-journal-bookmark-fill', 'color' => 'primary'],
|
||||||
['label' => 'Tenggat Waktu', 'value' => $viewData['bukuOffline']->where('sisa_hari', '<=', 3)->where('sisa_hari', '>=', 0)->count(), 'icon' => 'bi-clock-history', 'color' => 'danger'],
|
['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', 'color' => 'success'],
|
['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'],
|
['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.
|
* Update data profil ke Database MySQL.
|
||||||
*/
|
*/
|
||||||
public function update(Request $request): RedirectResponse
|
public function update(ProfileUpdateRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$user->fill($request->validated());
|
$user->fill($request->validated());
|
||||||
$user->name = $user->nama_lengkap; // Sync for compatibility
|
$user->nama_lengkap = $user->name; // Sync for compatibility
|
||||||
|
|
||||||
if ($user->isDirty('email')) {
|
if ($user->isDirty('email')) {
|
||||||
$user->email_verified_at = null;
|
$user->email_verified_at = null;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class ProfileUpdateRequest extends FormRequest
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'nama_lengkap' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'email' => [
|
'email' => [
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<label for="stok" class="form-label">Stok</label>
|
<label for="stok" class="form-label">Stok</label>
|
||||||
<input type="text" name="stok" class="form-control" id="stok"
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-4 mb-3">
|
||||||
<label for="stok" class="form-label">Stok</label>
|
<label for="stok" class="form-label">Stok</label>
|
||||||
<input type="number" name="stok" class="form-control" id="stok"
|
<input type="text" name="stok" class="form-control" id="stok"
|
||||||
value="{{ old('stok', $buku->stok ?? 1) }}" min="0" required>
|
value="{{ old('stok', $buku->stok ?? 1) }}" required oninput="this.value = this.value.replace(/[^0-9]/g, '')">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,7 @@
|
||||||
<button type="button" class="btn btn-primary btn-konfirmasi-kembali"
|
<button type="button" class="btn btn-primary btn-konfirmasi-kembali"
|
||||||
data-nama-peminjam="{{ $transaksi['peminjam'] }}"
|
data-nama-peminjam="{{ $transaksi['peminjam'] }}"
|
||||||
data-nomor-hp="{{ $transaksi['nomor_hp'] }}"
|
data-nomor-hp="{{ $transaksi['nomor_hp'] }}"
|
||||||
|
data-email="{{ $transaksi['email'] }}"
|
||||||
data-user-id="{{ $transaksi['user_id'] }}">
|
data-user-id="{{ $transaksi['user_id'] }}">
|
||||||
Konfirmasi & Selesai
|
Konfirmasi & Selesai
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -346,12 +347,15 @@ function hitungTotalDenda(modal) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const userId = modalEl.find('.btn-konfirmasi-kembali').data('user-id');
|
const userId = modalEl.find('.btn-konfirmasi-kembali').data('user-id');
|
||||||
|
const userEmail = modalEl.find('.btn-konfirmasi-kembali').data('email');
|
||||||
const totalDenda = dendaOverdueTotal + totalDendaRusak;
|
const totalDenda = dendaOverdueTotal + totalDendaRusak;
|
||||||
|
|
||||||
// Construct WA Message
|
// Construct WA Message
|
||||||
let waLink = null;
|
let waLink = null;
|
||||||
if (isWaChecked && hp && hp !== '-') {
|
if (isWaChecked && hp && hp !== '-') {
|
||||||
let phone = 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);
|
if (phone.startsWith('0')) phone = '62' + phone.substring(1);
|
||||||
|
|
||||||
let message = `*BUKTI PENGEMBALIAN BUKU*\n\n`;
|
let message = `*BUKTI PENGEMBALIAN BUKU*\n\n`;
|
||||||
|
|
@ -371,10 +375,9 @@ function hitungTotalDenda(modal) {
|
||||||
|
|
||||||
// Loading Kirim Email flow
|
// Loading Kirim Email flow
|
||||||
if (isEmailChecked) {
|
if (isEmailChecked) {
|
||||||
const dummyEmail = nama.replace(/\s+/g, '.').toLowerCase() + '@sekolah.sch.id';
|
|
||||||
modernSwal.fire({
|
modernSwal.fire({
|
||||||
title: 'Mengirim Email...',
|
title: 'Mengirim Email...',
|
||||||
html: `Mengirim nota ke: <b>${dummyEmail}</b>`,
|
html: `Mengirim nota ke: <b>${userEmail}</b>`,
|
||||||
timer: 2000,
|
timer: 2000,
|
||||||
timerProgressBar: true,
|
timerProgressBar: true,
|
||||||
didOpen: () => Swal.showLoading()
|
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>
|
<h6 class="fw-bold text-dark mb-1">PENTING: Aturan Peminjaman!</h6>
|
||||||
<p class="mb-0 text-muted small">
|
<p class="mb-0 text-muted small">
|
||||||
Sesuai peraturan perpustakaan, durasi peminjaman buku maksimal adalah
|
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>
|
<br>
|
||||||
Mohon kembalikan tepat waktu untuk menghindari denda (Rp 1.000/hari).
|
Mohon kembalikan tepat waktu untuk menghindari denda (Rp 1.000/hari).
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -320,20 +320,18 @@ class="bi bi-check-circle me-1"></i>Tersedia</span>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
|
||||||
// (Default Hari Ini + 2 Hari)
|
|
||||||
const fpKembali = flatpickr("#tanggalKembali", {
|
const fpKembali = flatpickr("#tanggalKembali", {
|
||||||
dateFormat: "d F Y",
|
dateFormat: "Y-m-d",
|
||||||
altInput: true,
|
altInput: true,
|
||||||
altFormat: "d F Y",
|
altFormat: "d F Y",
|
||||||
defaultDate: new Date().fp_incr(2),
|
defaultDate: new Date().fp_incr(7),
|
||||||
locale: "id",
|
locale: "id",
|
||||||
minDate: new Date().fp_incr(1),
|
minDate: new Date().fp_incr(1),
|
||||||
maxDate: new Date().fp_incr(2)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Inisialisasi Tanggal Pinjam
|
// Inisialisasi Tanggal Pinjam
|
||||||
flatpickr("#tanggalPinjam", {
|
flatpickr("#tanggalPinjam", {
|
||||||
dateFormat: "d F Y",
|
dateFormat: "Y-m-d",
|
||||||
altInput: true,
|
altInput: true,
|
||||||
altFormat: "d F Y",
|
altFormat: "d F Y",
|
||||||
defaultDate: "today",
|
defaultDate: "today",
|
||||||
|
|
@ -343,12 +341,11 @@ class="bi bi-check-circle me-1"></i>Tersedia</span>
|
||||||
onChange: function(selectedDates, dateStr) {
|
onChange: function(selectedDates, dateStr) {
|
||||||
if (selectedDates.length > 0) {
|
if (selectedDates.length > 0) {
|
||||||
const tglMulai = selectedDates[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);
|
const minDateBaru = new Date(tglMulai).fp_incr(1);
|
||||||
|
|
||||||
fpKembali.set("minDate", minDateBaru);
|
fpKembali.set("minDate", minDateBaru);
|
||||||
fpKembali.set("maxDate", maxDateBaru);
|
fpKembali.setDate(defaultReturnDate);
|
||||||
fpKembali.setDate(maxDateBaru);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
<h5 class="fw-bold mb-3">Informasi Personal</h5>
|
||||||
<div class="row g-3">
|
<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>
|
<small class="text-muted d-block mb-1">NISN</small>
|
||||||
<p class="fw-semibold mb-0">{{ $user->nomor_induk ?? '-' }}</p>
|
<p class="fw-semibold mb-0">{{ $user->nomor_induk ?? '-' }}</p>
|
||||||
</div>
|
</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>
|
<small class="text-muted d-block mb-1">Email</small>
|
||||||
<p class="fw-semibold mb-0 text-break">{{ $user->email }}</p>
|
<p class="fw-semibold mb-0 text-break">{{ $user->email }}</p>
|
||||||
</div>
|
</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>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card border-0 flex-grow-1">
|
<div class="mb-3 mb-md-4 flex-grow-1">
|
||||||
<div class="card-body p-3 p-md-4 d-flex flex-column h-100">
|
<h5 class="fw-bold mb-3">Statistik Saya</h5>
|
||||||
<div class="d-flex flex-column flex-sm-row justify-content-between align-items-center mb-4 gap-3">
|
<div class="row g-3">
|
||||||
<h5 class="fw-bold mb-0">Ringkasan Laporan Minat Baca</h5>
|
@foreach ($statistik as $stat)
|
||||||
</div>
|
<div class="col-sm-6 col-lg-3">
|
||||||
<div class="row flex-grow-1">
|
<div class="card border-0 h-100">
|
||||||
<div class="col-md-6 mb-3 mb-md-0">
|
<div class="card-body p-3 p-md-4 text-center">
|
||||||
<h6 class="small text-muted mb-3 text-uppercase fw-semibold">Buku Terpopuler</h6>
|
<div class="icon-circle bg-{{ $stat['color'] }}-light mx-auto mb-3"
|
||||||
<ul class="list-group list-group-flush laporan-list">
|
style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; border-radius: 15px;">
|
||||||
@foreach ($laporan['buku_terpopuler'] as $buku)
|
<i class="bi {{ $stat['icon'] }} fs-2 text-{{ $stat['color'] }}"></i>
|
||||||
<li class="list-group-item px-0 py-3 d-flex justify-content-between align-items-center">
|
</div>
|
||||||
<span class="text-truncate me-2">{{ $buku['judul'] }}</span>
|
<h3 class="fw-bold mb-2">{{ $stat['value'] }}</h3>
|
||||||
<span class="badge bg-primary rounded-pill">{{ $buku['total_pembaca'] }}</span>
|
<p class="text-muted mb-0 small">{{ $stat['label'] }}</p>
|
||||||
</li>
|
</div>
|
||||||
@endforeach
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h6 class="small text-muted mb-3 text-uppercase fw-semibold">Kategori Terpopuler</h6>
|
|
||||||
<ul class="list-group list-group-flush laporan-list">
|
|
||||||
@foreach ($laporan['kategori_populer'] as $kategori)
|
|
||||||
<li class="list-group-item px-0 py-3 d-flex justify-content-between align-items-center">
|
|
||||||
<span class="text-truncate me-2">{{ $kategori['nama'] }}</span>
|
|
||||||
<span class="badge bg-success rounded-pill">{{ $kategori['total_pembaca'] }}</span>
|
|
||||||
</li>
|
|
||||||
@endforeach
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@
|
||||||
Route::get('/buku/{id}', [AdminBookController::class, 'show'])->name('buku.show');
|
Route::get('/buku/{id}', [AdminBookController::class, 'show'])->name('buku.show');
|
||||||
Route::post('/buku', [AdminBookController::class, 'store'])->name('buku.store');
|
Route::post('/buku', [AdminBookController::class, 'store'])->name('buku.store');
|
||||||
Route::get('/buku/{id}/edit', [AdminBookController::class, 'edit'])->name('buku.edit');
|
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/arsip', [AdminBookController::class, 'arsip'])->name('buku.arsip');
|
||||||
Route::post('/buku/pulihkan', [AdminBookController::class, 'pulihkan'])->name('buku.pulihkan');
|
Route::post('/buku/pulihkan', [AdminBookController::class, 'pulihkan'])->name('buku.pulihkan');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue