Tutorial lengkap membangun sistem absensi mahasiswa berbasis GPS dan Kamera menggunakan Kodular, PHP Native, dan MySQL
โฌ Download File-- ============================================ -- Smart Attendance System - Database Setup -- ============================================ CREATE DATABASE IF NOT EXISTS absensi_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE absensi_db; -- Tabel Users (Mahasiswa) CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, nama VARCHAR(100) NOT NULL, nim VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- Tabel Absensi CREATE TABLE absensi ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, latitude DOUBLE NOT NULL, longitude DOUBLE NOT NULL, foto TEXT, waktu DATETIME DEFAULT CURRENT_TIMESTAMP, status VARCHAR(20) DEFAULT 'hadir', FOREIGN KEY (user_id) REFERENCES users(id) ); -- Data sample mahasiswa (password: 123456) INSERT INTO users (nama, nim, password) VALUES ('Ahmad Fauzi', '2021001', MD5('123456')), ('Siti Rahayu', '2021002', MD5('123456')), ('Budi Santoso', '2021003', MD5('123456'));
<?php // Konfigurasi koneksi database define('DB_HOST', 'localhost'); define('DB_USER', 'root'); define('DB_PASS', ''); define('DB_NAME', 'absensi_db'); // Header CORS untuk akses dari mobile app header('Access-Control-Allow-Origin: *'); header('Content-Type: application/json'); header('Access-Control-Allow-Methods: POST, GET'); function getConnection() { $conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME); if ($conn->connect_error) { http_response_code(500); echo json_encode([ 'status' => 'error', 'message' => 'Koneksi database gagal' ]); exit(); } $conn->set_charset('utf8mb4'); return $conn; } function sendResponse($status, $message, $data = null) { $response = ['status' => $status, 'message' => $message]; if ($data !== null) $response['data'] = $data; echo json_encode($response); exit(); } ?>
<?php require_once 'koneksi.php'; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { sendResponse('error', 'Method tidak diizinkan'); } // Ambil data dari POST atau JSON body $input = json_decode(file_get_contents('php://input'), true); $nim = trim($input['nim'] ?? $_POST['nim'] ?? ''); $password = trim($input['password'] ?? $_POST['password'] ?? ''); if (empty($nim) || empty($password)) { sendResponse('error', 'NIM dan password wajib diisi'); } $conn = getConnection(); $passMd5 = md5($password); $stmt = $conn->prepare( "SELECT id, nama, nim FROM users WHERE nim = ? AND password = ?" ); $stmt->bind_param('ss', $nim, $passMd5); $stmt->execute(); $result = $stmt->get_result(); if ($result->num_rows === 0) { sendResponse('error', 'NIM atau password salah'); } $user = $result->fetch_assoc(); $conn->close(); sendResponse('success', 'Login berhasil', [ 'user_id' => $user['id'], 'nama' => $user['nama'], 'nim' => $user['nim'] ]); ?>
<?php require_once 'koneksi.php'; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { sendResponse('error', 'Method tidak diizinkan'); } $input = json_decode(file_get_contents('php://input'), true); $userId = (int)($input['user_id'] ?? $_POST['user_id'] ?? 0); $latitude = (float)($input['latitude'] ?? $_POST['latitude'] ?? 0); $longitude = (float)($input['longitude'] ?? $_POST['longitude'] ?? 0); $foto = $input['foto'] ?? $_POST['foto'] ?? ''; if (!$userId || !$latitude || !$longitude) { sendResponse('error', 'Data tidak lengkap'); } // Koordinat kampus (sesuaikan dengan lokasi kampus) $kampusLat = -6.2088; $kampusLng = 106.8456; $maxRadius = 100; // meter // Hitung jarak menggunakan Haversine formula function hitungJarak($lat1, $lng1, $lat2, $lng2) { $R = 6371000; // radius bumi dalam meter $dLat = deg2rad($lat2 - $lat1); $dLng = deg2rad($lng2 - $lng1); $a = sin($dLat/2) * sin($dLat/2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2) * sin($dLng/2); return $R * 2 * atan2(sqrt($a), sqrt(1 - $a)); } $jarak = hitungJarak($latitude, $longitude, $kampusLat, $kampusLng); if ($jarak > $maxRadius) { sendResponse('error', 'Lokasi di luar radius kampus (' . round($jarak) . 'm)'); } // Tentukan status hadir/terlambat $jam = (int)date('H'); $menit = (int)date('i'); $status = ($jam < 8 || ($jam == 8 && $menit == 0)) ? 'hadir' : 'terlambat'; // Simpan foto Base64 jika ada $fotoPath = ''; if (!empty($foto)) { $fotoData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $foto)); $fotoName = 'foto_' . $userId . '_' . time() . '.jpg'; $fotoDir = __DIR__ . '/../uploads/'; if (!is_dir($fotoDir)) mkdir($fotoDir, 0755, true); file_put_contents($fotoDir . $fotoName, $fotoData); $fotoPath = 'uploads/' . $fotoName; } $conn = getConnection(); $stmt = $conn->prepare( "INSERT INTO absensi (user_id,latitude,longitude,foto,status) VALUES (?,?,?,?,?)" ); $stmt->bind_param('iddss', $userId, $latitude, $longitude, $fotoPath, $status); $stmt->execute(); $conn->close(); sendResponse('success', 'Absensi berhasil', [ 'status' => $status, 'jarak' => round($jarak) . 'm', 'waktu' => date('Y-m-d H:i:s') ]); ?>
<?php require_once 'koneksi.php'; $userId = (int)($_GET['user_id'] ?? 0); if (!$userId) { sendResponse('error', 'user_id diperlukan'); } $conn = getConnection(); $stmt = $conn->prepare(" SELECT a.id, a.latitude, a.longitude, a.foto, a.waktu, a.status, u.nama, u.nim FROM absensi a JOIN users u ON a.user_id = u.id WHERE a.user_id = ? ORDER BY a.waktu DESC LIMIT 30 "); $stmt->bind_param('i', $userId); $stmt->execute(); $result = $stmt->get_result(); $data = []; while ($row = $result->fetch_assoc()) { $data[] = $row; } $conn->close(); sendResponse('success', 'Data riwayat', $data); ?>
<?php require_once 'koneksi.php'; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { sendResponse('error', 'Method tidak diizinkan'); } $input = json_decode(file_get_contents('php://input'), true); $base64 = $input['foto'] ?? ''; if (empty($base64)) { sendResponse('error', 'Data foto kosong'); } // Bersihkan header base64 jika ada $imageData = preg_replace('#^data:image/\w+;base64,#i', '', $base64); $decoded = base64_decode($imageData); if ($decoded === false) { sendResponse('error', 'Format base64 tidak valid'); } $uploadDir = __DIR__ . '/../uploads/'; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); } $filename = 'selfie_' . uniqid() . '.jpg'; $filepath = $uploadDir . $filename; if (file_put_contents($filepath, $decoded) === false) { sendResponse('error', 'Gagal menyimpan foto'); } sendResponse('success', 'Foto berhasil diupload', [ 'path' => 'uploads/' . $filename, 'url' => 'http://localhost/absensi/uploads/' . $filename ]); ?>
<?php require_once '../api/koneksi.php'; // Nonaktifkan JSON header untuk halaman HTML header_remove('Content-Type'); header('Content-Type: text/html; charset=utf-8'); $conn = getConnection(); $result = $conn->query(" SELECT a.*, u.nama, u.nim FROM absensi a JOIN users u ON a.user_id = u.id ORDER BY a.waktu DESC LIMIT 50 "); $absensi = $result->fetch_all(MYSQLI_ASSOC); $total = $conn->query("SELECT COUNT(*) as c FROM absensi")->fetch_assoc()['c']; $hadir = $conn->query("SELECT COUNT(*) as c FROM absensi WHERE status='hadir'")->fetch_assoc()['c']; $terlambat = $conn->query("SELECT COUNT(*) as c FROM absensi WHERE status='terlambat'")->fetch_assoc()['c']; $conn->close(); ?> <!DOCTYPE html> <html lang="id"> <head> <meta charset="UTF-8"> <title>Admin Dashboard Absensi</title> <style> body { font-family: sans-serif; background: #0a1628; color: #fff; margin: 0; padding: 20px; } h1 { color: #f97316; } .stats { display: flex; gap: 20px; margin: 20px 0; } .stat { background: #1a3a6b; padding: 20px; border-radius: 12px; text-align: center; flex: 1; } .stat .num { font-size: 2rem; font-weight: 900; color: #f97316; } table { width: 100%; border-collapse: collapse; background: #1a3a6b; border-radius: 12px; overflow: hidden; } th { background: #2563eb; padding: 12px; text-align: left; font-size: 0.85rem; } td { padding: 10px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); font-size: 0.82rem; } .hadir { color: #22c55e; font-weight: 700; } .terlambat { color: #f97316; font-weight: 700; } </style> </head> <body> <h1>๐ Dashboard Admin Absensi</h1> <div class="stats"> <div class="stat"><div class="num"><?= $total ?></div>Total Absensi</div> <div class="stat"><div class="num" style="color:#22c55e"><?= $hadir ?></div>Hadir</div> <div class="stat"><div class="num"><?= $terlambat ?></div>Terlambat</div> </div> <table> <tr><th>Nama</th><th>NIM</th><th>Waktu</th><th>Lokasi</th><th>Status</th></tr> <?php foreach ($absensi as $row): ?> <tr> <td><?= htmlspecialchars($row['nama']) ?></td> <td><?= htmlspecialchars($row['nim']) ?></td> <td><?= $row['waktu'] ?></td> <td><?= round($row['latitude'],4) ?>, <?= round($row['longitude'],4) ?></td> <td class="<?= $row['status'] ?>"><?= strtoupper($row['status']) ?></td> </tr> <?php endforeach; ?> </table> </body></html>
Download dari apachefriends.org, install dan jalankan Apache + MySQL dari XAMPP Control Panel.
Buat folder absensi di dalam C:/xampp/htdocs/
Buka phpMyAdmin di localhost/phpmyadmin, buat database absensi_db, lalu import file SQL.
Salin semua file PHP ke folder htdocs/absensi/api/ dan htdocs/absensi/admin/
Buka browser, akses localhost/absensi/api/login.php untuk verifikasi.
Akses create.kodular.io di browser, login atau buat akun baru.
Klik "New Project", beri nama SmartAttendance, pilih template kosong.
Install app Kodular Companion di HP Android untuk live testing langsung dari browser.
Tambahkan: LocationSensor, Camera, Web (HTTP), TinyDB, dan komponen UI yang diperlukan.
Ganti localhost dengan IP komputer kamu (cek via ipconfig), contoh: 192.168.1.x