TKK_E32230273/edit_data.php

959 lines
50 KiB
PHP

<?php
session_start();
include 'koneksi.php';
include 'run_background.php';
$token = isset($_GET['token']) ? trim($_GET['token']) : '';
if (empty($token)) {
header("Location: 404.php");
exit;
}
// Validasi Token
$manifestPath = __DIR__ . '/manifest_edit_tokens.json';
if (!file_exists($manifestPath)) {
header("Location: 404.php");
exit;
}
$tokens = json_decode(file_get_contents($manifestPath), true) ?: [];
if (!isset($tokens[$token])) {
header("Location: 404.php");
exit;
}
$tokenData = $tokens[$token];
if (time() > $tokenData['expires_at']) {
unset($tokens[$token]);
file_put_contents($manifestPath, json_encode($tokens, JSON_PRETTY_PRINT));
header("Location: 404.php");
exit;
}
$id_occupant = $tokenData['id_occupant'];
// Ambil data occupant
$stmt = $pdo->prepare("SELECT name, nik_nip, no_hp, foto FROM occupant WHERE id_occupant = ?");
$stmt->execute([$id_occupant]);
$occupant = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$occupant) {
header("Location: 404.php");
exit;
}
// Proses jika form disubmit via AJAX
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$name = trim($_POST['name']);
$nik_nip = isset($_POST['nik_nip']) ? trim($_POST['nik_nip']) : '';
if (empty($name)) {
echo json_encode(["status" => "error", "message" => "Name is required"]);
exit;
}
$fotoPath = $occupant['foto']; // Default bawaan
if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) {
echo json_encode(["status" => "error", "message" => "Photo format not supported"]);
exit;
}
if ($_FILES['foto']['size'] > 5 * 1024 * 1024) {
echo json_encode(["status" => "error", "message" => "Max photo size is 5MB"]);
exit;
}
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$fotoName = 'occ_' . time() . '_' . random_int(1000, 9999) . '.' . $ext;
$targetPath = $uploadDir . $fotoName;
if (move_uploaded_file($_FILES['foto']['tmp_name'], $targetPath)) {
$fotoPath = 'uploads/' . $fotoName;
// Hapus foto lama jika ada dan bukan bawaan sistem
if (!empty($occupant['foto']) && file_exists(__DIR__ . '/' . $occupant['foto'])) {
@unlink(__DIR__ . '/' . $occupant['foto']);
}
} else {
echo json_encode(["status" => "error", "message" => "Failed to upload photo. Check folder permissions."]);
exit;
}
}
// Handle base64 foto_data from camera capture
$fotoData = isset($_POST['foto_data']) ? $_POST['foto_data'] : '';
if (!empty($fotoData) && strpos($fotoData, 'data:image') === 0) {
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Decode base64
$parts = explode(',', $fotoData, 2);
if (count($parts) === 2) {
$imgBinary = base64_decode($parts[1]);
if ($imgBinary !== false) {
$fotoName = 'occ_' . time() . '_' . random_int(1000, 9999) . '.jpg';
$targetPath = $uploadDir . $fotoName;
if (file_put_contents($targetPath, $imgBinary)) {
// Hapus foto lama
if (!empty($occupant['foto']) && file_exists(__DIR__ . '/' . $occupant['foto'])) {
@unlink(__DIR__ . '/' . $occupant['foto']);
}
$fotoPath = 'uploads/' . $fotoName;
}
}
}
}
try {
$stmtUpdate = $pdo->prepare("UPDATE occupant SET name = ?, nik_nip = ?, foto = ? WHERE id_occupant = ?");
$stmtUpdate->execute([$name, $nik_nip, $fotoPath, $id_occupant]);
// Success, invalidate token dan update pesan telegram
if (isset($tokenData['msg_id'])) {
$botToken = "8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho";
$chatId = '';
$tstmt = $pdo->prepare("SELECT telegram_id FROM occupant WHERE id_occupant = ?");
$tstmt->execute([$id_occupant]);
$tdata = $tstmt->fetch(PDO::FETCH_ASSOC);
if ($tdata && $tdata['telegram_id']) {
$chatId = $tdata['telegram_id'];
@file_get_contents("https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['msg_id']}");
if (isset($tokenData['user_msg_id'])) {
@file_get_contents("https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['user_msg_id']}");
}
// Send success message that auto-deletes after 5 seconds with live countdown
$successLabel = "✅ Data updated successfully";
$successText = "<b>" . $successLabel . "</b>";
$successPost = ['chat_id' => $chatId, 'text' => $successText, 'parse_mode' => 'HTML'];
$res = @file_get_contents("https://api.telegram.org/bot$botToken/sendMessage?" . http_build_query($successPost));
$resData = json_decode($res, true);
if (isset($resData['result']['message_id'])) {
$successMsgId = $resData['result']['message_id'];
runBackground(__DIR__ . '/auto_delete_quick.php', [$chatId, $successMsgId, $successLabel]);
}
}
}
unset($tokens[$token]);
file_put_contents($manifestPath, json_encode($tokens, JSON_PRETTY_PRINT));
echo json_encode(["status" => "success", "message" => "Data updated successfully!"]);
} catch (PDOException $e) {
echo json_encode(["status" => "error", "message" => "Failed to save: " . $e->getMessage()]);
}
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1, user-scalable=0">
<title>Profile Update - Identia</title>
<!-- CSS & Plugins (mirip manage_occupant) -->
<link rel="stylesheet" href="vendors/feather/feather.css">
<link rel="stylesheet" href="vendors/ti-icons/css/themify-icons.css">
<link rel="stylesheet" href="vendors/css/vendor.bundle.base.css">
<link rel="stylesheet" href="css/vertical-layout-light/style.css">
<link rel="stylesheet" href="vendors/mdi/css/materialdesignicons.min.css">
<style>
.auth .brand-logo img { width: 140px; max-width: 100%; }
.auth-form-light { border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
.custom-alert {
position: fixed; top: 20px; right: 20px; background: #fff;
border-left: 4px solid #fcd34d; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 15px 20px; border-radius: 4px; z-index: 9999;
display: none; align-items: center; gap: 12px; min-width: 300px;
max-width: 400px; animation: slideIn 0.3s ease-out;
}
.custom-alert.error { border-left-color: #ef4444; }
.custom-alert.success { border-left-color: #10b981; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
.custom-alert-icon { font-size: 24px; }
.custom-alert.error .custom-alert-icon { color: #ef4444; }
.custom-alert.success .custom-alert-icon { color: #10b981; }
.custom-alert.warning .custom-alert-icon { color: #f59e0b; }
.custom-alert-content { flex: 1; text-align: left; }
.custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }
.custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }
.custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }
.custom-alert-close:hover { color: #333; }
.info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 10px 12px; font-size: 13px; color: #1e293b; }
.submit-btn {
background: var(--primary); color: #fff; width: 100%; padding: 16px; border-radius: 16px; font-size: 16px; font-weight: 700; border: none; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 10px;
}
.info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }
body, .container-scroller, .page-body-wrapper, .content-wrapper { background-color: #f8f9fa !important; }
@media (max-width: 480px) {
.custom-alert { top: 10px; right: 10px; left: 10px; min-width: unset; max-width: unset; }
}
.success-checkmark { text-align: center; padding: 30px 10px; animation: scaleIn 0.5s ease; }
@keyframes scaleIn { 0% { transform: scale(0.5); opacity: 0; } 50% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }
.foto-wrapper:hover .foto-circle { border-color: #4747a1 !important; }
.foto-wrapper:active { transform: scale(0.95); }
/* Session Timer Pill */
.session-timer-pill {
position: fixed; top: 12px; right: 12px; z-index: 10000;
display: flex; align-items: center; gap: 6px;
padding: 6px 14px; border-radius: 30px;
background: rgba(255,255,255,0.95); border: 1px solid #e2e8f0;
font-size: 12px; font-weight: 700; color: #64748b;
backdrop-filter: blur(8px); box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: all 0.3s ease;
}
.session-timer-pill i { font-size: 16px; color: #f59e0b; }
.session-timer-pill.urgent { background: #fef2f2; border-color: #fecaca; color: #dc2626; }
.session-timer-pill.urgent i { color: #dc2626; }
/* Session Expired Overlay */
.session-expired-overlay {
display: none; position: fixed; inset: 0; z-index: 99999;
background: #f8f9fa;
flex-direction: column; align-items: center; justify-content: center;
text-align: center; padding: 40px 30px;
animation: fadeInOverlay 0.4s ease;
}
@keyframes fadeInOverlay { from { opacity: 0; } to { opacity: 1; } }
.expired-icon {
width: 80px; height: 80px; border-radius: 50%;
background: #fef2f2; border: 1px solid #fecaca;
display: flex; align-items: center; justify-content: center;
margin-bottom: 20px;
}
.expired-icon i { font-size: 40px; color: #ef4444; }
.session-expired-overlay h3 { font-size: 22px; font-weight: 800; color: #1e293b; margin-bottom: 8px; }
.session-expired-overlay p { font-size: 14px; color: #64748b; line-height: 1.6; max-width: 300px; }
</style>
</head>
<body>
<div class="container-scroller">
<div class="container-fluid page-body-wrapper full-page-wrapper">
<div class="content-wrapper d-flex align-items-center auth px-0" style="padding-top: 2rem; padding-bottom: 2rem; align-items: flex-start !important;">
<div class="row w-100 mx-0">
<div class="col-lg-4 mx-auto pe-0 ps-0">
<div class="auth-form-light text-left py-4 px-4 px-sm-5 bg-white" style="min-height: 10vh; padding-bottom: 80px !important;">
<div class="brand-logo mb-4 text-center">
<img src="images/ko.png" alt="logo" style="width: 140px;">
</div>
<h4 class="font-weight-bold mb-1" style="color: #1e293b;">Edit Profile</h4>
<p class="font-weight-light text-muted mb-4" style="font-size: 13px;">Update your personal identity data</p>
<div class="info-alert mb-4">
<div class="info-alert-icon"><i class="mdi mdi-information-variant"></i></div>
<div class="info-alert-text" style="flex:1; font-size:12px;">
<span>Phone Number (Telegram) cannot be changed on this page.</span>
</div>
</div>
<!-- STEP 1: Edit Data -->
<div id="step1-data">
<form class="pt-2" id="editForm">
<input type="hidden" name="action" value="update_data">
<div class="form-group mb-3">
<label class="font-weight-bold text-dark" style="font-size: 12px; margin-bottom: 6px;">Full Name</label>
<input type="text" class="form-control form-control-sm" name="name" id="name" value="<?= htmlspecialchars($occupant['name'] ?? '') ?>" required style="border-radius: 6px;">
</div>
<div class="form-group mb-3">
<label class="font-weight-bold text-dark" style="font-size: 12px; margin-bottom: 6px;">NIK / NIP</label>
<input type="text" class="form-control form-control-sm" name="nik_nip" id="nik_nip" value="<?= htmlspecialchars($occupant['nik_nip'] ?? '') ?>" style="border-radius: 6px;" required>
</div>
<div class="form-group mb-4">
<label class="font-weight-bold text-dark" style="font-size: 12px; margin-bottom: 6px;">Phone Number</label>
<input type="text" class="form-control form-control-sm bg-light" name="no_hp" id="no_hp" value="<?= htmlspecialchars($occupant['no_hp'] ?? '') ?>" readonly style="border-radius: 6px; cursor: not-allowed; color: #6c757d;">
</div>
<div class="mt-3">
<button type="button" id="btnLanjut" class="btn btn-block btn-primary btn-lg font-weight-medium auth-form-btn" style="border-radius: 8px; font-size: 14px; padding: 12px;">Next <i class="mdi mdi-arrow-right ml-1"></i></button>
</div>
</form>
</div>
<!-- STEP 2: Edit Foto -->
<div id="step2-foto" style="display: none;">
<div class="text-center mb-4">
<?php
$fotoSrc = (!empty($occupant['foto']) && file_exists($occupant['foto'])) ? $occupant['foto'] : 'images/faces/user.jpg';
?>
<div class="foto-wrapper" style="cursor: pointer; display: inline-block; position: relative;" onclick="startCamera()">
<img id="previewFoto" src="<?= htmlspecialchars($fotoSrc) ?>" class="foto-circle" style="width: 120px; height: 120px; border-radius: 50%; object-fit: cover; border: 3px solid #e0e7ff; box-shadow: 0 4px 10px rgba(0,0,0,0.05); transition: transform 0.2s;">
<div class="foto-badge" style="position: absolute; bottom: 5px; right: 5px; width: 32px; height: 32px; border-radius: 50%; background: #4747a1; display: flex; align-items: center; justify-content: center; border: 3px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.2);">
<i class="mdi mdi-camera text-white" style="font-size: 16px;"></i>
</div>
</div>
<p class="mt-3 text-muted" style="font-size:12px; font-weight:500;"><i class="mdi mdi-cursor-default-click text-primary mr-1"></i>Click the photo above to update</p>
</div>
<input type="hidden" name="foto_data" id="fotoData" value="" form="editForm">
<div class="d-flex justify-content-between mt-4">
<button type="button" id="btnBack" class="btn btn-light" style="border-radius: 8px; font-size: 14px; padding: 12px 20px;"><i class="mdi mdi-arrow-left mr-1"></i> Back</button>
<button type="button" id="btnSimpan" class="btn btn-primary" style="border-radius: 8px; font-size: 14px; padding: 12px 30px; flex: 1; margin-left: 10px;"><i class="mdi mdi-content-save mr-1"></i> Save Data</button>
</div>
</div>
<div id="step3-success" class="success-checkmark" style="display: none; padding-top: 30px;">
<div class="checkmark-circle text-center">
<div style="width: 80px; height: 80px; border-radius: 50%; background: #10b981; display: inline-flex; justify-content: center; align-items: center; margin-bottom: 20px; box-shadow: 0 10px 15px -3px rgba(16, 185, 129, 0.3);">
<i class="mdi mdi-check text-white" style="font-size: 40px; line-height: 1;"></i>
</div>
<h4 class="font-weight-bold text-success mb-2 text-center">Success!</h4>
<p class="text-muted text-center" style="font-size: 14px;">Your profile data has been updated.</p>
<hr class="my-4">
<p class="text-muted mb-4 text-center" style="font-size: 13px;">This page will automatically close in <span id="countdownText" class="font-weight-bold text-primary">3</span> seconds to keep your session secure.</p>
<button type="button" onclick="Telegram.WebApp.close(); window.close()" class="btn btn-block btn-outline-primary fw-bold" style="border-radius: 8px; padding: 12px;">Close Now</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- FULL CAMERA OVERLAY (registrasi_foto.php style) -->
<style>
.edit-cam-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; z-index:9999; background:#1e2223; flex-direction:column; font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif; }
.edit-cam-overlay * { box-sizing:border-box; }
.ecam-header { display:flex; justify-content:space-between; align-items:center; padding:20px; z-index:30; position:absolute; top:0; left:0; right:0; }
.ecam-area { flex:1; position:relative; overflow:hidden; background:#1e2223; }
.ecam-area video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }
.ecam-area canvas.ecam-overlay-cv { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }
.ecam-face-oval { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:10; }
.ecam-face-oval svg { width:100%; height:100%; }
#editBgRect { fill: #ffffff; }
.ecam-face-oval #editOvalRing { stroke:#e2e8f0; stroke-width:6; display:block; }
.ecam-face-oval #editOvalRingOut { stroke:none; display:block; }
.ecam-face-oval.detecting #editBgRect { animation: bankBgGlow 0.5s infinite; filter: brightness(1.2); }
.ecam-face-oval.success #editBgRect { fill: #00FF00; animation: none; filter: brightness(1.2); }
.ecam-face-oval.error #editBgRect { fill: #FF0000; animation: none; filter: brightness(1.2); }
@keyframes bankBgGlow {
0% { fill: #FF0000; }
25% { fill: #FFFF00; }
50% { fill: #00FF00; }
75% { fill: #00FFFF; }
100% { fill: #FF0000; }
}
.ecam-bar-after {
position:absolute; bottom:0; left:0; right:0; z-index:40;
padding:30px 0; background: rgba(30,34,35,0.95); backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);
display:flex; align-items:center; justify-content:center; gap:40px; border-top:1px solid rgba(226, 232, 240, 0.2);
border-top-left-radius:24px; border-top-right-radius:24px;
pointer-events:auto; display:none;
}
.ecam-act-btn {
background:none; border:none; display:flex; flex-direction:column; align-items:center; gap:8px; cursor:pointer;
}
.ecam-act-btn .ring {
width:64px; height:64px; border-radius:50%; display:flex; align-items:center; justify-content:center;
font-size:28px; color:#fff; transition:transform 0.2s;
}
.ecam-act-btn:active .ring { transform:scale(0.95); }
.ecam-act-btn.retry .ring { background:#f59e0b; box-shadow:0 8px 20px rgba(245, 158, 11, 0.3); }
.ecam-act-btn.save .ring { background:#10b981; box-shadow:0 8px 20px rgba(16, 185, 129, 0.3); }
.ecam-act-btn span { font-size:14px; font-weight:600; color:#fff; }
/* Hide elements */
.ecam-hud, .ecam-bm, .ecam-lm, .ecam-st-pill { display: none !important; }
</style>
<div id="cameraOverlay" class="edit-cam-overlay">
<div class="ecam-header">
<button type="button" onclick="stopEditCamera()" style="background:rgba(255,255,255,0.2); border:none; width:40px; height:40px; border-radius:50%; color:white; display:flex; align-items:center; justify-content:center; backdrop-filter:blur(4px);">
<i class="mdi mdi-close" style="font-size:24px;"></i>
</button>
<div style="color:white; font-weight:700; font-size:14px; text-shadow:0 1px 3px rgba(0,0,0,0.5);">Photo Registration</div>
<div style="width:40px;"></div>
</div>
<!-- Camera Area -->
<div class="ecam-area" id="editCamArea">
<video id="editVid" autoplay playsinline muted></video>
<canvas class="ecam-overlay-cv" id="editOvCanvas"></canvas>
<div class="ecam-face-oval" id="editOval">
<svg viewBox="0 0 400 800" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="editSilhouetteMask">
<rect width="100%" height="100%" fill="white"/>
<path d="M 200 200 C 110 200 70 270 70 360 C 40 350 30 460 70 450 C 90 550 140 580 200 580 C 260 580 310 550 330 450 C 370 460 360 350 330 360 C 330 270 290 200 200 200 Z" fill="black" />
</mask>
</defs>
<rect id="editBgRect" width="100%" height="100%" mask="url(#editSilhouetteMask)"/>
<path d="M 200 200 C 110 200 70 270 70 360 C 40 350 30 460 70 450 C 90 550 140 580 200 580 C 260 580 310 550 330 450 C 370 460 360 350 330 360 C 330 270 290 200 200 200 Z" fill="none" id="editOvalRingOut"/>
<path d="M 200 200 C 110 200 70 270 70 360 C 40 350 30 460 70 450 C 90 550 140 580 200 580 C 260 580 310 550 330 450 C 370 460 360 350 330 360 C 330 270 290 200 200 200 Z" fill="none" id="editOvalRing"/>
</svg>
</div>
<!-- Brightness Meter -->
<div class="ecam-bm" id="editBrightMeter" style="display:none;">
<i class="mdi mdi-white-balance-sunny" id="editBmIcon"></i>
<div class="ecam-meter-bar"><div class="ecam-meter-fill" id="editBmFill" style="height:50%;background:#f59e0b;"></div></div>
</div>
<!-- Liveness Meter -->
<div class="ecam-lm" id="editLiveMeter" style="display:none;">
<i class="mdi mdi-shield-check" id="editLmIcon"></i>
<div class="ecam-meter-bar"><div class="ecam-meter-fill" id="editLmFill" style="height:0%;background:#f59e0b;"></div></div>
</div>
<!-- Loading -->
<div class="ecam-loading" id="editCamLoading">
<div class="ecam-ldring"></div>
<p id="editLoadText">Loading camera & face detection models...</p>
<div class="ecam-load-progress"><div class="ecam-load-progress-fill" id="editLoadProgress"></div></div>
</div>
<!-- Status Pill -->
<div class="ecam-st-pill searching" id="editStPill" style="display:none;">
<i class="mdi mdi-face-recognition" id="editStIcon"></i>
<span id="editStText">Preparing...</span>
</div>
</div>
<!-- Preview -->
<div class="ecam-preview" id="editPreviewWrap">
<canvas id="editCapCanvas"></canvas>
<div class="ecam-preview-tag"><i class="mdi mdi-check-circle"></i> Photo Captured</div>
</div>
<!-- HUD: Shutter -->
<div class="ecam-hud">
<div style="margin-bottom:20px;">
<button class="ecam-shutter" id="editBtnShutter" disabled></button>
</div>
</div>
<!-- Bar After (retake/save) -->
<div class="ecam-bar-after" id="editBarAfter">
<button class="ecam-act-btn retry" id="editBtnRetake">
<div class="ring"><i class="mdi mdi-refresh"></i></div>
<span>Retake</span>
</button>
<button class="ecam-act-btn save" id="editBtnUseFoto">
<div class="ring"><i class="mdi mdi-check"></i></div>
<span>Save</span>
</button>
</div>
</div>
<!-- Custom Alert Container -->
<div id="customAlert" class="custom-alert warning" style="display: none;">
<i id="customAlertIcon" class="mdi mdi-alert-circle custom-alert-icon"></i>
<div class="custom-alert-content">
<div id="customAlertTitle" class="custom-alert-title">Notice</div>
<div id="customAlertMsg" class="custom-alert-msg">Warning message here.</div>
</div>
<i class="mdi mdi-close custom-alert-close" onclick="closeCustomAlert()"></i>
</div>
<!-- Session Timer Pill -->
<div class="session-timer-pill" id="sessionTimerPill">
<i class="mdi mdi-timer-outline"></i>
<span id="sessionTimerText">5:00</span>
</div>
<!-- Session Expired Overlay -->
<div class="session-expired-overlay" id="sessionExpiredOverlay">
<div class="expired-icon"><i class="mdi mdi-timer-off-outline"></i></div>
<h3>Session Expired</h3>
<p>Your 5-minute access window has ended. Please use the <b>/edit_data</b> command in Telegram to request a new session.</p>
</div>
<script src="vendors/js/vendor.bundle.base.js"></script>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<script>
// Session expiry timestamp from server
var SESSION_EXPIRES_AT = <?= (int)$tokenData['expires_at'] ?>;
window.Telegram.WebApp.ready();
window.Telegram.WebApp.expand();
function previewImage(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
document.getElementById('previewFoto').src = e.target.result;
}
reader.readAsDataURL(input.files[0]);
}
}
function showCustomAlert(title, message, type='warning') {
const alertBox = document.getElementById('customAlert');
const icon = document.getElementById('customAlertIcon');
alertBox.className = 'custom-alert ' + type;
if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';
else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';
else icon.className = 'mdi mdi-alert-circle custom-alert-icon';
document.getElementById('customAlertTitle').innerText = title;
document.getElementById('customAlertMsg').innerText = message;
alertBox.style.display = 'flex';
alertBox.style.animation = 'slideIn 0.3s ease-out';
setTimeout(() => closeCustomAlert(), 5000);
}
function closeCustomAlert() {
const alertBox = document.getElementById('customAlert');
alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';
setTimeout(() => { alertBox.style.display = 'none'; }, 300);
}
// Navigation Handlers
const infoAlertText = document.querySelector('.info-alert-text');
const originalInfoText = infoAlertText.innerHTML;
document.getElementById('btnLanjut').addEventListener('click', function() {
const name = document.getElementById('name').value.trim();
if(name === '') {
showCustomAlert('Incomplete Form', 'Full Name is required.', 'warning');
return;
}
document.getElementById('step1-data').style.display = 'none';
document.getElementById('step2-foto').style.display = 'block';
infoAlertText.innerHTML = 'Click the photo below to update it. If you do <b>not</b> want to update the photo, click <b>Save Data</b>';
});
document.getElementById('btnBack').addEventListener('click', function() {
document.getElementById('step2-foto').style.display = 'none';
document.getElementById('step1-data').style.display = 'block';
infoAlertText.innerHTML = originalInfoText;
});
</script>
<script src="js/face-api/face-api.min.js"></script>
<script>
// ── Full Camera System (registrasi_foto.php style) ──
let editStream = null, editDetectLoop = null, editFaceOk = false, editDataURL = null;
let editBrightInterval = null, editLivenessScore = 0;
let editPrevLandmarks = null, editLivenessFrames = 0;
let editModelsLoaded = false;
const editVid = document.getElementById('editVid');
const editOvCanvas = document.getElementById('editOvCanvas');
const editOval = document.getElementById('editOval');
const editCamLoading = document.getElementById('editCamLoading');
const editStPill = document.getElementById('editStPill');
const editStIcon = document.getElementById('editStIcon');
const editStText = document.getElementById('editStText');
const editBtnShutter = document.getElementById('editBtnShutter');
const editCamArea = document.getElementById('editCamArea');
const editPreviewWrap = document.getElementById('editPreviewWrap');
const editCapCanvas = document.getElementById('editCapCanvas');
const editBarAfter = document.getElementById('editBarAfter');
const editBmFill = document.getElementById('editBmFill');
const editBmIcon = document.getElementById('editBmIcon');
const editLmFill = document.getElementById('editLmFill');
const editLmIcon = document.getElementById('editLmIcon');
const editLoadProgress = document.getElementById('editLoadProgress');
const editLoadText = document.getElementById('editLoadText');
function editSetProgress(pct) { editLoadProgress.style.width = Math.min(100, pct) + '%'; }
function editSetStatus(type, txt) {
editStPill.className = 'ecam-st-pill';
if (type === 'search') { editStPill.classList.add('searching'); editStIcon.className = 'mdi mdi-face-recognition'; }
if (type === 'ok') { editStPill.classList.add('found'); editStIcon.className = 'mdi mdi-check-circle'; }
if (type === 'err') { editStPill.classList.add('warn'); editStIcon.className = 'mdi mdi-alert-circle'; }
editStText.textContent = txt;
}
function startCamera() {
document.getElementById('cameraOverlay').style.display = 'flex';
editCamLoading.style.display = 'flex';
editLoadText.textContent = 'Loading camera & face detection models...';
editSetProgress(5);
editCamArea.style.display = 'block';
editPreviewWrap.style.display = 'none';
editBarAfter.style.display = 'none';
document.querySelector('.ecam-hud').style.display = 'flex';
editFaceOk = false;
editDataURL = null;
editLivenessScore = 0;
editLivenessFrames = 0;
editPrevLandmarks = null;
editLmFill.style.height = '0%';
initEditCamera();
}
async function initEditCamera() {
try {
if (!editModelsLoaded) {
editSetProgress(20);
editLoadText.textContent = 'Loading face detection models...';
const loadModelsPromise = Promise.all([
faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models'),
faceapi.nets.faceLandmark68Net.loadFromUri('js/face-api/models')
]);
await Promise.race([
loadModelsPromise,
new Promise((_, reject) => setTimeout(() => reject(new Error("Model loading timed out after 15 seconds. Make sure your internet connection is stable.")), 15000))
]);
editModelsLoaded = true;
}
editSetProgress(50);
editLoadText.textContent = 'Starting camera...';
// Force Standard Definition (SD) settings for camera to improve performance
const videoConstraints = {
facingMode: 'user',
width: { ideal: 480 }
};
editStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints, audio: false });
editVid.srcObject = editStream;
await new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => reject(new Error("Camera stream initialized but video playback timed out. Device may not support video playback.")), 15000);
editVid.onloadedmetadata = () => {
clearTimeout(timeoutId);
editVid.play().then(resolve).catch((e) => reject(new Error("Failed to play video stream: " + e.message)));
};
});
editSetProgress(100);
setTimeout(() => {
editCamLoading.style.display = 'none';
editStPill.style.display = 'flex';
document.getElementById('editBrightMeter').style.display = 'flex';
document.getElementById('editLiveMeter').style.display = 'flex';
editSetStatus('search', 'Position your face...');
startEditDetection();
startEditBrightness();
}, 400);
} catch(err) {
let msg = err.message;
if (err.name === 'NotAllowedError') msg = 'Camera access denied by browser.';
else if (err.name === 'NotFoundError') msg = 'Camera not found.';
stopEditCamera();
showCustomAlert('Camera Access Failed', msg, 'error');
}
}
let isDetecting = false;
let faceState = 'idle';
let scanStartTime = 0;
function startEditDetection() {
const opts = new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.15 });
editDetectLoop = setInterval(async () => {
if (editVid.paused || editVid.ended || !editVid.videoWidth || isDetecting) return;
isDetecting = true;
editOvCanvas.width = editVid.videoWidth;
editOvCanvas.height = editVid.videoHeight;
try {
const results = await faceapi.detectAllFaces(editVid, opts).withFaceLandmarks().run();
if (results.length === 1) {
const det = results[0].detection;
const box = det.box;
const landmarks = results[0].landmarks;
if (faceState === 'idle') {
faceState = 'scanning';
scanStartTime = Date.now();
editOval.className = 'ecam-face-oval detecting';
editLivenessFrames = 0; editLivenessScore = 0;
if (landmarks) editPrevLandmarks = landmarks.positions.map(p => ({x: p.x, y: p.y}));
} else if (faceState === 'scanning') {
if (landmarks) {
const pts = landmarks.positions;
if (editPrevLandmarks && pts.length === editPrevLandmarks.length) {
let totalDiff = 0;
for (let i = 0; i < pts.length; i++) {
totalDiff += Math.abs(pts[i].x - editPrevLandmarks[i].x) + Math.abs(pts[i].y - editPrevLandmarks[i].y);
}
const avgDiff = totalDiff / pts.length;
if (avgDiff > 0.05 && avgDiff < 15) {
editLivenessFrames = Math.min(editLivenessFrames + 1, 20);
} else if (avgDiff >= 15) {
editLivenessFrames = Math.max(0, editLivenessFrames - 2);
} else {
editLivenessFrames = Math.min(editLivenessFrames + 0.5, 20);
}
}
editPrevLandmarks = pts.map(p => ({x: p.x, y: p.y}));
editLivenessScore = Math.min(100, (editLivenessFrames / 12) * 100);
}
if (Date.now() - scanStartTime > 1500) {
faceState = 'showing_result';
if (editLivenessScore >= 15) {
editOval.className = 'ecam-face-oval success';
clearInterval(editDetectLoop);
clearInterval(editBrightInterval);
// Countdown 3...2...1
let count = 3;
const cdText = document.createElement('div');
cdText.style = 'position:absolute; top:40%; left:50%; transform:translate(-50%, -50%); font-size:120px; font-weight:900; color:#10b981; z-index:999; text-shadow:0 4px 20px rgba(0,0,0,0.5);';
cdText.innerText = count;
editCamArea.appendChild(cdText);
const cdInt = setInterval(() => {
count--;
if (count > 0) {
cdText.innerText = count;
} else {
clearInterval(cdInt);
cdText.remove();
editFaceOk = true;
editCapture();
}
}, 1000);
} else {
editOval.className = 'ecam-face-oval error';
setTimeout(() => {
if (faceState === 'showing_result') {
faceState = 'idle';
editOval.className = 'ecam-face-oval';
editLivenessFrames = 0;
editLivenessScore = 0;
}
}, 1500);
}
}
}
} else {
if (faceState !== 'showing_result') {
faceState = 'idle';
editOval.className = 'ecam-face-oval';
editLivenessFrames = 0; editLivenessScore = 0;
}
}
} catch (e) {
console.error("Detection error:", e);
} finally {
isDetecting = false;
}
}, 400);
}
function startEditBrightness() {
const tmpCanvas = document.createElement('canvas');
const tmpCtx = tmpCanvas.getContext('2d');
editBrightInterval = setInterval(() => {
if (editVid.paused || editVid.ended || !editVid.videoWidth) return;
tmpCanvas.width = 80; tmpCanvas.height = 60;
tmpCtx.drawImage(editVid, 0, 0, 80, 60);
const data = tmpCtx.getImageData(0, 0, 80, 60).data;
let sum = 0; const count = data.length / 4;
for (let i = 0; i < data.length; i += 4) sum += (data[i] * 0.299 + data[i+1] * 0.587 + data[i+2] * 0.114);
const pct = Math.min(100, Math.max(5, (sum / count / 255) * 100));
editBmFill.style.height = pct + '%';
if (pct < 30) { editBmFill.style.background = '#ef4444'; editBmIcon.style.color = '#ef4444'; }
else if (pct < 55) { editBmFill.style.background = '#f59e0b'; editBmIcon.style.color = '#f59e0b'; }
else { editBmFill.style.background = '#22c55e'; editBmIcon.style.color = '#22c55e'; }
}, 800);
}
function editCapture() {
if (!editFaceOk) return;
clearInterval(editDetectLoop);
clearInterval(editBrightInterval);
const vw = editVid.videoWidth, vh = editVid.videoHeight;
editCapCanvas.width = vw; editCapCanvas.height = vh;
const ctx = editCapCanvas.getContext('2d');
ctx.save(); ctx.translate(vw, 0); ctx.scale(-1, 1);
ctx.drawImage(editVid, 0, 0, vw, vh);
ctx.restore();
editDataURL = editCapCanvas.toDataURL('image/jpeg', 0.8);
if (editStream) editStream.getTracks().forEach(t => t.stop());
editCamArea.style.display = 'none';
editPreviewWrap.style.display = 'block';
document.querySelector('.ecam-hud').style.display = 'none';
editBarAfter.style.display = 'flex';
}
function editRetake() {
editDataURL = null;
editLivenessScore = 0; editLivenessFrames = 0; editPrevLandmarks = null;
editLmFill.style.height = '0%';
editPreviewWrap.style.display = 'none';
editCamArea.style.display = 'block';
editBarAfter.style.display = 'none';
document.querySelector('.ecam-hud').style.display = 'flex';
editCamLoading.style.display = 'flex';
editLoadText.textContent = 'Reloading camera...';
editSetProgress(50);
faceState = 'idle';
editOval.className = 'ecam-face-oval';
editFaceOk = false;
initEditCamera();
}
function editUseFoto() {
if (!editDataURL) return;
document.getElementById('previewFoto').src = editDataURL;
document.getElementById('fotoData').value = editDataURL;
stopEditCamera();
showCustomAlert('Success', 'Photo captured successfully. Click Save Data to save.', 'success');
}
function stopEditCamera() {
document.getElementById('cameraOverlay').style.display = 'none';
clearInterval(editDetectLoop);
clearInterval(editBrightInterval);
if (editStream) { editStream.getTracks().forEach(t => t.stop()); editStream = null; }
}
editBtnShutter.addEventListener('click', editCapture);
document.getElementById('editBtnRetake').addEventListener('click', editRetake);
document.getElementById('editBtnUseFoto').addEventListener('click', editUseFoto);
document.getElementById('btnSimpan').addEventListener('click', function() {
// Validate step 1 data again just in case
const name = document.getElementById('name').value.trim();
if(name === '') {
showCustomAlert('Incomplete Form', 'Full Name is required.', 'warning');
document.getElementById('step2-foto').style.display = 'none';
document.getElementById('step1-data').style.display = 'block';
return;
}
const btn = document.getElementById('btnSimpan');
const originalText = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm mr-2" role="status" aria-hidden="true"></span>Saving...';
btn.disabled = true;
document.getElementById('btnBack').disabled = true;
const form = document.getElementById('editForm');
const formData = new FormData(form);
fetch('', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Success
document.getElementById('step2-foto').style.display = 'none';
document.getElementById('step3-success').style.display = 'block';
let countdown = 3;
const cText = document.getElementById('countdownText');
if(cText) cText.innerText = countdown;
setInterval(() => {
countdown--;
if(cText) cText.innerText = countdown;
if(countdown <= 0) {
if (window.Telegram && window.Telegram.WebApp) {
window.Telegram.WebApp.close();
}
window.close();
}
}, 1000);
} else {
showCustomAlert('Failed', data.message, 'error');
btn.innerHTML = originalText;
btn.disabled = false;
document.getElementById('btnBack').disabled = false;
}
})
.catch(error => {
showCustomAlert('Network Error', 'A connection error occurred.', 'error');
btn.innerHTML = originalText;
btn.disabled = false;
document.getElementById('btnBack').disabled = false;
});
});
// ─── SESSION EXPIRY TIMER ───
(function() {
var timerPill = document.getElementById('sessionTimerPill');
var timerText = document.getElementById('sessionTimerText');
var expiredOverlay = document.getElementById('sessionExpiredOverlay');
var sessionCompleted = false;
function updateSessionTimer() {
if (sessionCompleted) return;
var now = Math.floor(Date.now() / 1000);
var remaining = SESSION_EXPIRES_AT - now;
if (remaining <= 0) {
// Session expired - stop camera if active, hide ALL content
timerPill.style.display = 'none';
// Stop camera if it's open
if (typeof stopEditCamera === 'function') stopEditCamera();
// Hide all main content
var mainContent = document.querySelector('.auth-form-light');
if (mainContent) mainContent.style.display = 'none';
var cameraOverlay = document.getElementById('cameraOverlay');
if (cameraOverlay) cameraOverlay.style.display = 'none';
// Show expired overlay
expiredOverlay.style.display = 'flex';
// Auto-close after 3 seconds
setTimeout(function() {
if (window.Telegram && window.Telegram.WebApp) {
window.Telegram.WebApp.close();
}
window.close();
}, 3000);
return;
}
var mins = Math.floor(remaining / 60);
var secs = remaining % 60;
timerText.textContent = mins + ':' + (secs < 10 ? '0' : '') + secs;
if (remaining <= 60) {
timerPill.classList.add('urgent');
} else {
timerPill.classList.remove('urgent');
}
setTimeout(updateSessionTimer, 1000);
}
// Stop timer if user saves data successfully
var origStep3 = document.getElementById('step3-success');
if (origStep3) {
var observer = new MutationObserver(function() {
if (origStep3.style.display !== 'none') {
sessionCompleted = true;
timerPill.style.display = 'none';
}
});
observer.observe(origStep3, { attributes: true, attributeFilter: ['style'] });
}
updateSessionTimer();
})();
</script>
</body>
</html>