383 lines
15 KiB
PHP
383 lines
15 KiB
PHP
@extends('layouts.anggota')
|
|
|
|
@section('content')
|
|
<style>
|
|
/* Menghilangkan scrollbar untuk IE, Edge, dan Firefox */
|
|
html, body {
|
|
-ms-overflow-style: none; /* IE and Edge */
|
|
scrollbar-width: none; /* Firefox */
|
|
}
|
|
/* Global Styles */
|
|
.header-section {
|
|
background: #ffffff;
|
|
border-left: 5px solid #4c51bf;
|
|
padding: 1.25rem 1.5rem;
|
|
border-radius: 0.75rem;
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.04);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.card-main {
|
|
border: none;
|
|
border-radius: 1rem !important;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
|
|
background: #fff;
|
|
}
|
|
|
|
/* Table Typography */
|
|
#anggotaTable thead th {
|
|
background-color: #f8fafc;
|
|
color: #64748b;
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
padding: 1.1rem 1rem;
|
|
border-bottom: 2px solid #e2e8f0;
|
|
}
|
|
|
|
#anggotaTable tbody td {
|
|
padding: 1rem;
|
|
vertical-align: middle;
|
|
font-size: 0.85rem;
|
|
color: #334155;
|
|
}
|
|
|
|
/* UID & NIM/NIP Badges */
|
|
.uid-text {
|
|
font-family: 'Monaco', monospace;
|
|
color: #64748b;
|
|
font-weight: 600;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.nik-badge {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
background: #f0f9ff;
|
|
color: #0369a1;
|
|
padding: 0.35rem 0.7rem;
|
|
border-radius: 8px;
|
|
font-weight: 700;
|
|
border: 1px solid #bae6fd;
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Address Styling */
|
|
.address-box {
|
|
max-width: 200px;
|
|
font-size: 0.8rem;
|
|
line-height: 1.4;
|
|
color: #64748b;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Status Pill */
|
|
.status-pill {
|
|
padding: 0.4rem 0.8rem;
|
|
border-radius: 50rem;
|
|
font-size: 0.7rem;
|
|
font-weight: 700;
|
|
border: 1px solid transparent;
|
|
}
|
|
.st-menunggu { background: #fffbeb; color: #92400e; border-color: #fde68a; }
|
|
.st-disetujui { background: #f0fdf4; color: #166534; border-color: #bbf7d0; }
|
|
.st-ditolak { background: #fef2f2; color: #991b1b; border-color: #fecaca; }
|
|
|
|
/* Action Buttons & Layout Fixes */
|
|
.btn-action-group {
|
|
display: flex;
|
|
gap: 0.35rem;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.btn-manage {
|
|
padding: 0.4rem 0.75rem;
|
|
border-radius: 8px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
transition: all 0.2s ease-in-out;
|
|
border: 1px solid #e2e8f0;
|
|
background: #fff;
|
|
color: #475569;
|
|
text-decoration: none !important;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.btn-manage:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.btn-edit:hover { border-color: #f59e0b; color: #f59e0b; background: #fffbeb; }
|
|
.btn-print:hover { border-color: #0ea5e9; color: #0ea5e9; background: #f0f9ff; }
|
|
.btn-approve:hover { border-color: #10b981; color: #10b981; background: #f0fdf4; }
|
|
.btn-reject:hover { border-color: #ef4444; color: #ef4444; background: #fef2f2; }
|
|
.btn-delete:hover { border-color: #dc2626; color: #dc2626; background: #fef2f2; }
|
|
|
|
/* Responsiveness */
|
|
@media (max-width: 1200px) {
|
|
.btn-manage span { display: none; }
|
|
.btn-manage { padding: 0.6rem; font-size: 1rem; }
|
|
}
|
|
</style>
|
|
|
|
<div class="container-fluid py-4">
|
|
<div class="header-section d-flex justify-content-between align-items-center flex-wrap gap-3">
|
|
<div>
|
|
<h3 class="fw-bold text-primary mb-0">Database Anggota</h3>
|
|
<p class="text-muted small mb-0">Manajemen Identitas, NIK/NIP, dan Verifikasi Anggota</p>
|
|
</div>
|
|
{{-- 🔥 FIX 1: Pengaman Total Record dengan Ternary Operator --}}
|
|
<div class="badge bg-light text-primary border px-3 py-2">
|
|
Total Record: {{ ($members && method_exists($members, 'total')) ? $members->total() : 0 }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card card-main mb-4">
|
|
<div class="card-body p-3">
|
|
<form action="{{ route('admin.anggota.index') }}" method="GET" class="row g-2">
|
|
<div class="col-md-5">
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text bg-white border-end-0"><i class="fas fa-search text-muted"></i></span>
|
|
<input type="text" name="search" class="form-control border-start-0 ps-0"
|
|
placeholder="Cari Nama, NIK/NIP, atau UID..." value="{{ request('search') }}">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select name="status" class="form-select form-select-sm">
|
|
<option value="">Semua Status</option>
|
|
<option value="menunggu" {{ request('status') == 'menunggu' ? 'selected' : '' }}>Menunggu</option>
|
|
<option value="disetujui" {{ request('status') == 'disetujui' ? 'selected' : '' }}>Disetujui</option>
|
|
<option value="ditolak" {{ request('status') == 'ditolak' ? 'selected' : '' }}>Ditolak</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4 d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm px-4">Cari</button>
|
|
@if(request('search') || request('status'))
|
|
<a href="{{ route('admin.anggota.index') }}" class="btn btn-light btn-sm px-3">Reset</a>
|
|
@endif
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card card-main">
|
|
<div class="table-responsive">
|
|
<table id="anggotaTable" class="table align-items-center mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-center">No</th>
|
|
<th>UID</th>
|
|
<th>NIK / NIP</th>
|
|
<th>Nama & Email</th>
|
|
<th>Alamat Lengkap</th>
|
|
<th class="text-center">Status</th>
|
|
<th class="text-center">Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($members as $member)
|
|
{{-- 🔥 FIX 2: Menambahkan ID baris agar fungsi injeksi real-time MQTT bekerja sempurna --}}
|
|
<tr id="member-{{ $member->id }}">
|
|
<td class="text-center fw-bold text-muted">
|
|
{{ ($members->currentPage()-1) * $members->perPage() + $loop->iteration }}
|
|
</td>
|
|
<td>
|
|
{{-- 🔥 FIX 3: Menambahkan class rfid-uid-cell untuk target tangkapan websocket MQTT --}}
|
|
<span class="rfid-uid-cell uid-text text-uppercase">{{ $member->uid ?? '-' }}</span>
|
|
</td>
|
|
<td>
|
|
<span class="nik-badge">{{ $member->nik_nip ?? 'N/A' }}</span>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex flex-column">
|
|
<span class="fw-bold text-dark">{{ $member->nama }}</span>
|
|
<span class="text-xs text-muted">{{ $member->email }}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="address-box" title="{{ $member->alamat }}">
|
|
{{ $member->alamat }}
|
|
</div>
|
|
</td>
|
|
<td class="text-center">
|
|
<span class="status-pill st-{{ $member->status }}">
|
|
{{ ucfirst($member->status) }}
|
|
</span>
|
|
</td>
|
|
<td class="text-center">
|
|
<div class="btn-action-group">
|
|
<a href="{{ route('admin.anggota.edit', $member->id) }}" class="btn-manage btn-edit" title="Edit Data">
|
|
<i class="fas fa-edit text-warning"></i> <span>Edit</span>
|
|
</a>
|
|
|
|
<a href="{{ route('admin.anggota.print', $member->id) }}"
|
|
target="_blank"
|
|
class="btn-manage btn-print"
|
|
title="Cetak Kartu">
|
|
<i class="fas fa-print text-info"></i>
|
|
<span>Cetak</span>
|
|
</a>
|
|
|
|
@if($member->status === 'menunggu')
|
|
<form action="{{ route('admin.anggota.approve', $member->id) }}" method="POST" class="m-0">
|
|
@csrf
|
|
<button type="submit" class="btn-manage btn-approve" onclick="return confirm('Terima anggota ini?')">
|
|
<i class="fas fa-check text-success"></i> <span>Terima</span>
|
|
</button>
|
|
</form>
|
|
|
|
<form action="{{ route('admin.anggota.reject', $member->id) }}" method="POST" class="m-0">
|
|
@csrf
|
|
<button type="submit" class="btn-manage btn-reject" onclick="return confirm('Tolak pendaftaran ini?')">
|
|
<i class="fas fa-times text-danger"></i> <span>Tolak</span>
|
|
</button>
|
|
</form>
|
|
@endif
|
|
|
|
<form action="{{ route('admin.anggota.destroy', $member->id) }}" method="POST" class="m-0">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit" class="btn-manage btn-delete" onclick="return confirm('Apakah Anda yakin ingin menghapus anggota {{ $member->nama }} secara permanen?')">
|
|
<i class="fas fa-trash text-danger"></i> <span>Hapus</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="7" class="text-center py-5 text-muted">
|
|
<i class="fas fa-user-slash fa-3x mb-3 opacity-20"></i><br>
|
|
Data anggota tidak tersedia dalam database.
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card-footer bg-white border-0 py-3">
|
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
{{-- 🔥 FIX 4: Proteksi Total Informasi Pagination --}}
|
|
@if(isset($members) && $members->count() > 0)
|
|
<small class="text-muted fw-bold">Showing {{ $members->firstItem() }} to {{ $members->lastItem() }} dari {{ $members->total() }} data</small>
|
|
<div>
|
|
{{ $members->links('pagination::bootstrap-5') }}
|
|
</div>
|
|
@else
|
|
<small class="text-muted fst-italic">Menampilkan 0 sampai 0 data</small>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
|
|
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
|
|
|
|
<script>
|
|
const mqttBroker = 'ws://127.0.0.1:9001';
|
|
const clientId = 'laravel_blade_admin_' + Math.random().toString(16).substr(2, 8);
|
|
|
|
const client = mqtt.connect(mqttBroker, {
|
|
clientId: clientId,
|
|
clean: true,
|
|
connectTimeout: 4000,
|
|
});
|
|
|
|
const topicAdmin = 'pendaftaran/refresh/admin';
|
|
|
|
client.on('connect', () => {
|
|
console.log('Terhubung ke MQTT Broker melalui WebSocket!');
|
|
client.subscribe(topicAdmin);
|
|
});
|
|
|
|
client.on('message', (topic, message) => {
|
|
try {
|
|
const data = JSON.parse(message.toString());
|
|
console.log("Mendapat sinyal real-time:", data);
|
|
|
|
if (data.action === 'created' || data.action === 'deleted') {
|
|
window.location.reload();
|
|
}
|
|
else if (data.action === 'uid_saved') {
|
|
const row = document.getElementById('member-' + data.member_id);
|
|
if (row) {
|
|
const uidCell = row.querySelector('.rfid-uid-cell');
|
|
if (uidCell) {
|
|
uidCell.innerText = data.rfid_uid;
|
|
uidCell.className = "rfid-uid-cell badge badge-success";
|
|
}
|
|
row.style.backgroundColor = '#fef3c7';
|
|
setTimeout(() => { row.style.backgroundColor = ''; }, 2000);
|
|
}
|
|
showToastNotification("Kartu RFID Anggota berhasil direkam secara real-time!");
|
|
}
|
|
else if (data.action === 'approved' || data.action === 'rejected' || data.action === 'updated') {
|
|
alertRealtimeNotification(data.action, data.member_id);
|
|
}
|
|
} catch (e) {
|
|
console.log("Pesan bukan JSON: ", message.toString());
|
|
}
|
|
});
|
|
|
|
client.on('error', (err) => {
|
|
console.error('Koneksi MQTT Gagal: ', err);
|
|
});
|
|
|
|
function alertRealtimeNotification(action, memberId) {
|
|
let message = "Ada perubahan data pada anggota!";
|
|
if (action === 'approved') message = "Seorang anggota baru saja disetujui! Alat RFID siap scan.";
|
|
|
|
showToastNotification(message);
|
|
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1500);
|
|
}
|
|
|
|
function showToastNotification(message) {
|
|
const alertBox = document.createElement('div');
|
|
alertBox.style.position = 'fixed';
|
|
alertBox.style.top = '20px';
|
|
alertBox.style.right = '20px';
|
|
alertBox.style.backgroundColor = '#4c51bf';
|
|
alertBox.style.color = '#fff';
|
|
alertBox.style.padding = '15px 25px';
|
|
alertBox.style.borderRadius = '10px';
|
|
alertBox.style.boxShadow = '0 4px 15px rgba(0,0,0,0.2)';
|
|
alertBox.style.zIndex = '9999';
|
|
alertBox.innerText = message;
|
|
|
|
document.body.appendChild(alertBox);
|
|
setTimeout(() => { alertBox.remove(); }, 3000);
|
|
}
|
|
</script>
|
|
<iframe id="printFrame" style="display:none;"></iframe>
|
|
|
|
<script>
|
|
function langsungCetak(url) {
|
|
const iframe = document.getElementById('printFrame');
|
|
|
|
// Set sumber URL iframe ke halaman cetak kartu
|
|
iframe.src = url;
|
|
|
|
// Begitu dokumen kartu selesai dimuat di latar belakang, langsung picu Print Preview
|
|
iframe.onload = function() {
|
|
if(iframe.src && iframe.src !== 'about:blank') {
|
|
iframe.contentWindow.focus();
|
|
iframe.contentWindow.print();
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
@endsection
|
|
|