Files
bij/public/ios/app/rekam.php
2026-04-21 05:59:39 +07:00

1063 lines
50 KiB
PHP

<?php
session_start();
require_once 'config.php';
// Check if user is logged in
if (!isset($_SESSION['token'])) {
header('Location: login.php');
exit;
}
$token = $_SESSION['token'];
$user_data = $_SESSION['user_data'] ?? [];
// Get user profile data
$profile_result = api_get_profil($token);
$profile_data = $profile_result['success'] ? $profile_result['data'] : [];
// Get today's attendance data
$presensi_today_result = api_get_presensi_today($token);
$presensi_today = $presensi_today_result['success'] ? $presensi_today_result['data'] : [];
// Extract user info - sama seperti di dashboard
$pegawai = $profile_data['pegawai'] ?? $profile_data['data']['pegawai'] ?? $profile_data['data'] ?? [];
$user_name = $pegawai['nama_lengkap'] ?? $pegawai['nama'] ?? $user_data['nama'] ?? $user_data['nama_lengkap'] ?? 'User';
$user_jabatan = $pegawai['jabatan']['nama_jabatan'] ?? $pegawai['jabatan'] ?? $user_data['jabatan'] ?? $user_data['nama_jabatan'] ?? 'Karyawan';
$user_id = $pegawai['nip'] ?? $pegawai['id'] ?? $user_data['id'] ?? $user_data['nip'] ?? '';
$user_photo = uploads_pengguna_url((string) ($pegawai['photo'] ?? $pegawai['foto'] ?? $user_data['foto'] ?? $user_data['photo'] ?? ''));
// Extract work schedule info
$jam_masuk = $pegawai['jam_masuk'] ?? '08:00';
$jam_pulang = $pegawai['jam_pulang'] ?? '17:00';
$jam_istirahat = $pegawai['jam_istirahat'] ?? '12:00';
$kantor_nama = $pegawai['kantor']['nama_kantor'] ?? $user_data['kantor'] ?? 'Kantor Pusat';
$alamat_kantor = $pegawai['kantor']['alamat_kantor'] ?? $pegawai['alamat_kantor'] ?? $user_data['alamat_kantor'] ?? '';
// Extract kantor coordinates for distance validation - cek berbagai kemungkinan field
$kantor_lat = null;
$kantor_lng = null;
$toleransi_meter = 100; // Default 100 meter
// Cek di berbagai level struktur data
if (isset($pegawai['kantor'])) {
$kantor_data = $pegawai['kantor'];
$kantor_lat = $kantor_data['latitude'] ?? $kantor_data['lat'] ?? $kantor_data['koordinat_lat'] ?? $kantor_data['lat_kantor'] ?? null;
$kantor_lng = $kantor_data['longitude'] ?? $kantor_data['lng'] ?? $kantor_data['koordinat_lng'] ?? $kantor_data['lng_kantor'] ?? null;
$toleransi_meter = $kantor_data['jarak_rekam_presensi'] ?? $kantor_data['toleransi'] ?? $kantor_data['radius'] ?? $kantor_data['jarak_toleransi'] ?? 100;
} elseif (isset($profile_data['kantor'])) {
$kantor_data = $profile_data['kantor'];
$kantor_lat = $kantor_data['latitude'] ?? $kantor_data['lat'] ?? $kantor_data['koordinat_lat'] ?? $kantor_data['lat_kantor'] ?? null;
$kantor_lng = $kantor_data['longitude'] ?? $kantor_data['lng'] ?? $kantor_data['koordinat_lng'] ?? $kantor_data['lng_kantor'] ?? null;
$toleransi_meter = $kantor_data['jarak_rekam_presensi'] ?? $kantor_data['toleransi'] ?? $kantor_data['radius'] ?? $kantor_data['jarak_toleransi'] ?? 100;
} elseif (isset($profile_data['data']['kantor'])) {
$kantor_data = $profile_data['data']['kantor'];
$kantor_lat = $kantor_data['latitude'] ?? $kantor_data['lat'] ?? $kantor_data['koordinat_lat'] ?? $kantor_data['lat_kantor'] ?? null;
$kantor_lng = $kantor_data['longitude'] ?? $kantor_data['lng'] ?? $kantor_data['koordinat_lng'] ?? $kantor_data['lng_kantor'] ?? null;
$toleransi_meter = $kantor_data['jarak_rekam_presensi'] ?? $kantor_data['toleransi'] ?? $kantor_data['radius'] ?? $kantor_data['jarak_toleransi'] ?? 100;
}
// Fallback ke default jika tidak ditemukan
if ($kantor_lat === null) {
$kantor_lat = -7.212239323216; // Koordinat kantor sebenarnya dari API
$kantor_lng = 107.902314730224; // Koordinat kantor sebenarnya dari API
$toleransi_meter = 20; // Toleransi sebenarnya dari API (jarak_rekam_presensi)
}
// Debug log untuk troubleshooting
error_log("Kantor coordinates - Lat: $kantor_lat, Lng: $kantor_lng, Toleransi: $toleransi_meter");
// Extract today's attendance status (API/DB bisa kirim 00:00:00 walau belum rekam — pakai presensi_waktu_terisi)
$presensi_data_today = $presensi_today['data'] ?? [];
$masuk_time = $presensi_data_today['jam_masuk'] ?? null;
$istirahat_mulai = $presensi_data_today['mulai_istirahat'] ?? null;
$istirahat_selesai = $presensi_data_today['beres_istirahat'] ?? null;
$pulang_time = $presensi_data_today['jam_pulang'] ?? null;
$masuk_disabled = presensi_waktu_terisi($masuk_time);
$istirahat_disabled = presensi_waktu_terisi($istirahat_mulai);
$pulang_disabled = presensi_waktu_terisi($pulang_time);
// Helper function untuk format tanggal Indonesia
function formatTanggalIndonesia($tanggal = null, $format = 'l, d F Y')
{
$hari = ['Sunday' => 'Minggu', 'Monday' => 'Senin', 'Tuesday' => 'Selasa', 'Wednesday' => 'Rabu', 'Thursday' => 'Kamis', 'Friday' => 'Jumat', 'Saturday' => 'Sabtu'];
$bulan = ['January' => 'Januari', 'February' => 'Februari', 'March' => 'Maret', 'April' => 'April', 'May' => 'Mei', 'June' => 'Juni', 'July' => 'Juli', 'August' => 'Agustus', 'September' => 'September', 'October' => 'Oktober', 'November' => 'November', 'December' => 'Desember'];
if ($tanggal) {
$tanggal_inggris = date($format, strtotime($tanggal));
} else {
$tanggal_inggris = date($format);
}
$tanggal_indonesia = str_replace(array_keys($hari), array_values($hari), str_replace(array_keys($bulan), array_values($bulan), $tanggal_inggris));
return $tanggal_indonesia;
}
// Handle form submission
$success_message = '';
$error_message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$latitude = $_POST['latitude'] ?? '';
$longitude = $_POST['longitude'] ?? '';
$photo_base64 = $_POST['photo_base64'] ?? '';
$data = [
'latitude' => $latitude,
'longitude' => $longitude,
'waktu' => date('Y-m-d H:i:s')
];
// Handle photo dari kamera (base64) jika tersedia
if (!empty($photo_base64)) {
$data['photo'] = $photo_base64;
}
$result = null;
switch ($action) {
case 'masuk':
$result = api_save_masuk($token, $data);
break;
case 'pulang':
$result = api_save_pulang($token, $data);
break;
case 'istirahat':
$result = api_save_istirahat($token, $data);
break;
}
if ($result && $result['success']) {
$success_message = 'Presensi berhasil direkam!';
} else {
// Handle berbagai jenis error dari API
$api_message = '';
if (isset($result['data']['pesan'])) {
$api_message = $result['data']['pesan'];
} elseif (isset($result['data']['message'])) {
$api_message = $result['data']['message'];
} elseif (isset($result['data']['error'])) {
$api_message = $result['data']['error'];
}
// Debug info untuk troubleshooting
error_log("Presensi API Error - Action: $action, Message: $api_message, Full Response: " . json_encode($result));
$error_message = $api_message ?: 'Gagal merekam presensi.';
}
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1">
<meta name="theme-color" content="#F2F2F7">
<meta name="color-scheme" content="light">
<!-- ✅ PATCH: Izinkan lokasi & kamera di mobile browser -->
<meta name="permissions-policy" content="geolocation=(self), camera=(self)">
<meta name="referrer" content="no-referrer">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<title>Bank BIJ — Rekam presensi</title>
<link rel="icon" type="image/png" href="<?php echo htmlspecialchars(site_public_asset('assets/images/bij_logo.png')); ?>">
<link rel="stylesheet" href="<?php echo asset('css/style.css'); ?>">
<link rel="stylesheet" href="<?php echo asset('css/tailwind.css'); ?>">
<link rel="stylesheet" href="<?php echo asset('css/ios-app.css'); ?>">
<link rel="stylesheet" href="<?php echo asset('fa-7.1.0-web/css/all.min.css'); ?>">
</head>
<body class="ios-app">
<div class="mobile-frame p-4">
<header class="ios-screen-head">
<div class="ios-screen-head__row">
<div class="bank-logo flex-shrink-0">
<img src="<?php echo asset('logo_bij3.png'); ?>" alt="Logo Bank BIJ">
</div>
<div class="ios-head-meta">
<h1 class="ios-head-brand">BPR <span class="ios-accent">Intan Jabar</span></h1>
<p class="ios-head-page">Rekam presensi</p>
</div>
<div class="ios-head-user">
<span class="ios-head-name"><?php echo htmlspecialchars($user_name); ?></span>
<span class="ios-head-role"><?php echo htmlspecialchars($user_jabatan); ?></span>
</div>
</div>
</header>
<nav class="ios-nav-back" aria-label="Navigasi">
<a href="dashboard.php"><i class="fas fa-chevron-left" aria-hidden="true"></i> Dashboard</a>
</nav>
<!-- Error/Success Messages -->
<?php if (!empty($error_message)): ?>
<div class="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded-lg">
<div class="flex items-center">
<i class="fas fa-exclamation-circle mr-2"></i>
<?php echo htmlspecialchars($error_message); ?>
</div>
</div>
<?php endif; ?>
<?php if (!empty($success_message)): ?>
<div class="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded-lg">
<div class="flex items-center">
<i class="fas fa-check-circle mr-2"></i>
<?php echo htmlspecialchars($success_message); ?>
</div>
</div>
<?php endif; ?>
<!-- Informasi Rekam Masuk -->
<div class="dashboard-card p-5 mb-4">
<h2 class="text-xl font-bold text-gray-800 mb-4 text-center">Rekam Kehadiran</h2>
<!-- Informasi Karyawan -->
<div class="mb-5 p-3 bg-blue-50 rounded-lg">
<div class="flex items-center">
<?php if ($user_photo !== ''): ?>
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-3 overflow-hidden">
<img src="<?php echo htmlspecialchars($user_photo); ?>" alt="Foto Profil" class="w-full h-full object-cover">
</div>
<?php else: ?>
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-3">
<i class="fas fa-user text-blue-700"></i>
</div>
<?php endif; ?>
<div>
<p class="font-medium text-gray-800"><?php echo htmlspecialchars($user_name); ?></p>
<p class="text-sm text-gray-600"><?php echo htmlspecialchars($user_jabatan); ?></p>
<p class="text-xs text-gray-500">NIP: <?php echo htmlspecialchars($user_id); ?></p>
</div>
</div>
</div>
<!-- Tanggal dan Waktu -->
<div class="mb-5">
<div class="flex justify-between items-center mb-2">
<p class="text-gray-700 font-medium">Tanggal</p>
<p class="text-gray-800" id="current-date"><?php echo formatTanggalIndonesia(); ?></p>
</div>
<div class="flex justify-between items-center">
<p class="text-gray-700 font-medium">Waktu Sekarang</p>
<p class="text-xl font-bold text-blue-700" id="current-time">08:45:32</p>
</div>
</div>
<!-- Pilihan Presensi -->
<div class="mb-5">
<h3 class="text-gray-700 font-medium mb-4 text-center" id="label-jenis-presensi">Pilih Jenis Presensi</h3>
<div class="grid grid-cols-3 gap-2 sm:gap-4" role="radiogroup" aria-labelledby="label-jenis-presensi">
<?php
if (!$masuk_disabled) {
$default_action = 'masuk';
} elseif (!$istirahat_disabled) {
$default_action = 'istirahat';
} elseif (!$pulang_disabled) {
$default_action = 'pulang';
} else {
$default_action = 'masuk';
}
$rekam_label = ['masuk' => 'Masuk', 'istirahat' => 'Istirahat', 'pulang' => 'Pulang'];
?>
<button type="button" role="radio" aria-checked="<?php echo ($default_action === 'masuk' && !$masuk_disabled) ? 'true' : 'false'; ?>" class="presensi-btn presensi-btn--masuk py-4 px-4 rounded-xl font-medium flex flex-col items-center justify-center transition-all duration-200 <?php echo $masuk_disabled ? 'presensi-btn--done' : ($default_action === 'masuk' ? 'presensi-btn--selected' : 'presensi-btn--idle'); ?>" data-action="masuk" <?php echo $masuk_disabled ? 'disabled' : ''; ?>>
<span class="presensi-pick-badge" aria-hidden="true">✓</span>
<i class="fas fa-sign-in-alt text-2xl mb-2"></i>
<span class="text-sm font-semibold"><?php echo $masuk_disabled ? 'Sudah' : 'Masuk'; ?></span>
<?php if ($masuk_disabled): ?>
<span class="text-xs opacity-75 mt-1"><?php echo htmlspecialchars((string) $masuk_time); ?></span>
<?php endif; ?>
</button>
<button type="button" role="radio" aria-checked="<?php echo ($default_action === 'istirahat' && !$istirahat_disabled) ? 'true' : 'false'; ?>" class="presensi-btn presensi-btn--istirahat py-4 px-4 rounded-xl font-medium flex flex-col items-center justify-center transition-all duration-200 <?php echo $istirahat_disabled ? 'presensi-btn--done' : ($default_action === 'istirahat' ? 'presensi-btn--selected' : 'presensi-btn--idle'); ?>" data-action="istirahat" <?php echo $istirahat_disabled ? 'disabled' : ''; ?>>
<span class="presensi-pick-badge" aria-hidden="true">✓</span>
<i class="fas fa-coffee text-2xl mb-2"></i>
<span class="text-sm font-semibold"><?php echo $istirahat_disabled ? 'Sudah' : 'Istirahat'; ?></span>
<?php if ($istirahat_disabled): ?>
<span class="text-xs opacity-75 mt-1"><?php echo htmlspecialchars((string) $istirahat_mulai); ?></span>
<?php endif; ?>
</button>
<button type="button" role="radio" aria-checked="<?php echo ($default_action === 'pulang' && !$pulang_disabled) ? 'true' : 'false'; ?>" class="presensi-btn presensi-btn--pulang py-4 px-4 rounded-xl font-medium flex flex-col items-center justify-center transition-all duration-200 <?php echo $pulang_disabled ? 'presensi-btn--done' : ($default_action === 'pulang' ? 'presensi-btn--selected' : 'presensi-btn--idle'); ?>" data-action="pulang" <?php echo $pulang_disabled ? 'disabled' : ''; ?>>
<span class="presensi-pick-badge" aria-hidden="true">✓</span>
<i class="fas fa-sign-out-alt text-2xl mb-2"></i>
<span class="text-sm font-semibold"><?php echo $pulang_disabled ? 'Sudah' : 'Pulang'; ?></span>
<?php if ($pulang_disabled): ?>
<span class="text-xs opacity-75 mt-1"><?php echo htmlspecialchars((string) $pulang_time); ?></span>
<?php endif; ?>
</button>
</div>
</div>
<!-- Lokasi Perekaman -->
<div class="mb-5">
<h3 class="text-gray-700 font-medium mb-3 text-sm">Lokasi Perekaman</h3>
<div class="bg-white border border-gray-200 rounded-lg p-3 shadow-sm">
<!-- GPS Status -->
<div class="mb-3">
<div class="flex items-center justify-between">
<span class="text-gray-600 text-sm font-medium">Status GPS:</span>
<div id="gps-status" class="flex items-center">
<div class="w-2 h-2 bg-yellow-500 rounded-full mr-2 pulse-animation"></div>
<span class="text-yellow-700 text-xs">Mendeteksi lokasi...</span>
</div>
</div>
</div>
<!-- Koordinat Kantor -->
<div class="mb-3 p-2 bg-blue-50 rounded">
<div class="flex items-center mb-1">
<i class="fas fa-building text-blue-600 mr-2 text-xs"></i>
<span class="text-gray-600 text-xs font-medium">Lokasi Kerja:</span>
</div>
<p class="text-gray-800 text-xs font-medium mb-1"><?php echo htmlspecialchars($kantor_nama); ?></p>
<p class="text-gray-800 text-xs font-mono" id="kantor-coords"><?php echo $kantor_lat . ', ' . $kantor_lng; ?></p>
</div>
<!-- Koordinat Presensi -->
<div class="mb-3 p-2 bg-green-50 rounded">
<div class="flex items-center mb-1">
<i class="fas fa-map-marker-alt text-green-600 mr-2 text-xs"></i>
<span class="text-gray-600 text-xs font-medium">Koordinat Presensi:</span>
</div>
<p class="text-gray-800 text-xs font-mono" id="presensi-coords">-</p>
</div>
<!-- Alamat (reverse geocode; id dipakai skrip) -->
<div class="mb-3 p-2 bg-gray-50 rounded">
<div class="flex items-center mb-1">
<i class="fas fa-road text-gray-500 mr-2 text-xs"></i>
<span class="text-gray-600 text-xs font-medium">Alamat perkiraan</span>
</div>
<p class="text-gray-800 text-xs leading-snug" id="location-address">—</p>
</div>
<!-- Jarak Presensi -->
<div class="mb-3 p-2 bg-amber-50 rounded">
<div class="flex items-center mb-1">
<i class="fas fa-ruler text-amber-600 mr-2 text-xs"></i>
<span class="text-gray-600 text-xs font-medium">Jarak Presensi:</span>
</div>
<p class="text-gray-800 text-xs font-mono" id="jarak-presensi">-</p>
</div>
<!-- Tombol Retry -->
<div class="text-center">
<button id="retryLocationBtn" class="hidden text-blue-600 hover:text-blue-800 font-medium transition-colors text-xs" onclick="getCurrentLocation()">
<i class="fas fa-redo mr-1"></i> Coba Lagi
</button>
</div>
</div>
</div>
<!-- Upload Foto -->
<div class="mb-6">
<h3 class="text-gray-700 font-medium mb-2">Foto Selfie</h3>
<div class="camera-preview" id="cameraPreview">
<i class="fas fa-camera text-gray-400 text-4xl mb-2"></i>
<p class="text-gray-500 text-center">Ambil foto selfie untuk presensi</p>
<p class="text-gray-400 text-sm text-center mt-1">Pastikan wajah terlihat jelas</p>
</div>
<div class="mt-3">
<button id="takePhotoBtn" class="action-btn w-full py-2 px-4 bg-blue-700 text-white rounded-lg flex items-center justify-center">
<i class="fas fa-camera mr-2"></i> Ambil Foto
</button>
</div>
</div>
<!-- Form Rekam Presensi -->
<form method="POST" enctype="multipart/form-data" id="presensiForm">
<input type="hidden" name="action" id="actionInput" value="<?php echo htmlspecialchars($default_action); ?>">
<input type="hidden" name="latitude" id="latitudeInput" value="">
<input type="hidden" name="longitude" id="longitudeInput" value="">
<input type="hidden" name="photo_base64" id="photoBase64Input" value="">
<!-- Tombol Submit -->
<button type="submit" id="recordAttendanceBtn" class="w-full py-3 bg-blue-700 text-white rounded-lg font-medium flex items-center justify-center action-btn">
<i class="fas fa-fingerprint mr-2"></i> Rekam <?php echo htmlspecialchars($rekam_label[$default_action] ?? 'Kehadiran'); ?>
</button>
<!-- Status Lokasi untuk Submit -->
<div id="locationSubmitStatus" class="mt-2 text-center text-sm hidden">
<p class="text-red-600">
<i class="fas fa-exclamation-triangle mr-1"></i>
Lokasi harus terdeteksi untuk merekam presensi
</p>
</div>
</form>
</div>
<!-- Footer -->
<div class="text-center mt-6 ios-foot">
<p>© 2024 Bank BIJ • v2.4.1</p>
</div>
</div>
<!-- Modal Konfirmasi -->
<div id="successModal" class="fixed inset-0 flex items-center justify-center z-50 hidden">
<div class="absolute inset-0 bg-black opacity-50"></div>
<div class="dashboard-card p-6 mx-4 z-10 w-full max-w-sm">
<div class="text-center">
<div class="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fas fa-check text-green-600 text-2xl"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-2">Berhasil!</h3>
<p class="text-gray-600 mb-4">Kehadiran masuk Anda telah berhasil direkam.</p>
<button id="closeModalBtn" class="w-full py-2 bg-blue-700 text-white rounded-lg font-medium">
Oke
</button>
</div>
</div>
</div>
<!-- Modal Peringatan Lokasi -->
<div id="locationModal" class="fixed inset-0 flex items-center justify-center z-50 hidden">
<div class="absolute inset-0 bg-black opacity-50"></div>
<div class="dashboard-card p-6 mx-4 z-10 w-full max-w-sm relative">
<button id="closeLocationModalBtn" class="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
<div class="text-center">
<div class="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fas fa-map-marker-alt text-red-600 text-2xl"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-2">Lokasi Diperlukan!</h3>
<p class="text-gray-600 mb-4">Untuk merekam presensi, Anda harus mengaktifkan akses lokasi. Silakan:</p>
<div class="text-left text-sm text-gray-600 mb-4">
<p>1. Klik ikon lokasi di address bar browser</p>
<p>2. Pilih "Izinkan" atau "Allow"</p>
<p>3. Refresh halaman ini</p>
</div>
<div class="flex gap-2">
<button id="retryLocationModalBtn" class="flex-1 py-2 bg-blue-700 text-white rounded-lg font-medium">
<i class="fas fa-redo mr-1"></i> Coba Lagi
</button>
<button id="refreshPageBtn" class="flex-1 py-2 bg-gray-500 text-white rounded-lg font-medium">
<i class="fas fa-refresh mr-1"></i> Refresh
</button>
</div>
</div>
</div>
</div>
<script>
// Update waktu dan tanggal secara real-time
function updateDateTime() {
const now = new Date();
// Format tanggal
const optionsDate = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
};
const formattedDate = now.toLocaleDateString('id-ID', optionsDate);
document.getElementById('current-date').textContent = formattedDate;
// Format waktu
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
document.getElementById('current-time').textContent = `${hours}:${minutes}:${seconds}`;
}
// Update waktu setiap detik
setInterval(updateDateTime, 1000);
updateDateTime(); // Panggil sekali saat pertama kali load
function setLocationAddress(text) {
const el = document.getElementById('location-address');
if (el) {
el.textContent = text;
}
}
// Function untuk reverse geocoding (koordinat ke alamat)
function reverseGeocode(lat, lng) {
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1&accept-language=id`;
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
return response.json();
})
.then(data => {
if (data && data.display_name) {
let address = data.display_name;
const parts = address.split(', ');
const shortAddress = parts.length >= 3 ? parts.slice(-3).join(', ') : address;
setLocationAddress(shortAddress);
const distanceValidation = validateDistance(lat, lng);
document.getElementById('jarak-presensi').textContent =
`${distanceValidation.distance.toFixed(0)} meter (${distanceValidation.valid ? 'Dalam jangkauan' : 'Di luar jangkauan'})`;
} else {
throw new Error('Tidak dapat mendapatkan alamat');
}
})
.catch(error => {
console.error('Error reverse geocoding:', error);
setLocationAddress('Alamat tidak dapat dideteksi');
});
}
// Function untuk mendapatkan lokasi saat ini
function getCurrentLocation() {
console.log('=== GEOLOCATION DEBUG ===');
console.log('Navigator geolocation:', !!navigator.geolocation);
console.log('Protocol:', location.protocol);
console.log('Hostname:', location.hostname);
console.log('User Agent:', navigator.userAgent);
if (navigator.geolocation) {
const retryBtn = document.getElementById('retryLocationBtn');
// Reset status
locationDetected = false;
locationError = false;
retryBtn.classList.add('hidden');
// Update GPS status
const gpsStatus = document.getElementById('gps-status');
gpsStatus.innerHTML = `
<div class="w-2 h-2 bg-yellow-500 rounded-full mr-2 pulse-animation"></div>
<span class="text-yellow-700 text-xs">Mendeteksi lokasi...</span>
`;
// Cek permission status terlebih dahulu
if (navigator.permissions) {
navigator.permissions.query({
name: 'geolocation'
}).then(function(result) {
console.log('Geolocation permission:', result.state);
if (result.state === 'denied') {
showLocationError('Akses lokasi ditolak. Silakan aktifkan di pengaturan browser.');
return;
}
// Lanjutkan dengan getCurrentPosition
requestLocation();
}).catch(function(error) {
console.log('Permission query failed:', error);
// Lanjutkan dengan getCurrentPosition jika permission query gagal
requestLocation();
});
} else {
// Browser tidak support permission API, langsung coba
console.log('Permission API not supported, trying direct geolocation...');
requestLocation();
}
function requestLocation() {
navigator.geolocation.getCurrentPosition(
function(position) {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
const accuracy = position.coords.accuracy;
console.log('✅ Location obtained successfully!');
console.log('Latitude:', lat);
console.log('Longitude:', lng);
console.log('Accuracy:', accuracy, 'meters');
// Simpan koordinat ke hidden input
document.getElementById('latitudeInput').value = lat;
document.getElementById('longitudeInput').value = lng;
// Set status lokasi berhasil
locationDetected = true;
locationError = false;
// Update koordinat presensi
document.getElementById('presensi-coords').textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
// Hitung dan tampilkan jarak
const distanceValidation = validateDistance(lat, lng);
document.getElementById('jarak-presensi').textContent = `${distanceValidation.distance.toFixed(0)} meter (${distanceValidation.valid ? 'Dalam jangkauan' : 'Di luar jangkauan'})`;
// Update GPS status
const gpsStatus = document.getElementById('gps-status');
gpsStatus.innerHTML = `
<div class="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
<span class="text-green-700 text-xs">GPS Aktif</span>
`;
// Reverse geocoding untuk mendapatkan alamat
reverseGeocode(lat, lng);
},
function(error) {
console.error('❌ Geolocation Error:', error);
console.error('Error code:', error.code);
console.error('Error message:', error.message);
showLocationError(getErrorMessage(error));
}, {
enableHighAccuracy: true,
timeout: 30000, // 30 detik timeout untuk mobile
maximumAge: 0 // Jangan gunakan cache
}
);
}
function showLocationError(message) {
// Set status lokasi error
locationDetected = false;
locationError = true;
setLocationAddress(message);
document.getElementById('presensi-coords').textContent = '-';
document.getElementById('jarak-presensi').textContent = '-';
// Update GPS status
const gpsStatus = document.getElementById('gps-status');
gpsStatus.innerHTML = `
<div class="w-2 h-2 bg-red-500 rounded-full mr-2"></div>
<span class="text-red-700 text-xs">GPS Tidak Aktif</span>
`;
// Tampilkan tombol retry
retryBtn.classList.remove('hidden');
// Tampilkan modal peringatan
document.getElementById('locationModal').classList.remove('hidden');
}
function getErrorMessage(error) {
switch (error.code) {
case error.PERMISSION_DENIED:
return 'Akses lokasi ditolak. Silakan aktifkan di pengaturan browser.';
case error.POSITION_UNAVAILABLE:
return 'Lokasi tidak tersedia. Pastikan GPS aktif.';
case error.TIMEOUT:
return 'Timeout mendeteksi lokasi. Coba lagi.';
default:
return 'Tidak dapat mengakses lokasi.';
}
}
} else {
// Set status lokasi error
locationDetected = false;
locationError = true;
setLocationAddress('Geolocation tidak didukung');
document.getElementById('presensi-coords').textContent = '-';
document.getElementById('jarak-presensi').textContent = '-';
// Update GPS status
const gpsStatus = document.getElementById('gps-status');
gpsStatus.innerHTML = `
<div class="w-2 h-2 bg-red-500 rounded-full mr-2"></div>
<span class="text-red-700 text-xs">GPS Tidak Didukung</span>
`;
// Tampilkan tombol retry
document.getElementById('retryLocationBtn').classList.remove('hidden');
// Tampilkan modal peringatan
document.getElementById('locationModal').classList.remove('hidden');
}
}
// Global variable untuk tracking lokasi
let locationDetected = false;
let locationError = false;
// Kantor coordinates dan toleransi dari PHP
const kantorLat = <?php echo $kantor_lat; ?>;
const kantorLng = <?php echo $kantor_lng; ?>;
const toleransiMeter = <?php echo $toleransi_meter; ?>;
// Function untuk menghitung jarak antara dua koordinat (Haversine formula)
function calculateDistance(lat1, lng1, lat2, lng2) {
const R = 6371000; // Radius bumi dalam meter
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Jarak dalam meter
}
// Function untuk validasi jarak dari kantor
function validateDistance(userLat, userLng) {
const distance = calculateDistance(kantorLat, kantorLng, userLat, userLng);
console.log(`Jarak dari kantor: ${distance.toFixed(0)} meter (toleransi: ${toleransiMeter} meter)`);
if (distance <= toleransiMeter) {
return {
valid: true,
distance: distance,
message: `Jarak: ${distance.toFixed(0)}m (dalam toleransi)`
};
} else {
return {
valid: false,
distance: distance,
message: `Jarak: ${distance.toFixed(0)}m (melebihi toleransi ${toleransiMeter}m)`
};
}
}
// Debug info untuk troubleshooting
console.log('Browser geolocation support:', !!navigator.geolocation);
console.log('Browser permissions API support:', !!navigator.permissions);
console.log('HTTPS protocol:', location.protocol === 'https:');
console.log('Kantor coordinates:', kantorLat, kantorLng);
console.log('Toleransi:', toleransiMeter, 'meter');
// Panggil getCurrentLocation saat halaman load
getCurrentLocation();
// Cleanup camera saat halaman di-unload
window.addEventListener('beforeunload', function() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
});
// Global variables untuk camera
let stream = null;
let video = null;
let canvas = null;
let isCameraActive = false;
// Handle tombol ambil foto
document.getElementById('takePhotoBtn').addEventListener('click', function() {
if (!isCameraActive) {
startCamera();
} else {
capturePhoto();
}
});
// Function untuk memulai camera
async function startCamera() {
try {
const cameraPreview = document.getElementById('cameraPreview');
// Tampilkan loading
cameraPreview.innerHTML = `
<div class="w-full h-full bg-gray-800 flex items-center justify-center">
<div class="text-white text-center">
<div class="w-16 h-16 border-2 border-white rounded-full mx-auto mb-2 flex items-center justify-center">
<i class="fas fa-spinner fa-spin text-white text-xl"></i>
</div>
<p>Mengakses Camera...</p>
</div>
</div>
`;
// Request camera access (mobile-friendly constraints + fallbacks)
const constraintsList = [{
video: {
facingMode: {
exact: 'user'
},
width: {
ideal: 640
},
height: {
ideal: 480
}
},
audio: false
},
{
video: {
facingMode: 'user',
width: {
ideal: 640
},
height: {
ideal: 480
}
},
audio: false
},
{
video: true,
audio: false
}
];
let lastError = null;
for (const constraints of constraintsList) {
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
break;
} catch (err) {
lastError = err;
console.warn('getUserMedia failed with constraints', constraints, err);
}
}
if (!stream) {
throw lastError || new Error('Camera stream not available');
}
// Create video element
video = document.createElement('video');
video.srcObject = stream;
video.autoplay = true;
video.muted = true;
// iOS Safari perlu playsinline agar tidak masuk fullscreen otomatis
video.setAttribute('playsinline', '');
video.style.width = '100%';
video.style.height = '100%';
video.style.objectFit = 'cover';
// Create canvas untuk capture
canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
// Update preview
cameraPreview.innerHTML = '';
cameraPreview.appendChild(video);
// Update button
const takePhotoBtn = document.getElementById('takePhotoBtn');
takePhotoBtn.innerHTML = '<i class="fas fa-camera mr-2"></i> Ambil Foto';
takePhotoBtn.classList.remove('bg-blue-700');
takePhotoBtn.classList.add('bg-green-700');
isCameraActive = true;
} catch (error) {
console.error('Error accessing camera:', error);
const cameraPreview = document.getElementById('cameraPreview');
cameraPreview.innerHTML = `
<div class="w-full h-full bg-red-100 flex items-center justify-center">
<div class="text-red-600 text-center">
<i class="fas fa-exclamation-triangle text-4xl mb-2"></i>
<p class="font-medium">Camera tidak dapat diakses</p>
<p class="text-sm">Pastikan:
<br>- Akses camera sudah diizinkan pada browser
<br>- Situs ini diakses via HTTPS (bukan HTTP)
<br>- Tidak ada aplikasi lain yang sedang memakai camera
</p>
<p class="text-sm mt-2">Alternatif: gunakan tombol "Unggah" untuk memilih foto dari galeri</p>
</div>
</div>
`;
}
}
// Function untuk capture foto
function capturePhoto() {
if (!video || !canvas) return;
const ctx = canvas.getContext('2d');
// Resize canvas untuk kompresi (max width 400px, maintain aspect ratio)
const maxWidth = 400;
const videoAspectRatio = video.videoWidth / video.videoHeight;
const newWidth = Math.min(maxWidth, video.videoWidth);
const newHeight = newWidth / videoAspectRatio;
canvas.width = newWidth;
canvas.height = newHeight;
// Draw video dengan ukuran yang sudah di-resize
ctx.drawImage(video, 0, 0, newWidth, newHeight);
// Convert ke data URL dengan kompresi tinggi (kualitas 60%)
const photoDataUrl = canvas.toDataURL('image/jpeg', 0.6);
// Update preview dengan foto yang diambil
const cameraPreview = document.getElementById('cameraPreview');
cameraPreview.innerHTML = `<img src="${photoDataUrl}" alt="Foto Selfie" style="width: 100%; height: 100%; object-fit: cover;">`;
// Simpan foto ke hidden input base64 untuk dikirim ke server
const photoBase64Input = document.getElementById('photoBase64Input');
// Hapus prefix data URL agar hanya kirim base64 murni
const base64Data = photoDataUrl.split(',')[1] || '';
photoBase64Input.value = base64Data;
// Tampilkan info ukuran (estimasi) untuk debug
const approxSizeKB = Math.round((base64Data.length * 3 / 4) / 1024);
console.log(`Foto terkompres ~${approxSizeKB}KB (${newWidth}x${newHeight})`);
// Stop camera
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
// Update button
const takePhotoBtn = document.getElementById('takePhotoBtn');
takePhotoBtn.innerHTML = '<i class="fas fa-camera mr-2"></i> Ambil Foto';
takePhotoBtn.classList.remove('bg-green-700');
takePhotoBtn.classList.add('bg-blue-700');
// Tampilkan pesan sukses di area foto
const successMsg = document.createElement('div');
successMsg.className = 'absolute bottom-2 left-0 right-0 text-center';
successMsg.innerHTML = '<span class="bg-green-500 text-white text-xs py-1 px-2 rounded">Foto berhasil diambil</span>';
cameraPreview.appendChild(successMsg);
isCameraActive = false;
}
// Function untuk convert data URL ke File
function dataURLtoFile(dataurl, filename) {
return new Promise((resolve) => {
const arr = dataurl.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
resolve(new File([u8arr], filename, {
type: mime
}));
});
}
// Function untuk kompres gambar
function compressImage(dataUrl, callback) {
const img = new Image();
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Resize untuk kompresi (max width 400px, maintain aspect ratio)
const maxWidth = 400;
const aspectRatio = img.width / img.height;
const newWidth = Math.min(maxWidth, img.width);
const newHeight = newWidth / aspectRatio;
canvas.width = newWidth;
canvas.height = newHeight;
// Draw image dengan ukuran yang sudah di-resize
ctx.drawImage(img, 0, 0, newWidth, newHeight);
// Convert ke data URL dengan kompresi tinggi (kualitas 60%)
const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.6);
callback(compressedDataUrl);
};
img.src = dataUrl;
}
// Upload via galeri di-nonaktifkan sesuai permintaan; hanya kamera yang digunakan
const rekamLabelMap = {
masuk: 'Masuk',
istirahat: 'Istirahat',
pulang: 'Pulang'
};
function syncPresensiToggle(selectedBtn) {
document.querySelectorAll('.presensi-btn').forEach(b => {
if (b.disabled) {
b.setAttribute('aria-checked', 'false');
return;
}
const on = b === selectedBtn;
b.classList.toggle('presensi-btn--selected', on);
b.classList.toggle('presensi-btn--idle', !on);
b.setAttribute('aria-checked', on ? 'true' : 'false');
});
}
document.querySelectorAll('.presensi-btn').forEach(btn => {
btn.addEventListener('click', function() {
if (this.disabled) {
return;
}
syncPresensiToggle(this);
const action = this.getAttribute('data-action');
document.getElementById('actionInput').value = action;
const submitBtn = document.getElementById('recordAttendanceBtn');
const label = rekamLabelMap[action] || action;
submitBtn.innerHTML = `<i class="fas fa-fingerprint mr-2"></i> Rekam ${label}`;
});
});
// Handle form submission
document.getElementById('presensiForm').addEventListener('submit', function(e) {
// Validasi lokasi sebelum submit
if (!locationDetected) {
e.preventDefault(); // Block submit
// Tampilkan status submit
document.getElementById('locationSubmitStatus').classList.remove('hidden');
// Tampilkan modal peringatan
document.getElementById('locationModal').classList.remove('hidden');
// Focus ke tombol retry
setTimeout(() => {
document.getElementById('retryLocationModalBtn').focus();
}, 100);
return false;
}
// Validasi jarak dari kantor
const userLat = parseFloat(document.getElementById('latitudeInput').value);
const userLng = parseFloat(document.getElementById('longitudeInput').value);
if (!isNaN(userLat) && !isNaN(userLng)) {
const distanceValidation = validateDistance(userLat, userLng);
if (!distanceValidation.valid) {
e.preventDefault(); // Block submit
// Tampilkan alert untuk jarak terlalu jauh
alert(`Presensi tidak dapat dilakukan!\n\n${distanceValidation.message}\n\nSilakan mendekat ke kantor untuk merekam presensi.`);
return false;
}
}
// Sembunyikan status submit jika lokasi OK
document.getElementById('locationSubmitStatus').classList.add('hidden');
const btn = document.getElementById('recordAttendanceBtn');
const originalText = btn.innerHTML;
// Tampilkan loading
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Memproses...';
btn.disabled = true;
// Koordinat sudah diambil saat halaman load, langsung submit
// Form akan submit dengan koordinat yang sudah ada di hidden input
});
// Handle tombol tutup modal
document.getElementById('closeModalBtn').addEventListener('click', function() {
document.getElementById('successModal').classList.add('hidden');
// Redirect ke dashboard (dalam implementasi nyata)
// window.location.href = 'dashboard.html';
});
// Handle tombol retry di modal lokasi
document.getElementById('retryLocationModalBtn').addEventListener('click', function() {
document.getElementById('locationModal').classList.add('hidden');
getCurrentLocation();
});
// Handle tombol refresh di modal lokasi
document.getElementById('refreshPageBtn').addEventListener('click', function() {
window.location.reload();
});
// Handle tombol close di modal lokasi
document.getElementById('closeLocationModalBtn').addEventListener('click', function() {
document.getElementById('locationModal').classList.add('hidden');
});
</script>
</body>
</html>