conn = $database->connect(); $this->auth = new Auth(); date_default_timezone_set('Asia/Jakarta'); } public function handleRequest() { if (!$this->auth->checkSession()) { http_response_code(401); echo json_encode([ 'status' => 'error', 'message' => 'Unauthorized access', 'redirect' => '../admin/login.html' ]); return; } $action = $_GET['action'] ?? $_POST['action'] ?? ''; $method = $_SERVER['REQUEST_METHOD']; switch($action) { case 'add_visitor': if ($method === 'POST') $this->addVisitor(); break; case 'clear_temp': if ($method === 'POST' || $method === 'GET' || $method === 'DELETE') $this->clearRfidTemp(true); break; case 'end_session': if ($method === 'POST' && isset($_POST['kode_gelang'])) { $this->endPlaySession($_POST['kode_gelang']); } else { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Kode gelang tidak ditemukan untuk mengakhiri sesi.']); } break; case 'check_rfid': if ($method === 'GET') $this->checkRfidStatus(); break; case 'get_scanned_rfid': if ($method === 'GET') $this->getScannedRfid(); break; case 'simulate_scan': if ($method === 'POST' && isset($_POST['kode_gelang'])) { $this->simulateScan($_POST['kode_gelang']); } else { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Kode gelang tidak ditemukan untuk simulasi scan.']); } break; default: http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Action tidak ditentukan atau tidak valid.']); } } private function getScannedRfid() { try { $query = "SELECT kode_gelang FROM rfid_temp ORDER BY id DESC LIMIT 1"; $stmt = $this->conn->prepare($query); $stmt->execute(); if ($stmt->rowCount() > 0) { $row = $stmt->fetch(PDO::FETCH_ASSOC); echo json_encode([ 'status' => 'success', 'data' => [ 'kode_gelang' => $row['kode_gelang'], 'scanned' => true ] ]); } else { echo json_encode([ 'status' => 'success', 'data' => [ 'kode_gelang' => null, 'scanned' => false ] ]); } } catch(PDOException $e) { error_log("Database error di getScannedRfid: " . $e->getMessage()); http_response_code(500); echo json_encode([ 'status' => 'error', 'message' => 'Database error di getScannedRfid: ' . $e->getMessage() ]); } } private function simulateScan($kodeGelang) { try { $query = "DELETE FROM rfid_temp"; $stmt = $this->conn->prepare($query); $stmt->execute(); $query = "INSERT INTO rfid_temp (kode_gelang) VALUES (:kode_gelang)"; $stmt = $this->conn->prepare($query); $stmt->bindParam(':kode_gelang', $kodeGelang); $stmt->execute(); echo json_encode([ 'status' => 'success', 'message' => 'RFID scan disimulasikan', 'kode_gelang' => $kodeGelang ]); } catch(PDOException $e) { error_log("Error simulasi scan: " . $e->getMessage()); http_response_code(500); echo json_encode([ 'status' => 'error', 'message' => 'Error simulasi scan: ' . $e->getMessage() ]); } } private function checkRfidStatus() { try { $query = "SELECT kode_gelang FROM rfid_temp ORDER BY id DESC LIMIT 1"; $stmt = $this->conn->prepare($query); $stmt->execute(); if ($stmt->rowCount() === 0) { echo json_encode([ 'status' => 'error', 'message' => 'Belum ada gelang yang di-scan' ]); return; } $tempData = $stmt->fetch(PDO::FETCH_ASSOC); $kodeGelang = $tempData['kode_gelang']; // Cek status gelang di rfid_tags $query = "SELECT id, status FROM rfid_tags WHERE kode_gelang = :kode_gelang"; $stmt = $this->conn->prepare($query); $stmt->bindParam(':kode_gelang', $kodeGelang); $stmt->execute(); if ($stmt->rowCount() === 0) { echo json_encode([ 'status' => 'error', 'message' => 'Gelang tidak dikenali' ]); return; } $rfidData = $stmt->fetch(PDO::FETCH_ASSOC); if ($rfidData['status'] === 'digunakan') { // Gelang sedang digunakan. Cari sesi aktif terakhir yang menggunakan gelang ini. $userData = $this->getCurrentActiveSessionData($kodeGelang); if ($userData['status_waktu'] === 'tidak_aktif' || $userData['id_kunjungan'] === null) { // Ini berarti rfid_tags bilang 'digunakan' tapi tidak ada sesi aktif di kunjungan. // Ini adalah inkonsistensi data. Kita bisa coba reset status gelang ini. // Namun, untuk alur normal, ini adalah error. http_response_code(400); // Atau 500, tergantung seberapa parah ini dianggap echo json_encode([ 'status' => 'error', 'message' => 'Inkonsistensi data: Gelang ' . $kodeGelang . ' berstatus "digunakan" tetapi tidak ada sesi aktif yang ditemukan.', 'data' => $userData // Masih bisa tampilkan nama anak terakhir yang pakai ]); // Opsional: Langsung reset status gelang di sini jika ini dianggap error yang bisa diatasi otomatis // $this->resetRfidTagStatus($kodeGelang, $rfidData['id']); } else { echo json_encode([ 'status' => 'in_use', 'message' => 'Gelang sedang digunakan', 'data' => $userData ]); } } else { // Gelang tersedia, siap untuk pendaftaran baru echo json_encode([ 'status' => 'available', 'message' => 'Gelang siap digunakan', 'data' => [ 'kode_gelang' => $kodeGelang ] ]); } } catch(PDOException $e) { error_log("Database error di checkRfidStatus: " . $e->getMessage()); http_response_code(500); echo json_encode([ 'status' => 'error', 'message' => 'Database error di checkRfidStatus: ' . $e->getMessage() ]); } catch(Exception $e) { error_log("Error umum di checkRfidStatus: " . $e->getMessage()); http_response_code(500); echo json_encode(['status' => 'error', 'message' => 'Error sistem di checkRfidStatus: ' . $e->getMessage()]); } } /** * Mencari data sesi aktif terakhir untuk sebuah kode gelang. * Mengambil id_anak dari kunjungan terbaru dan mendapatkan detail anak dari tabel anak. * @param string $kodeGelang * @return array */ private function getCurrentActiveSessionData($kodeGelang) { try { // Kita perlu mencari kunjungan yang paling baru dan aktif, yang kode gelangnya sesuai. // Langkah ini membutuhkan JOIN antara kunjungan dan anak untuk mendapatkan kode_gelang // Serta JOIN dengan rfid_tags untuk memastikan status gelang (meskipun rfid_tags sudah dicek di checkRfidStatus) $query = " SELECT k.id AS id_kunjungan, k.waktu_masuk, a.nama AS nama_anak, a.durasi AS durasi_anak_db FROM kunjungan k JOIN anak a ON k.id_anak = a.id JOIN rfid_tags rt ON a.kode_gelang = rt.kode_gelang -- Join untuk memastikan kode gelang ini WHERE rt.kode_gelang = :kode_gelang AND k.waktu_keluar IS NULL ORDER BY k.waktu_masuk DESC, k.id DESC -- Ambil yang paling baru masuk, lalu id terbaru LIMIT 1 "; $stmt = $this->conn->prepare($query); $stmt->bindParam(':kode_gelang', $kodeGelang); $stmt->execute(); $kunjunganData = $stmt->fetch(PDO::FETCH_ASSOC); if (!$kunjunganData) { // Tidak ada sesi aktif ditemukan untuk kode gelang ini return [ 'nama' => 'Tidak Ada', 'kode_gelang' => $kodeGelang, 'sisa_waktu' => 'Tidak ada sesi aktif', 'status_waktu' => 'tidak_aktif', 'color_class' => 'text-gray-500', 'id_kunjungan' => null ]; } $waktuMasuk = new DateTime($kunjunganData['waktu_masuk']); // Konversi durasi dari format HH:MM:SS ke total detik list($h, $m, $s) = explode(':', $kunjunganData['durasi_anak_db']); $durasiTotalDetik = ($h * 3600) + ($m * 60) + $s; $waktuSelesaiSeharusnya = clone $waktuMasuk; $waktuSelesaiSeharusnya->modify("+$durasiTotalDetik seconds"); $now = new DateTime(); $sisaWaktuDetik = $waktuSelesaiSeharusnya->getTimestamp() - $now->getTimestamp(); $sisaWaktuFormatted = ""; $colorClass = ""; if ($sisaWaktuDetik <= 0) { $sisaWaktuFormatted = "Telat " . floor(abs($sisaWaktuDetik) / 60) . " menit"; $colorClass = "text-red-500"; } else if ($sisaWaktuDetik <= (10 * 60)) { $sisaWaktuFormatted = floor($sisaWaktuDetik / 60) . " menit " . ($sisaWaktuDetik % 60) . " detik"; $colorClass = "text-orange-500"; } else { $hours = floor($sisaWaktuDetik / 3600); $minutes = floor(($sisaWaktuDetik % 3600) / 60); $sisaWaktuFormatted = sprintf("%02d:%02d", $hours, $minutes); $colorClass = "text-green-500"; } return [ 'nama' => $kunjunganData['nama_anak'], 'kode_gelang' => $kodeGelang, 'sisa_waktu' => $sisaWaktuFormatted, 'status_waktu' => ($sisaWaktuDetik <= 0) ? 'habis' : 'tersisa', 'color_class' => $colorClass, 'id_kunjungan' => $kunjunganData['id_kunjungan'] // ID kunjungan yang sedang aktif ]; } catch(Exception $e) { error_log("Error in getCurrentActiveSessionData: " . $e->getMessage()); return [ 'nama' => 'Error', 'kode_gelang' => $kodeGelang, 'sisa_waktu' => 'N/A', 'status_waktu' => 'error', 'color_class' => 'text-red-500', 'id_kunjungan' => null ]; } } private function addVisitor() { $namaAnak = $_POST['namaAnak'] ?? ''; $noHpOrtu = $_POST['noHpOrtu'] ?? ''; $durasiMenit = $_POST['durasi'] ?? ''; $kodeGelang = $_POST['kodeGelang'] ?? ''; if (empty($namaAnak) || empty($noHpOrtu) || empty($durasiMenit) || empty($kodeGelang)) { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Semua field wajib diisi.']); return; } if (strlen($namaAnak) < 2) { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Nama anak minimal 2 karakter.', 'field' => 'namaAnak']); return; } $noHpOrtuClean = preg_replace('/\D/', '', $noHpOrtu); if (!preg_match('/^\d{10,15}$/', $noHpOrtuClean)) { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Nomor HP tidak valid (10-15 digit angka).', 'field' => 'noHpOrtu']); return; } $validDurations = [30, 60, 90, 120, 180, 240]; if (!in_array((int)$durasiMenit, $validDurations)) { http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Durasi bermain tidak valid.', 'field' => 'durasi']); return; } try { $this->conn->beginTransaction(); // 1. Cek status RFID di tabel rfid_tags $queryRfid = "SELECT id, status FROM rfid_tags WHERE kode_gelang = :kode_gelang"; $stmtRfid = $this->conn->prepare($queryRfid); $stmtRfid->bindParam(':kode_gelang', $kodeGelang); $stmtRfid->execute(); $rfidData = $stmtRfid->fetch(PDO::FETCH_ASSOC); if (!$rfidData) { $this->conn->rollBack(); http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Kode gelang tidak ditemukan di sistem.']); return; } if ($rfidData['status'] === 'digunakan') { $this->conn->rollBack(); http_response_code(400); echo json_encode(['status' => 'error', 'message' => 'Gelang sudah digunakan oleh pengunjung lain.']); return; } $idGelangRfid = $rfidData['id']; // ID internal gelang dari rfid_tags, tetap simpan untuk update status rfid_tags // 2. Insert atau update ke tabel 'anak' // Cari anak berdasarkan nama dan no_hp (jika ada, update durasi dan kode_gelang) $queryCheckAnak = "SELECT id FROM anak WHERE nama = :nama_anak AND no_hp = :no_hp_ortu LIMIT 1"; $stmtCheckAnak = $this->conn->prepare($queryCheckAnak); $stmtCheckAnak->bindParam(':nama_anak', $namaAnak); $stmtCheckAnak->bindParam(':no_hp_ortu', $noHpOrtuClean); $stmtCheckAnak->execute(); $idAnak = $stmtCheckAnak->fetchColumn(); $durasiFormattedForAnakTable = $this->formatDuration($durasiMenit); if (!$idAnak) { // Jika anak belum ada, insert baru // Kode gelang disimpan di tabel anak (sesuai kebutuhan Anda) $queryInsertAnak = "INSERT INTO anak (nama, kode_gelang, no_hp, durasi) VALUES (:nama, :kode_gelang, :no_hp, :durasi)"; $stmtInsertAnak = $this->conn->prepare($queryInsertAnak); $stmtInsertAnak->bindParam(':nama', $namaAnak); $stmtInsertAnak->bindParam(':kode_gelang', $kodeGelang); $stmtInsertAnak->bindParam(':no_hp', $noHpOrtuClean); $stmtInsertAnak->bindParam(':durasi', $durasiFormattedForAnakTable); $stmtInsertAnak->execute(); $idAnak = $this->conn->lastInsertId(); } else { // Jika anak sudah ada, update info gelang dan durasi di tabel anak $queryUpdateAnak = "UPDATE anak SET kode_gelang = :kode_gelang, durasi = :durasi WHERE id = :id_anak"; $stmtUpdateAnak = $this->conn->prepare($queryUpdateAnak); $stmtUpdateAnak->bindParam(':kode_gelang', $kodeGelang); $stmtUpdateAnak->bindParam(':durasi', $durasiFormattedForAnakTable); $stmtUpdateAnak->bindParam(':id_anak', $idAnak); $stmtUpdateAnak->execute(); } // 3. Insert ke tabel 'kunjungan' // Hanya ada id_anak, waktu_masuk, waktu_keluar $queryKunjungan = "INSERT INTO kunjungan (id_anak, waktu_masuk, waktu_keluar) VALUES (:id_anak, NOW(), NULL)"; $stmtKunjungan = $this->conn->prepare($queryKunjungan); $stmtKunjungan->bindParam(':id_anak', $idAnak); $stmtKunjungan->execute(); // 4. Update status di tabel 'rfid_tags' $queryUpdateRfid = "UPDATE rfid_tags SET status = 'digunakan' WHERE id = :id_gelang"; $stmtUpdateRfid = $this->conn->prepare($queryUpdateRfid); $stmtUpdateRfid->bindParam(':id_gelang', $idGelangRfid); $stmtUpdateRfid->execute(); // 5. Clear rfid_temp after successful registration $this->clearRfidTemp(); $this->conn->commit(); echo json_encode(['status' => 'success', 'message' => 'Pengunjung berhasil didaftarkan!']); } catch(PDOException $e) { $this->conn->rollBack(); error_log("PDOException di addVisitor: " . $e->getMessage() . " di baris " . $e->getLine()); http_response_code(500); echo json_encode(['status' => 'error', 'message' => 'Error mendaftarkan pengunjung (DB): ' . $e->getMessage()]); } catch(Exception $e) { $this->conn->rollBack(); error_log("Exception umum di addVisitor: " . $e->getMessage() . " di baris " . $e->getLine()); http_response_code(500); echo json_encode(['status' => 'error', 'message' => 'Error sistem (umum): ' . $e->getMessage()]); } } /** * Mengakhiri sesi permainan untuk kode gelang yang diberikan. * Mencari sesi kunjungan aktif paling baru yang terkait dengan kode gelang tersebut. * @param string $kodeGelang */ private function endPlaySession($kodeGelang) { try { $this->conn->beginTransaction(); // 1. Dapatkan ID kunjungan aktif paling baru yang terkait dengan kode_gelang ini // JOIN dengan tabel anak untuk mendapatkan id_anak yang terhubung dengan kode_gelang, // lalu cari kunjungan aktif terbaru untuk id_anak tersebut. $queryKunjungan = " SELECT k.id AS id_kunjungan_to_end, a.id AS id_anak_affected FROM kunjungan k JOIN anak a ON k.id_anak = a.id WHERE a.kode_gelang = :kode_gelang AND k.waktu_keluar IS NULL ORDER BY k.waktu_masuk DESC, k.id DESC LIMIT 1 "; $stmtKunjungan = $this->conn->prepare($queryKunjungan); $stmtKunjungan->bindParam(':kode_gelang', $kodeGelang); $stmtKunjungan->execute(); $activeKunjungan = $stmtKunjungan->fetch(PDO::FETCH_ASSOC); if (!$activeKunjungan) { // Ini bisa terjadi jika gelang sudah di-set 'digunakan' di rfid_tags, // tapi tidak ada kunjungan aktif yang cocok di tabel kunjungan. // Ini adalah inkonsistensi data. Kita tetap bebaskan gelangnya. error_log("Peringatan: Gelang " . $kodeGelang . " berstatus 'digunakan' di rfid_tags, tetapi tidak ada sesi aktif di kunjungan. Mencoba reset status gelang saja."); $this->resetRfidTagStatus($kodeGelang); // Panggil fungsi reset $this->clearRfidTemp(); $this->conn->commit(); echo json_encode(['status' => 'warning', 'message' => 'Tidak ada sesi aktif ditemukan, tetapi status gelang telah direset menjadi "tersedia".']); return; } $idKunjunganToEnd = $activeKunjungan['id_kunjungan_to_end']; $idAnakAffected = $activeKunjungan['id_anak_affected']; // 2. Update waktu_keluar di tabel 'kunjungan' untuk sesi yang ditemukan $queryUpdateKunjungan = "UPDATE kunjungan SET waktu_keluar = NOW() WHERE id = :id_kunjungan"; $stmtUpdateKunjungan = $this->conn->prepare($queryUpdateKunjungan); $stmtUpdateKunjungan->bindParam(':id_kunjungan', $idKunjunganToEnd); $stmtUpdateKunjungan->execute(); // 3. Update status di tabel 'rfid_tags' menjadi 'tersedia' $queryUpdateRfid = "UPDATE rfid_tags SET status = 'tersedia' WHERE kode_gelang = :kode_gelang"; $stmtUpdateRfid = $this->conn->prepare($queryUpdateRfid); $stmtUpdateRfid->bindParam(':kode_gelang', $kodeGelang); $stmtUpdateRfid->execute(); // 4. Clear rfid_temp after ending session $this->clearRfidTemp(); $this->conn->commit(); echo json_encode(['status' => 'success', 'message' => 'Sesi permainan berhasil diakhiri! Gelang telah dibebaskan.']); } catch(PDOException $e) { $this->conn->rollBack(); error_log("PDOException di endPlaySession: " . $e->getMessage() . " di baris " . $e->getLine()); http_response_code(500); echo json_encode(['status' => 'error', 'message' => 'Error mengakhiri sesi (DB): ' . $e->getMessage()]); } catch(Exception $e) { $this->conn->rollBack(); error_log("Exception umum di endPlaySession: " . $e->getMessage() . " di baris " . $e->getLine()); http_response_code(500); echo json_encode(['status' => 'error', 'message' => 'Error sistem (umum): ' . $e->getMessage()]); } } /** * Fungsi untuk mereset status gelang di rfid_tags menjadi 'tersedia'. * Berguna untuk mengatasi inkonsistensi data. * @param string $kodeGelang */ private function resetRfidTagStatus($kodeGelang) { try { $query = "UPDATE rfid_tags SET status = 'tersedia' WHERE kode_gelang = :kode_gelang"; $stmt = $this->conn->prepare($query); $stmt->bindParam(':kode_gelang', $kodeGelang); $stmt->execute(); error_log("Gelang " . $kodeGelang . " statusnya berhasil direset menjadi 'tersedia' karena inkonsistensi data."); } catch(PDOException $e) { error_log("Error resetting RFID tag status for " . $kodeGelang . ": " . $e->getMessage()); } } private function clearRfidTemp($returnJson = false) { try { $query = "DELETE FROM rfid_temp"; $stmt = $this->conn->prepare($query); $stmt->execute(); if ($returnJson) { echo json_encode(['status' => 'success', 'message' => 'RFID temp cleared']); } } catch(PDOException $e) { error_log("Error clearing RFID temp: " . $e->getMessage()); if ($returnJson) { http_response_code(500); echo json_encode(['status' => 'error', 'message' => 'Error clearing RFID temp: ' . $e->getMessage()]); } } } private function formatDuration($minutes) { $hours = floor($minutes / 60); $remainingMinutes = $minutes % 60; return sprintf('%02d:%02d:00', $hours, $remainingMinutes); } } $controller = new TambahPengunjung(); $controller->handleRequest();