connect(); $payload = is_array($message) ? json_encode($message) : (string)$message; $mqtt->publish($topic, $payload, 0); $mqtt->disconnect(); } catch (\Exception $e) { \Log::error("MQTT Error: " . $e->getMessage()); } } /* ================= MANAJEMEN ANGGOTA ================= */ public function index(Request $request) { // Ambil input dengan nama variabel yang jelas agar tidak bentrok dengan scope Eloquent ($query) $searchKeyword = $request->input('search'); $statusFilter = $request->input('status'); // Bangun query dasar $query = Anggota::query(); // Kondisi jika ada pencarian (Bungkus dalam sub-query OR agar tidak merusak filter status) if (!empty($searchKeyword)) { $query->where(function ($q) use ($searchKeyword) { $q->where('nama', 'like', "%{$searchKeyword}%") ->orWhere('email', 'like', "%{$searchKeyword}%") ->orWhere('nip_nim', 'like', "%{$searchKeyword}%") // Sinkronisasi pencarian NIP/NIM ->orWhere('rfid_uid', 'like', "%{$searchKeyword}%"); // Sinkronisasi pencarian RFID UID (dari database: rfid_uid) }); } // Kondisi jika ada filter status if (!empty($statusFilter)) { $query->where('status', $statusFilter); } // Eksekusi pagination dengan aman $members = $query->orderByDesc('created_at')->paginate(10)->withQueryString(); // PROTEKSI EXTRA: Jika hasil kosong, pastikan tetap mengirim object kosong yang aman, bukan null if (!$members) { $members = new \Illuminate\Pagination\LengthAwarePaginator([], 0, 10); } return view('admin.anggota.index', compact('members')); } public function create() { return view('admin.anggota.create'); } public function store(Request $request) { // 🌟 SINKRONISASI: Menambahkan nip_nim dan gender ke formulir tambah manual oleh admin $validatedData = $request->validate([ 'nama' => 'required|string|max:100', 'gender' => 'required|in:Laki-laki,Perempuan', 'nip_nim' => 'required|string|max:255|unique:anggota,nip_nim', 'email' => 'required|email|unique:anggota,email', 'alamat' => 'required|string|max:255', 'no_hp' => 'nullable|string|max:20', ]); $validatedData['status'] = 'disetujui'; $member = Anggota::create($validatedData); // MQTT: Memicu reload halaman index admin karena ada entri data fisik baru $this->mqttPublish('pendaftaran/refresh/admin', [ 'action' => 'created', 'message' => 'Anggota baru telah ditambahkan oleh Admin' ]); return redirect()->route('admin.anggota.index') ->with('success', 'Anggota baru berhasil didaftarkan dan disetujui!'); } public function edit($id) { $member = Anggota::findOrFail($id); return view('admin.anggota.edit', compact('member')); } public function update(Request $request, $id) { $anggota = Anggota::findOrFail($id); // 1. Validasi Input Form $validatedData = $request->validate([ 'nama' => 'required|string|max:100', 'gender' => 'required|in:Laki-laki,Perempuan', 'nik_nip' => 'required|string|max:255|unique:anggota,nik_nip,' . $anggota->id, 'email' => 'required|email|unique:anggota,email,' . $anggota->id, 'no_hp' => 'nullable|string|max:20', 'alamat' => 'required|string|max:255', // Alamat lengkap gabungan dari JS Blade 'provinsi' => 'nullable|string', 'kota' => 'nullable|string', 'kecamatan' => 'nullable|string', 'desa' => 'nullable|string', ]); try { // 2. Simpan Perubahan Utama $anggota->nama = $validatedData['nama']; $anggota->gender = $validatedData['gender']; $anggota->nik_nip = $validatedData['nik_nip']; $anggota->email = $validatedData['email']; $anggota->no_hp = $validatedData['no_hp']; $anggota->alamat = $validatedData['alamat']; // 🌟 Ini sudah mencakup data wilayah lengkap // 🌟 FIXED: 4 baris pengisian wilayah dihapus karena kolomnya tidak ada di tabel database Anda // Data wilayah Anda sudah aman dan rapi di dalam kolom $anggota->alamat $anggota->save(); // 3. MQTT Terintegrasi try { if (method_exists($this, 'mqttPublish')) { $this->mqttPublish('pendaftaran/refresh/admin', [ 'action' => 'updated', 'member_id' => $anggota->id ]); if ($anggota->user_id) { $this->mqttPublish('pendaftaran/status/' . $anggota->user_id, [ 'status' => $anggota->status, 'message' => 'Data pendaftaran Anda telah diperbarui oleh Admin menjadi: ' . $anggota->status ]); } } } catch (\Exception $mqttError) { \Log::warning('MQTT Gagal: ' . $mqttError->getMessage()); } // 4. Kembali Ke Index Sembari Membawa Toast/Alert Sukses return redirect()->route('admin.anggota.index') ->with('success', 'Data anggota berhasil diperbarui! 🎉'); } catch (\Exception $e) { return redirect()->back() ->withInput() ->withErrors(['error_server' => 'Terjadi kesalahan pada server: ' . $e->getMessage()]); } } public function destroy($id) { $member = Anggota::findOrFail($id); $savedUserId = $member->user_id; $member->delete(); // MQTT: Sinyal refresh untuk admin karena ada baris data yang hilang $this->mqttPublish('pendaftaran/refresh/admin', [ 'action' => 'deleted' ]); // MQTT: Paksa pengguna logout atau refresh jika akunnya dihapus oleh admin if ($savedUserId) { $this->mqttPublish('pendaftaran/status/' . $savedUserId, [ 'status' => 'deleted', 'message' => 'Data pendaftaran Anda telah dihapus oleh pihak admin.' ]); } return redirect()->route('admin.anggota.index') ->with('success', 'Anggota berhasil dihapus!'); } public function cetak($id) { $member = Anggota::findOrFail($id); $qrCode = base64_encode(QrCode::format('svg')->size(120)->generate($member->nama)); return view('admin.anggota.cetak', compact('member', 'qrCode')); } /* ================= INTEGRASI IOT & STATE FLOW ================= */ public function approve($id) { $member = Anggota::findOrFail($id); // 1. Cek kesiapan IoT (Jangan timpa jika sedang ada transaksi berjalan) $current = DB::table('iot_modes')->where('id', 1)->first(); if ($current && $current->mode !== 'standby') { return back()->with('warning', 'Sistem sedang sibuk. Pastikan alat dalam posisi standby.'); } // 2. Update status anggota di database menjadi disetujui $member->update(['status' => 'disetujui']); // 3. Kunci State IoT ke mode 'admin' untuk User ID target DB::table('iot_modes')->where('id', 1)->update([ 'mode' => 'admin', 'user_id' => $member->id, 'updated_at' => now() ]); // 4. Perintah utama ke perangkat keras ESP32 $this->mqttPublish('rfid/mode/set', 'admin'); $this->mqttPublish('rfid/admin/request_scan', [ 'status' => 'waiting_scan', 'nama' => $member->nama, 'id' => $member->id ]); // 5. SINKRONISASI STATE ASINKRON: Kirim aksi 'approved' ke JavaScript panel admin. $this->mqttPublish('pendaftaran/refresh/admin', [ 'action' => 'approved', 'member_id' => $member->id ]); // 6. Notifikasi real-time ke aplikasi/halaman milik user if ($member->user_id) { $this->mqttPublish('pendaftaran/status/' . $member->user_id, [ 'status' => 'disetujui', 'message' => "Pendaftaran Anda disetujui! Alat RFID sekarang menunggu scan kartu Anda." ]); } // 7. Pengiriman Notifikasi via Email try { Mail::to($member->email)->send(new StatusAnggotaDiperbarui($member)); } catch (\Exception $e) { \Log::error("Email Error: " . $e->getMessage()); } return back()->with('success', "Anggota {$member->nama} disetujui. Perangkat RFID Nawasena siap menerima pemindaian kartu."); } public function reject($id) { $member = Anggota::findOrFail($id); $member->update(['status' => 'ditolak']); // MQTT: Beritahu halaman admin untuk memperbarui status penolakan $this->mqttPublish('pendaftaran/refresh/admin', [ 'action' => 'rejected', 'member_id' => $member->id ]); // 🌟 SINKRONISASI: Sinyal 'ditolak' dikirim ke user agar di view user langsung muncul tombol pendaftaran ulang (sinkron dengan UserMemberController) if ($member->user_id) { $this->mqttPublish('pendaftaran/status/' . $member->user_id, [ 'status' => 'ditolak', 'message' => "Pendaftaran Anda ditolak oleh admin. Silakan periksa kembali data Anda dan lakukan daftar ulang jika diperlukan." ]); } try { Mail::to($member->email)->send(new StatusAnggotaDiperbarui($member)); } catch (\Exception $e) { \Log::error("Email Error: " . $e->getMessage()); } return back()->with('warning', "Anggota {$member->nama} telah ditolak."); } public function receiveRfidUid(Request $request) { $uid = $request->input('uid'); $anggotaId = $request->input('anggota_id'); $member = Anggota::find($anggotaId); if ($member) { // 1. Simpan string UID fisik kartu ke data anggota $member->update(['rfid_uid' => $uid]); // 2. Kembalikan state mesin IoT ke mode 'standby' agar siap digunakan kembali DB::table('iot_modes')->where('id', 1)->update([ 'mode' => 'standby', 'user_id' => null, 'updated_at' => now() ]); $this->mqttPublish('pendaftaran/refresh/admin', [ 'action' => 'uid_saved', 'member_id' => $member->id, 'rfid_uid' => $uid ]); // 4. Perintahkan sistem user untuk mengalihkan view ke halaman dashboard utama library if ($member->user_id) { $this->mqttPublish('pendaftaran/status/' . $member->user_id, [ 'status' => 'uid_ready', 'message' => 'Kartu RFID Anda berhasil diregistrasikan!' ]); } return response()->json(['status' => 'success', 'message' => 'UID Berhasil disimpan dan disinkronkan ke Web-Admin via MQTT.']); } return response()->json(['status' => 'error', 'message' => 'Data Anggota tidak ditemukan.'], 404); } public function print($id) { $member = Anggota::findOrFail($id); // Load view kartu yang sudah rapi $pdf = Pdf::loadView('admin.anggota.print', compact('member')); // Atur ukuran kertas kustom atau default (misal A4 atau ukuran kartu) $pdf->setPaper('a4', 'portrait'); // Menggunakan STREAM agar PDF terbuka langsung di PDF Viewer browser (persis seperti contoh gambar) return $pdf->stream('Kartu Anggota ' . $member->nama . '.pdf'); } }