TKK_E32222685/WEB-playground/api/tambah_pengunjung.php

555 lines
24 KiB
PHP

<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/../logs/php_error.log');
require_once '../config/database.php';
require_once '../includes/auth.php';
class TambahPengunjung {
private $conn;
private $auth;
public function __construct() {
$database = new Database();
$this->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();