TKK_E32230908/resources/views/admin/anggota/index.blade.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