420 lines
19 KiB
PHP
420 lines
19 KiB
PHP
@extends('layouts.anggota')
|
|
|
|
@section('content')
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
|
|
|
<style>
|
|
/* Menghilangkan scrollbar untuk IE, Edge, dan Firefox */
|
|
html, body {
|
|
-ms-overflow-style: none; /* IE and Edge */
|
|
scrollbar-width: none; /* Firefox */
|
|
}
|
|
:root {
|
|
--primary: #4f46e5;
|
|
--danger: #ef4444;
|
|
--slate-200: #e2e8f0;
|
|
--slate-900: #0f172a;
|
|
--success: #22c55e;
|
|
--indigo: #6366f1;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
|
background-color: #f8fafc;
|
|
color: var(--slate-900);
|
|
}
|
|
|
|
.hero-title {
|
|
font-weight: 800;
|
|
letter-spacing: -0.02em;
|
|
color: var(--slate-900);
|
|
}
|
|
|
|
.table-card {
|
|
background: white;
|
|
border: 1px solid var(--slate-200);
|
|
border-radius: 20px;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.table thead th {
|
|
background-color: #f1f5f9;
|
|
padding: 1rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
color: #475569;
|
|
}
|
|
|
|
.user-name { font-weight: 700; display: block; }
|
|
.clickable-meta {
|
|
font-size: 0.75rem;
|
|
text-decoration: none;
|
|
transition: color 0.2s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.user-email { color: var(--primary); font-weight: 500; }
|
|
.user-email:hover { color: #3730a3; text-decoration: underline; }
|
|
|
|
.contact-link { color: #64748b; font-size: 0.8rem; }
|
|
.contact-link:hover { color: var(--slate-900); }
|
|
.wa-link:hover { color: #25d366 !important; }
|
|
|
|
.status-badge {
|
|
padding: 6px 12px;
|
|
border-radius: 8px;
|
|
font-size: 0.7rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.btn-nav {
|
|
padding: 6px 14px;
|
|
border-radius: 8px;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
border: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.btn-setujui { background: var(--slate-900); color: white; }
|
|
.btn-setujui:hover { background: var(--primary); transform: translateY(-1px); }
|
|
|
|
.btn-perpanjang { background: var(--indigo); color: white; }
|
|
.btn-perpanjang:hover { background: #4338ca; transform: translateY(-1px); }
|
|
|
|
.btn-tolak { background: white; color: var(--danger); border: 1px solid var(--danger); }
|
|
.btn-tolak:hover { background: var(--danger); color: white; transform: translateY(-1px); }
|
|
|
|
.btn-print { background: var(--success); color: white; }
|
|
.btn-print:hover { background: #16a34a; transform: translateY(-1px); }
|
|
</style>
|
|
|
|
<div class="container-fluid py-4">
|
|
<div class="mb-4 px-2 d-flex justify-content-between align-items-end">
|
|
<div>
|
|
<h2 class="hero-title">Daftar Transaksi Peminjaman</h2>
|
|
<p class="text-muted small mb-0">Kelola permohonan peminjaman dan perpanjangan buku secara efisien.</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if(session('success'))
|
|
<div class="alert alert-success border-0 shadow-sm rounded-4 mb-4 d-flex align-items-center gap-2">
|
|
<i class="bi bi-check-circle-fill"></i> {{ session('success') }}
|
|
</div>
|
|
@endif
|
|
|
|
<div class="table-card">
|
|
<div class="table-responsive">
|
|
<table class="table align-middle mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-center">No</th>
|
|
<th>Anggota & Akun</th>
|
|
<th>Kontak & Lokasi</th>
|
|
<th>Koleksi Buku</th>
|
|
<th class="text-center">Timeline</th>
|
|
<th class="text-center">Status</th>
|
|
<th class="text-center">Navigasi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($transactions as $t)
|
|
<tr>
|
|
<td class="text-center fw-bold text-muted">{{ $loop->iteration }}</td>
|
|
<td>
|
|
<span class="user-name">{{ $t->anggota->nama ?? '-' }}</span>
|
|
<a href="mailto:{{ $t->anggota->user->email ?? '' }}" class="clickable-meta user-email">
|
|
<i class="bi bi-envelope"></i> {{ $t->anggota->user->email ?? '-' }}
|
|
</a>
|
|
<div class="text-muted small">NIP/NIM: {{ $t->anggota->nip_nim ?? '-' }}</div>
|
|
</td>
|
|
<td>
|
|
@php
|
|
$phone = $t->anggota->no_hp ?? '';
|
|
$cleanPhone = preg_replace('/[^0-9]/', '', $phone);
|
|
if(str_starts_with($cleanPhone, '0')) {
|
|
$cleanPhone = '62' . substr($cleanPhone, 1);
|
|
}
|
|
@endphp
|
|
|
|
<div class="d-flex flex-column gap-1">
|
|
<a href="https://wa.me/{{ $cleanPhone }}" target="_blank" class="clickable-meta contact-link wa-link text-decoration-none">
|
|
<i class="bi bi-whatsapp text-success"></i> {{ $phone ?: '-' }}
|
|
</a>
|
|
<div class="address-info small text-muted">
|
|
<i class="bi bi-geo-alt text-danger"></i> {{ Str::limit($t->anggota->alamat ?? '-', 25) }}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-bold" style="max-width: 200px; white-space: normal;">{{ $t->koleksi->title ?? '-' }}</div>
|
|
@if($t->surat_pengantar)
|
|
<a href="{{ route('admin.peminjaman.download', $t->id) }}" class="text-decoration-none small text-primary fw-600" target="_blank">
|
|
<i class="bi bi-file-earmark-pdf"></i> Lihat Dokumen
|
|
</a>
|
|
@endif
|
|
</td>
|
|
<td class="text-center">
|
|
<div class="small">P: <strong>{{ $t->tanggal_pinjam ? \Carbon\Carbon::parse($t->tanggal_pinjam)->format('d/m/y') : '-' }}</strong></div>
|
|
<div class="small text-danger">K: <strong>{{ $t->tanggal_kembali ? \Carbon\Carbon::parse($t->tanggal_kembali)->format('d/m/y') : '-' }}</strong></div>
|
|
</td>
|
|
<td class="text-center">
|
|
@php
|
|
$badges = [
|
|
'menunggu' => 'bg-warning text-dark',
|
|
'perpanjangan_menunggu' => 'bg-info text-white',
|
|
'disetujui' => 'bg-info text-white',
|
|
'dipinjam' => 'bg-primary text-white',
|
|
'dikembalikan' => 'bg-success text-white',
|
|
'ditolak' => 'bg-danger text-white'
|
|
];
|
|
$statusLabel = ($t->status == 'perpanjangan_menunggu') ? 'Butuh Perpanjang' : ucfirst($t->status);
|
|
@endphp
|
|
<span class="status-badge {{ $badges[$t->status] ?? 'bg-secondary' }}">
|
|
{{ $statusLabel }}
|
|
</span>
|
|
</td>
|
|
<td class="text-center">
|
|
<div class="d-flex justify-content-center gap-2">
|
|
{{-- 1. PERSSETUJUAN PINJAM BARU --}}
|
|
@if($t->status == 'menunggu')
|
|
<button class="btn-nav btn-setujui shadow-sm"
|
|
data-bs-toggle="modal" data-bs-target="#approveModal"
|
|
data-id="{{ $t->id }}"
|
|
data-title="Setujui Peminjaman"
|
|
data-action="/admin/peminjaman/{{ $t->id }}/setujui"
|
|
data-pickup="{{ date('Y-m-d') }}"
|
|
data-pickuptime="{{ date('H:i') }}"
|
|
data-return="{{ \Carbon\Carbon::now()->addDays(7)->format('Y-m-d') }}">
|
|
Setujui
|
|
</button>
|
|
<form action="{{ route('admin.peminjaman.tolak', $t->id) }}" method="POST">
|
|
@csrf
|
|
<button type="submit" class="btn-nav btn-tolak" onclick="return confirm('Tolak permohonan pinjam ini?')">Tolak</button>
|
|
</form>
|
|
|
|
{{-- 2. PERSETUJUAN PERPANJANGAN --}}
|
|
@elseif($t->status == 'perpanjangan_menunggu')
|
|
<button class="btn-nav btn-perpanjang shadow-sm"
|
|
data-bs-toggle="modal" data-bs-target="#approveModal"
|
|
data-id="{{ $t->id }}"
|
|
data-title="Konfirmasi Perpanjangan"
|
|
data-action="/admin/peminjaman/{{ $t->id }}/setujui-perpanjangan"
|
|
data-pickup="{{ \Carbon\Carbon::parse($t->pickup_date)->format('Y-m-d') }}"
|
|
data-pickuptime="{{ \Carbon\Carbon::parse($t->pickup_date)->format('H:i') }}"
|
|
data-return="{{ \Carbon\Carbon::parse($t->tanggal_kembali)->addDays(7)->format('Y-m-d') }}">
|
|
<i class="bi bi-arrow-repeat"></i> Terima
|
|
</button>
|
|
<form action="{{ route('admin.peminjaman.tolak-perpanjangan', $t->id) }}" method="POST">
|
|
@csrf
|
|
<button type="submit" class="btn-nav btn-tolak" onclick="return confirm('Tolak permohonan perpanjangan ini?')">Tolak</button>
|
|
</form>
|
|
|
|
{{-- 3. STATUS LAIN --}}
|
|
@elseif(in_array($t->status, ['disetujui', 'dipinjam']))
|
|
@if($t->status == 'disetujui')
|
|
<form action="{{ route('admin.peminjaman.ambil', $t->id) }}" method="POST">
|
|
@csrf
|
|
<button type="submit" class="btn-nav btn-setujui bg-dark">Ambil</button>
|
|
</form>
|
|
@endif
|
|
|
|
<button onclick="printNota('{{ $t->id }}')" class="btn-nav btn-print shadow-sm">
|
|
<i class="bi bi-printer"></i> Nota
|
|
</button>
|
|
@else
|
|
<span class="text-muted small">-</span>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="7" class="text-center py-5 text-muted">Belum ada data transaksi.</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- MODAL DINAMIS --}}
|
|
<div class="modal fade" id="approveModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<form id="approveForm" method="POST">
|
|
@csrf
|
|
<div class="modal-content border-0 shadow-lg rounded-4">
|
|
<div class="modal-header border-0">
|
|
<h5 class="fw-800 mb-0" id="modalTitle">Lengkapi Data Pinjam</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-4">
|
|
<div class="row g-3">
|
|
<div class="col-6">
|
|
<label class="form-label small fw-bold">Tanggal Ambil</label>
|
|
<input type="date" class="form-control" name="pickup_date" id="pickup_date" required>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small fw-bold">Waktu</label>
|
|
<input type="time" class="form-control" name="pickup_time" id="pickup_time" required>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label small fw-bold">Batas Pengembalian Baru</label>
|
|
<input type="date" class="form-control border-primary" name="tanggal_kembali" id="tanggal_kembali" required>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-link text-muted fw-600 text-decoration-none" data-bs-dismiss="modal">Batal</button>
|
|
<button type="submit" class="btn btn-setujui px-4 shadow">Konfirmasi</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<iframe id="printFrame" style="display:none;"></iframe>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var approveModal = document.getElementById('approveModal');
|
|
approveModal.addEventListener('show.bs.modal', function (event) {
|
|
var button = event.relatedTarget;
|
|
|
|
// Data pendukung dari atribut tombol
|
|
var actionUrl = button.getAttribute('data-action');
|
|
var modalTitle = button.getAttribute('data-title');
|
|
|
|
// Update Form dan Judul secara dinamis
|
|
document.getElementById('approveForm').action = actionUrl;
|
|
document.getElementById('modalTitle').innerText = modalTitle;
|
|
|
|
// Reset dan isi input
|
|
document.getElementById('pickup_date').value = button.getAttribute('data-pickup');
|
|
document.getElementById('pickup_time').value = button.getAttribute('data-pickuptime');
|
|
document.getElementById('tanggal_kembali').value = button.getAttribute('data-return');
|
|
});
|
|
});
|
|
|
|
function printNota(id) {
|
|
const printFrame = document.getElementById('printFrame');
|
|
printFrame.src = '/admin/peminjaman/' + id + '/cetak-nota';
|
|
|
|
printFrame.onload = function() {
|
|
printFrame.contentWindow.focus();
|
|
printFrame.contentWindow.print();
|
|
};
|
|
}
|
|
</script>
|
|
<!-- MQTT Library CDN via WebSockets -->
|
|
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// === 1. LOGIKA HANDLING MODAL BOOTSTRAP (EKSISTING) ===
|
|
var approveModal = document.getElementById('approveModal');
|
|
if (approveModal) {
|
|
approveModal.addEventListener('show.bs.modal', function (event) {
|
|
var button = event.relatedTarget;
|
|
var actionUrl = button.getAttribute('data-action');
|
|
var modalTitle = button.getAttribute('data-title');
|
|
|
|
document.getElementById('approveForm').action = actionUrl;
|
|
document.getElementById('modalTitle').innerText = modalTitle;
|
|
|
|
document.getElementById('pickup_date').value = button.getAttribute('data-pickup');
|
|
document.getElementById('pickup_time').value = button.getAttribute('data-pickuptime');
|
|
document.getElementById('tanggal_kembali').value = button.getAttribute('data-return');
|
|
});
|
|
}
|
|
|
|
// === 2. SINKRONISASI MQTT DASHBOARD ADMIN (REAL-TIME UPDATE) ===
|
|
console.log("=== 🔍 NAVASENA ADMIN MQTT MONITOR ===");
|
|
|
|
// Mengambil IP/Domain host tempat Laragon/Mosquitto berjalan secara dinamis
|
|
const brokerHost = window.location.hostname;
|
|
const brokerUrl = `ws://${brokerHost}:9001`;
|
|
|
|
// Menggunakan Wildcard (+) agar Admin bisa memonitor transaksi dari SELURUH User/Anggota
|
|
const topicTarget = 'peminjaman/user/+';
|
|
|
|
console.log(`🔌 Admin mencoba koneksi ke Broker: ${brokerUrl}`);
|
|
console.log(`📌 Admin memonitor seluruh user via topik: ${topicTarget}`);
|
|
|
|
const client = mqtt.connect(brokerUrl, {
|
|
clientId: 'nawasena_admin_monitor_' + Math.random().toString(16).substr(2, 8),
|
|
clean: true,
|
|
connectTimeout: 5000
|
|
});
|
|
|
|
// Event saat berhasil terhubung ke broker via WebSockets
|
|
client.on('connect', function () {
|
|
console.log("✅ ADMIN BERHASIL TERHUBUNG ke Mosquitto via WebSockets!");
|
|
|
|
client.subscribe(topicTarget, function (err) {
|
|
if (err) {
|
|
console.error("❌ Admin Gagal Subscribe ke topik wildcard:", err);
|
|
} else {
|
|
console.log(`📡 Admin Berhasil Subscribe! Memantau sinyal peminjaman/pengembalian seluruh anggota...`);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Event saat menerima pesan MQTT dari user mana pun
|
|
client.on('message', function (topic, message) {
|
|
console.log(`📥 Sinyal Transaksi Masuk di topik [${topic}]:`, message.toString());
|
|
|
|
// Validasi struktur topik (Memastikan berasal dari rumpun peminjaman/user/)
|
|
if (topic.startsWith('peminjaman/user/')) {
|
|
// Ekstrak ID Anggota dari topik jika sewaktu-waktu Anda butuh debug (contoh: peminjaman/user/5 -> id = 5)
|
|
const scannedAnggotaId = topic.split('/')[2];
|
|
|
|
try {
|
|
const payload = JSON.parse(message.toString());
|
|
console.log(`📦 Data JSON Terurai untuk Anggota ID [${scannedAnggotaId}]:`, payload);
|
|
|
|
// Notifikasi Alert untuk Admin sebelum refresh (bisa dimatikan jika mengganggu produksi)
|
|
alert(`🔔 Real-time Info: Transaksi Anggota ID ${scannedAnggotaId} (${payload.status || 'Update'}) terdeteksi! Memperbarui data tabel admin...`);
|
|
|
|
// Eksekusi Reload Tabel Admin secara Instan
|
|
window.location.reload();
|
|
|
|
} catch (e) {
|
|
console.error("❌ Payload dari Hardware bukan JSON yang valid. Mencoba refresh paksa...");
|
|
alert(`🔔 Sinyal transaksi masuk (Non-JSON) dari Anggota ID ${scannedAnggotaId}. Memuat ulang halaman admin...`);
|
|
window.location.reload();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Event jika koneksi gagal/error
|
|
client.on('error', function (err) {
|
|
console.error("🔴 Admin MQTT Connection Error:", err);
|
|
});
|
|
|
|
client.on('close', function() {
|
|
console.warn("⚠️ Koneksi MQTT Admin ke Broker terputus.");
|
|
});
|
|
});
|
|
|
|
function printNota(id) {
|
|
const printFrame = document.getElementById('printFrame');
|
|
printFrame.src = '/admin/peminjaman/' + id + '/cetak-nota';
|
|
|
|
printFrame.onload = function() {
|
|
printFrame.contentWindow.focus();
|
|
printFrame.contentWindow.print();
|
|
};
|
|
}
|
|
</script>
|
|
@endsection
|
|
|