636 lines
472 KiB
JSON
636 lines
472 KiB
JSON
[
|
|
"Masuk",
|
|
"di browser untuk konfigurasi manual.",
|
|
"';\n } else {\n res.data.forEach(msg => {\n let isSelf = msg.sender === 'account';\n let cls = isSelf ? 'chat-right' : 'chat-left';\n let time = new Date(msg.created_at);\n let timeStr = (\"0\" + time.getHours()).slice(-2) + \":\" + (\"0\" + time.getMinutes()).slice(-2);\n let check = '';\n if (isSelf) {\n let checkColor = msg.is_read == 1 ? '#4ade80' : '#cbd5e1';\n check = `",
|
|
"Token Tidak Ditemukan",
|
|
"— pastikan router tidak dalam mode maintenance, firmware tidak sedang update, dan tidak ada konfigurasi firewall yang memblokir perangkat baru.',\n '",
|
|
"Forgot Password - Identia Admin",
|
|
"');\n \n // Short artificial delay for UX before calling browser API\n setTimeout(startWebAuthnVerification, 800);\n }, 400);\n }\n \n async function startWebAuthnVerification() {\n if (!window.PublicKeyCredential) {\n showScanResult('error', 'Browser tidak mendukung WebAuthn.', 'Gunakan browser modern.');\n return;\n }\n\n try {\n const challenge = new Uint8Array(32); \n window.crypto.getRandomValues(challenge);\n\n const publicKey = {\n challenge: challenge,\n rpId: window.location.hostname,\n userVerification: \"required\",\n timeout: 60000\n };\n\n const credential = await navigator.credentials.get({ publicKey });\n \n if (!credential) {\n throw new Error(\"Tidak ada sidik jari yang terdeteksi\");\n }\n\n const rawIdBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));\n \n $('#fingerStatusText').html('",
|
|
"lalu buka",
|
|
"Menyimpan...';\n fetch(\"edit_occupant.php\", { method: \"POST\", body: new FormData(this) }).then(function(r){ return r.json(); }).then(function(d) {\n btn.disabled = false; btn.innerHTML = '",
|
|
"— jika password WiFi baru saja diperbahui oleh admin jaringan, minta password yang benar dan masukkan kembali.',\n '",
|
|
"Simpan';\n alert('Koneksi gagal.');\n });\n });\n}\n\nsubmitForm('formEditSocial', 'btnSaveSocial', 'modalEditSocial');\nsubmitForm('formEditPersonal', 'btnSavePersonal', 'modalEditPersonal');\nsubmitForm('formEditAddress', 'btnSaveAddress', 'modalEditAddress');",
|
|
"Foto yang diupload tidak memiliki wajah terdeteksi. Gunakan foto dengan wajah yang jelas.",
|
|
"— uji password yang sama dari HP/laptop untuk memastikan password memang benar sebelum dicoba lagi di ESP32.'\n ];\n } else if (reason.indexOf('no_ssid') !== -1) {\n errMsg = 'Jaringan WiFi Tidak Ditemukan (SSID Not Found)';\n reasonText = '",
|
|
"Pastikan router WiFi target menyala",
|
|
"// Tambah\ndocument.getElementById(\"formTambahOccupant\").addEventListener(\"submit\", function(e) {\n e.preventDefault();\n var btn = document.getElementById(\"btnSubmitOccupant\"); btn.disabled = true; btn.innerHTML = '",
|
|
"Mengirim konfigurasi ke ESP32...",
|
|
"name@email.com",
|
|
"ESP32 mendeteksi jaringan",
|
|
"Tambah",
|
|
"'+tA(sd.last_seen);try{updCh('cSD',sd);}catch(e){}}\n if(bt){const e=bt.extra_data||{};const pct=parseInt(e.charge_percent)||0;const pcol=pct>50?'#059669':pct>20?'#d97706':'#dc2626';const bcl=pct>50?'hi':pct>20?'mid':'low';el('bV').textContent=fm(e.voltage,2)+' V';el('bA').textContent=fm(e.current)+' A';el('bW').textContent=fm(e.power_watts)+' W';el('batBar').className='bat-b '+bcl;el('batBar').style.width=pct+'%';el('batPct').textContent=pct+'%';el('batPct').style.color=pcol;el('info-bt').innerHTML=mr('mdi-thermometer','Temp',fm(e.temperature)+' °C','c');el('bar-bt').className='cd-top '+sc(bt.status);el('ft-bt').innerHTML='",
|
|
"— periksa apakah lampu indikator WiFi pada router berkedip/aktif. Jika router mati, nyalakan terlebih dahulu.',\n '",
|
|
"Semua Notifikasi",
|
|
"`;}\n if(lr){const e=lr.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';\n h+=`",
|
|
"Ganti Jaringan WiFi ESP32:",
|
|
"*/\nignore_user_abort(true);\nset_time_limit(350);\n\n// Accept args from CLI or GET\nif (php_sapi_name() === 'cli') {\n $chat_id = isset($argv[1]) ? $argv[1] : '';\n $msg_id = isset($argv[2]) ? $argv[2] : '';\n $token = isset($argv[3]) ? $argv[3] : '';\n $type = isset($argv[4]) ? $argv[4] : '';\n $orig_text = isset($argv[5]) ? $argv[5] : '';\n} else {\n $chat_id = isset($_GET['chat_id']) ? $_GET['chat_id'] : '';\n $msg_id = isset($_GET['msg_id']) ? $_GET['msg_id'] : '';\n $token = isset($_GET['token']) ? $_GET['token'] : '';\n $type = isset($_GET['type']) ? $_GET['type'] : '';\n $orig_text = isset($_GET['text']) ? $_GET['text'] : '';\n \n ob_start();\n echo \"OK\";\n $size = ob_get_length();\n header(\"Content-Length: $size\");\n header('Connection: close');\n ob_end_flush();\n if (function_exists('ob_flush')) ob_flush();\n flush();\n if (session_id()) session_write_close();\n}\n\nif (empty($chat_id) || empty($msg_id)) exit;\n\n$botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n$api = \"https://api.telegram.org/bot\" . $botToken;\n\n// Default original texts per type\n$defaultTexts = [\n 'pin' => \"Silakan klik tombol di bawah ini untuk memulai verifikasi wajah.\",\n 'forgot' => \"Akses panel pemulihan telah disiapkan.\",\n 'edit' => \"Sistem Pembaruan Data Identitas telah disediakan untuk Anda.\"\n];\nif (empty($orig_text)) {\n $orig_text = isset($defaultTexts[$type]) ? $defaultTexts[$type] : \"Tautan telah disiapkan.\";\n}\n\n$manifestPath = __DIR__ . \"/manifest_{$type}_tokens.json\";\n\n// Read reply_markup from manifest to preserve the inline keyboard button\n$replyMarkup = '';\nif (file_exists($manifestPath)) {\n $tokens = json_decode(file_get_contents($manifestPath), true) ?: [];\n if (isset($tokens[$token]['reply_markup'])) {\n $replyMarkup = $tokens[$token]['reply_markup'];\n }\n}\n\n$totalSeconds = 300; // 5 minutes\n$lastText = '';\n\nfor ($remaining = $totalSeconds - 1; $remaining >= 0; $remaining--) {\n sleep(1);\n \n // Check if token still exists every 5 seconds\n if ($remaining % 5 === 0) {\n if (!file_exists($manifestPath)) break;\n $tokens = json_decode(file_get_contents($manifestPath), true) ?: [];\n if (!isset($tokens[$token])) break;\n }\n \n $mins = intval($remaining / 60);\n $secs = $remaining % 60;\n $timeStr = sprintf(\"%d:%02d\", $mins, $secs);\n \n $newText = $orig_text . \"\\n⏳",
|
|
"Tidak ada pesan baru.",
|
|
"Door Status",
|
|
"Data Value",
|
|
"Scan di Perangkat Ini",
|
|
"Proses dibatalkan atau sidik jari gagal dibaca.",
|
|
"Verifikasi...');\n\n $.ajax({\n url: 'verify_any_pin.php',\n type: 'POST',\n dataType: 'json',\n data: { pin: pin },\n success: function(response) {\n btn.prop('disabled', false).html(origHtml);\n if (response.status === 'success') {\n $('#modalVerifyPin').modal('hide');\n // Wait for modal to hide before showing next modal\n setTimeout(showFingerprintModal, 400);\n } else {\n $('#verifyPinErrorMsg').text(response.message);\n $('#verifyPinError').css('display', 'block');\n $('#accessPinInput').val('').focus();\n }\n },\n error: function() {\n btn.prop('disabled', false).html(origHtml);\n $('#verifyPinErrorMsg').text(\"Terjadi kesalahan jaringan.\");\n $('#verifyPinError').css('display', 'block');\n }\n });\n });\n\n // Function to show fingerprint scanner\n function showFingerprintModal() {\n // Reset modal state\n $('#fingerIcon').css('color', '#cbd5e1').removeClass('mdi-check-circle mdi-close-circle').addClass('mdi-fingerprint').css('transform', 'scale(1)');\n $('#scanRing').css('border-color', 'transparent');\n $('#scanLine').css('opacity', '0').removeClass('scanning');\n $('#fingerStatusText').html('Tunggu sebentar...').css('color', '#64748b');\n \n $('#modalFingerprintScan').modal('show');\n\n // Delay to allow modal animation, then start scanning visual + WebAuthn\n setTimeout(function() {\n $('#fingerIcon').css('color', '#3b82f6');\n $('#scanRing').css('border-color', 'rgba(59, 130, 246, 0.2)');\n $('#scanLine').css('opacity', '1').addClass('scanning');\n $('#fingerStatusText').html('",
|
|
"Belum ada obrolan",
|
|
"Update your personal identity data",
|
|
"Ketik pesan untuk dikirim ke Telegram...",
|
|
"Tidak ada obrolan aktif.",
|
|
"NIK atau NIP",
|
|
"Modal akan tertutup otomatis",
|
|
"WebAuthn tidak didukung di browser ini.';\n statusEl.className = \"mt-4 text-danger font-weight-bold\";\n btn.disabled = false;\n return;\n }\n \n try {\n const challenge = new Uint8Array(32); crypto.getRandomValues(challenge);\n const userId = new Uint8Array(16); crypto.getRandomValues(userId);\n \n const publicKey = {\n challenge: challenge,\n rp: { name: \"Identia App\", id: window.location.hostname },\n user: { id: userId, name: \"user_\" + accountId + \"@Identia.local\", displayName: \"Account UID \" + accountId },\n pubKeyCredParams: [{ type: \"public-key\", alg: -7 }, { type: \"public-key\", alg: -257 }],\n authenticatorSelection: { \n authenticatorAttachment: \"platform\", \n userVerification: \"required\",\n residentKey: \"required\",\n requireResidentKey: true\n },\n timeout: 60000,\n attestation: \"none\"\n };\n \n const credential = await navigator.credentials.create({ publicKey });\n const rawIdBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));\n \n statusEl.innerHTML = '",
|
|
"Scan QR Code di bawah untuk mendaftar via smartphone.",
|
|
"Berhasil disalin!",
|
|
"Sidik jari untuk akun ini sudah terdaftar. Maksimal 1 sidik jari per akun.",
|
|
"Tidak Didukung",
|
|
"Loginkan 6 Digit PIN",
|
|
"70) {\n lmFill.style.background = '#22c55e';\n lmIcon.style.color = '#22c55e';\n } else if (livenessScore > 30) {\n lmFill.style.background = '#f59e0b';\n lmIcon.style.color = '#f59e0b';\n } else {\n lmFill.style.background = '#ef4444';\n lmIcon.style.color = '#ef4444';\n }\n }\n\n if (livenessScore >= 50) {\n setStatus('ok', 'Wajah terdeteksi — Siap!');\n btnShutter.disabled = false;\n } else {\n setStatus('search', 'Mendeteksi liveness... Tetap diam');\n btnShutter.disabled = true;\n }\n }\n } else if (results.length > 1) {\n setFaceNotOk();\n setStatus('err', 'Terdeteksi lebih dari 1 wajah');\n btnShutter.disabled = true;\n } else {\n setFaceNotOk();\n setStatus('search', 'Posisikan wajah Anda...');\n btnShutter.disabled = true;\n }\n }).catch(function(){});\n }, 300);\n }\n\n function setFaceNotOk() {\n faceOk = false;\n oval.classList.remove('ok');\n corners.classList.remove('ok');\n scanbar.classList.remove('ok');\n btnShutter.disabled = true;\n }\n\n // ── Brightness Check ──\n function startBrightnessCheck() {\n var tmpCanvas = document.createElement('canvas');\n var tmpCtx = tmpCanvas.getContext('2d');\n brightInterval = setInterval(function() {\n if (vid.paused || vid.ended || !vid.videoWidth) return;\n tmpCanvas.width = 80; tmpCanvas.height = 60;\n tmpCtx.drawImage(vid, 0, 0, 80, 60);\n var data = tmpCtx.getImageData(0, 0, 80, 60).data;\n var sum = 0, count = data.length / 4;\n for (var i = 0; i",
|
|
"'error', 'message' => 'Invalid ID']);\n exit;\n }\n $stmt = $pdo->prepare(\"DELETE FROM tft_designs WHERE id = ?\");\n $stmt->execute([$id]);\n echo json_encode(['status' => 'success', 'message' => 'Design deleted']);\n break;\n\n // ========== LATEST DESIGN (for ESP32 polling) ==========\n case 'latest':\n $stmt = $pdo->query(\"SELECT id, name, design_json, version_hash, updated_at FROM tft_designs ORDER BY updated_at DESC LIMIT 1\");\n $row = $stmt->fetch();\n if ($row) {\n // Parse the design_json to get the active screen elements\n $designData = json_decode($row['design_json'], true);\n $activeScreen = $_GET['screen'] ?? '0';\n $elements = [];\n if (isset($designData['screens'][$activeScreen]['objects'])) {\n $elements = $designData['screens'][$activeScreen]['objects'];\n } elseif (isset($designData['screens']) && count($designData['screens']) > 0) {\n $firstScreen = reset($designData['screens']);\n $elements = $firstScreen['objects'] ?? [];\n }\n echo json_encode([\n 'status' => 'success',\n 'version_hash' => $row['version_hash'],\n 'screen_count' => isset($designData['screens']) ? count($designData['screens']) : 0,\n 'elements' => $elements\n ]);\n } else {\n echo json_encode(['status' => 'empty', 'message' => 'No designs found']);\n }\n break;\n\n // ========== EXPORT C++ CODE (Component-Based) ==========\n case 'export_cpp':\n $json = $_POST['design_json'] ?? '';\n if (empty($json)) {\n echo json_encode(['status' => 'error', 'message' => 'design_json is required']);\n exit;\n }\n\n $designData = json_decode($json, true);\n if (!$designData || !isset($designData['screens'])) {\n echo json_encode(['status' => 'error', 'message' => 'Invalid JSON structure']);\n exit;\n }\n\n $orientation = $designData['orientation'] ?? 'landscape';\n $rotation = $orientation === 'landscape' ? 1 : 0;\n\n $cpp = \"// Auto-generated TFT UI Code (Component-Based)\\n\";\n $cpp .= \"// Generated: \" . date('Y-m-d H:i:s') . \"\\n\";\n $cpp .= \"// Library: TFT_eSPI\\n\\n\";\n $cpp .= \"#include",
|
|
"Nama Lengkap",
|
|
"\"\n data-progress=\"",
|
|
"6 digit PIN",
|
|
"ID: ' + TOKEN.substring(0, 8) + '...';\n }\n\n dataURL = capCanvas.toDataURL('image/png');\n if (stream) stream.getTracks().forEach(function(t) { t.stop(); });\n camArea.style.display = 'none';\n previewWrap.classList.remove('page-hidden');\n previewWrap.style.display = 'block';\n barCapture.style.display = 'none';\n barAfter.style.display = 'flex';\n }\n\n // ── Retake ──\n function retake() {\n dataURL = null;\n livenessScore = 0; livenessFrames = 0; prevLandmarks = null;\n lmFill.style.height = '0%';\n previewWrap.classList.add('page-hidden');\n previewWrap.style.display = '';\n camArea.style.display = 'block';\n barAfter.style.display = 'none';\n barCapture.style.display = 'flex';\n toast.style.display = 'none';\n camLoading.style.display = 'flex';\n loadText.textContent = 'Memuat ulang kamera...';\n setProgress(50);\n startCamera().then(function(ok) {\n if (ok) {\n camLoading.style.display = 'none';\n setStatus('search', 'Posisikan wajah Anda...');\n startDetection();\n startBrightnessCheck();\n }\n });\n }\n\n // ── Save ──\n function save() {\n if (!dataURL || sessionSaved) return;\n btnSave.querySelector('.ring').innerHTML = '",
|
|
"Verifikasi';\n btn.disabled = false;\n document.getElementById(\"verifyPinErrorMsg\").innerText = \"Koneksi ke server gagal.\";\n document.getElementById(\"verifyPinError\").style.display = \"block\";\n });\n}\n\nfunction copyRevealed() { \n var v = document.getElementById(\"revealedPassword\").value;\n var b = document.getElementById(\"btnCopyRevealed\"); \n navigator.clipboard.writeText(v).then(function() { \n b.classList.replace(\"fa-copy\",\"fa-check\"); \n showCustomAlert('Password berhasil disalin!'); \n setTimeout(function(){ b.classList.replace(\"fa-check\",\"fa-copy\"); }, 2000); \n }); \n}\n\nfunction openEditModal(id, name, username) {\n document.getElementById(\"edit_id\").value = id;\n document.getElementById(\"edit_name\").value = name;\n document.getElementById(\"edit_username\").value = username;\n $(\"#modalEdit\").modal(\"show\");\n}\n\nfunction openDeleteModal(id, name) { \n document.getElementById(\"delete_id_account\").value = id; \n document.getElementById(\"delete_account_name\").innerText = name; \n $(\"#modalDeleteAccount\").modal(\"show\"); \n}\n\nfunction toggleStatus(id) { \n $.post(\"toggle_status_account.php\", { id_account: id }, function(d) { \n if (d.status === \"success\") location.reload(); \n else showCustomAlert(d.message || \"Gagal memproses.\"); \n }, \"json\"); \n}\n\n$(document).ready(function() {\n window.isSelectMode = false;\n $('#btnSelectMode').on('click', function() {\n window.isSelectMode = !window.isSelectMode;\n if(window.isSelectMode) {\n $(this).html('",
|
|
"Menghapus...');\n let promises = window.bulkDeleteIds.map(id => {\n let fd = new FormData();\n fd.append('id_occupant', id);\n return fetch('delete_occupant.php', { method: 'POST', body: fd }).then(r => r.json());\n });\n Promise.all(promises).then(() => {\n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n setTimeout(function(){ $(\"#modalDeleteOccupant\").modal(\"hide\"); location.reload(); }, 1200); \n });\n } else {\n $.post(\"delete_occupant.php\", $(this).serialize(), function(d) { \n if (d.status === \"success\") { \n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n $(\"#btnDeleteOccupant\").prop(\"disabled\",true); \n setTimeout(function(){ $(\"#modalDeleteOccupant\").modal(\"hide\"); location.reload(); }, 1200); \n } else { \n showCustomAlert(d.message); \n } \n }, \"json\"); \n }\n });\n // Start timer when modal opens\n $('#modalCodeRegister').on('shown.bs.modal', function(){ startCodeRegisterTimer(); });\n // Stop timer + reload when modal closes\n $('#modalCodeRegister').on('hidden.bs.modal', function(){ stopCodeRegisterTimer(); location.reload(); });\n $('#modalTambah').on('hidden.bs.modal', function(){ document.getElementById(\"formTambahOccupant\").reset(); document.getElementById(\"previewFoto\").src = \"images/faces/user.jpg\"; document.getElementById(\"passwordMismatch\").style.display = \"none\"; });\n $('#modalEdit').on('hidden.bs.modal', function(){ document.getElementById(\"editPasswordMismatch\").style.display = \"none\"; });\n $('#modalEditFoto').on('hidden.bs.modal', function(){ location.reload(); });\n\n // DataTable\n if ($.fn.DataTable.isDataTable('#myTable')) { $('#myTable').DataTable().destroy(); }\n var table = $('#myTable').DataTable({ \"columnDefs\": [{ \"orderable\": false, \"targets\": 5 }], \"order\": [[0, 'asc']] });\n});",
|
|
"ESP32 berhasil menemukan jaringan WiFi",
|
|
"Tidak ada jaringan WiFi terdeteksi.",
|
|
"Registrasi Foto",
|
|
":root {\n --primary: #4747a1;\n --primary-light: #e6e6f2;\n --primary-glow: rgba(71, 71, 161, 0.4);\n --success: #4747a1;\n --success-glow: rgba(71, 71, 161, 0.4);\n --danger: #ef4444;\n --bg-main: #f8fafc;\n --bg-glass: rgba(255, 255, 255, 0.85);\n --text-main: #0f172a;\n --text-muted: #64748b;\n }\n * { margin:0; padding:0; box-sizing:border-box; user-select:none; -webkit-user-select:none; }\n html, body {\n height:100%; width:100%; background:var(--bg-main);\n font-family:'Inter',sans-serif; overflow:hidden; display:flex; \n align-items:center; justify-content:center;\n }\n\n .phone { width:100%; height:100vh; background:#1e2223; display:flex; flex-direction:column; position:relative; overflow:hidden; }\n\n .header {\n display:flex; justify-content:space-between; align-items:center;\n padding:20px 20px; z-index:30; position:absolute; top:0; left:0; right:0;\n background: transparent;\n }\n\n .page { flex:1; display:flex; flex-direction:column; }\n .page-hidden { display:none !important; }\n\n .page-error { align-items:center; justify-content:center; padding:40px 30px; text-align:center; background: var(--bg-main); z-index: 50; position: absolute; inset: 0; }\n .err-icon { width:80px; height:80px; border-radius:24px; display:flex; align-items:center; justify-content:center; margin-bottom:8px; background:#fef2f2; border:1px solid #fecaca; color:var(--danger); }\n .err-icon i { font-size:40px; }\n .page-error h2 { font-size:22px; font-weight:800; color:var(--text-main); }\n .page-error p { font-size:14px; color:var(--text-muted); line-height:1.6; max-width:300px; margin-top: 8px; }\n\n .cam-area { flex:1; position:relative; overflow:hidden; background:#1e2223; }\n .cam-area video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }\n .cam-area canvas.overlay-cv { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }\n\n .face-oval { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index: 10; }\n .face-oval svg { width:100%; height:100%; transition:0.4s ease; filter:drop-shadow(0 4px 12px rgba(0,0,0,0.3)); }\n #ovalRing { stroke: white; stroke-width: 3; transition:all 0.4s ease; }\n .face-oval.ok #ovalRing { stroke:var(--success); stroke-width: 5; filter:drop-shadow(0 0 12px var(--success-glow)); }\n #ovalRingOut { stroke: rgba(255,255,255,0.4); stroke-width: 8; transition:all 0.4s ease; }\n .face-oval.ok #ovalRingOut { stroke: rgba(75, 73, 172, 0.4); }\n\n .cam-loading {\n position:absolute; inset:0; background:rgba(255, 255, 255, 0.95); backdrop-filter:blur(10px);\n display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:25; gap:20px;\n }\n .cam-loading .ldring {\n width:48px; height:48px; border-radius:50%; border:3px solid var(--primary-light); border-top-color:var(--primary);\n animation:spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;\n }\n @keyframes spin { to { transform:rotate(360deg); } }\n .cam-loading p { font-size:14px; color:var(--text-muted); font-weight:600; text-align:center; }\n .load-progress { width:200px; height:6px; background:rgba(99, 102, 241, 0.15); border-radius:4px; overflow:hidden; }\n .load-progress-fill { height:100%; width:0%; background:var(--primary); transition:width 0.4s ease; border-radius:4px; }\n\n .st-pill {\n position:absolute; top:80px; left:50%; transform:translateX(-50%); display:flex; align-items:center; gap:6px; padding:6px 16px; border-radius:30px; font-size:12px; font-weight:700; z-index:12; white-space:nowrap; background:var(--bg-glass); border:1px solid rgba(255,255,255,0.5); color:var(--text-main); backdrop-filter:blur(8px); transition:all 0.3s;\n }\n .st-pill i { font-size:18px; }\n .st-pill.searching { color:var(--text-muted); }\n .st-pill.searching i { color:var(--primary); }\n .st-pill.found { background:var(--success); color:#fff; border-color:var(--success); }\n .st-pill.found i { color:#fff; }\n .st-pill.warn { background:var(--danger); color:#fff; border-color:var(--danger); }\n .st-pill.warn i { color:#fff; }\n\n /* HUD */\n .hud-dashboard { position:absolute; bottom:20px; left:0; right:0; z-index:20; display:flex; flex-direction:column; align-items:center; pointer-events:none; }\n .match-card {\n background:rgba(255, 255, 255, 0.95); border:1px solid rgba(255,255,255,0.5);\n padding:12px 16px; border-radius:24px;\n width: 85%; max-width: 300px; position:relative; pointer-events:auto; text-align:center;\n backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);\n display:flex; flex-direction:column; align-items:center; gap:6px;\n }\n .match-card img { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; border: 2px solid var(--primary-light); }\n .match-card h4 { font-size: 13px; font-weight: 800; color:var(--text-main); margin-bottom: 0; }\n .match-card p { font-size: 10px; color:var(--text-muted); font-weight: 500; margin-bottom: 0; }\n .distance-bar { width:100%; height:4px; background:#e2e8f0; border-radius:4px; overflow:hidden; margin-top:5px; }\n .distance-fill { height:100%; width:0%; background:var(--primary); transition:width 0.1s linear, background-color 0.3s; }\n\n /* SUCCESS PIN REVEAL PAGE - ENHANCED */\n .page-success { align-items:center; justify-content:center; padding:40px 30px; text-align:center; color:var(--text-main); background: var(--bg-main); z-index:90; position:absolute; inset:0; }\n .suc-card {\n background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);\n border: 1px solid rgba(255, 255, 255, 0.6); border-radius: 32px; padding: 40px 30px;\n box-shadow: none; display: flex; flex-direction: column; align-items: center;\n max-width: 90%; animation: slideUpFade 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n }\n @keyframes slideUpFade { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }\n \n .suc-ring-wrap {\n width: 100px; height: 100px; border-radius: 50%; background: var(--primary-light);\n display: flex; align-items: center; justify-content: center; margin-bottom: 24px;\n position: relative;\n }\n .suc-ring-wrap::before {\n content: ''; position: absolute; inset: -15px; border-radius: 50%;\n border: 2px solid var(--primary-glow); animation: pulseSurround 2s infinite;\n }\n .suc-ring {\n width: 70px; height: 70px; border-radius: 50%; background: var(--success);\n display:flex; align-items:center; justify-content:center;\n z-index: 2;\n }\n .suc-ring i { font-size: 38px; color: #fff; }\n @keyframes pulseSurround { 0% { transform: scale(0.8); opacity: 1; } 100% { transform: scale(1.3); opacity: 0; } }\n \n .page-success h2 { font-size: 26px; font-weight: 800; color: var(--success); letter-spacing: -0.5px; margin-bottom: 12px; }\n .page-success p { font-size: 15px; color: var(--text-muted); line-height: 1.6; max-width: 280px; margin-bottom: 30px; }\n \n .pin-box-wrapper { position: relative; display: inline-block; }\n .pin-box {\n background: #fff; border: 2px dashed rgba(99, 102, 241, 0.4); padding: 20px 40px; border-radius: 20px;\n font-size: 42px; font-weight: 900; letter-spacing: 16px; color: var(--text-main); font-family: 'Courier New', monospace;\n text-align: center; position: relative; z-index: 2; display: flex; align-items: center; justify-content: center;\n }\n .pin-box-wrapper::after {\n content:''; position: absolute; bottom: -8px; left: 10px; right: 10px; height: 20px;\n background: rgba(99, 102, 241, 0.15); border-radius: 50%; z-index: 1;\n }\n \n /* ── APP-LIKE METERS ── */\n .brightness-meter, .liveness-meter {\n position:absolute; top:50%; transform:translateY(-50%);\n display:flex; flex-direction:column; align-items:center; gap:12px; z-index:12;\n padding: 14px 10px; background: none; border-radius: 0; backdrop-filter: none;\n }\n .brightness-meter { right:6px; }\n .liveness-meter { left:6px; }\n \n .bm-icon, .lm-icon { font-size:16px; color:#fff; }\n \n .bm-bar, .lm-bar { \n width:6px; height:100px; background:rgba(255,255,255,0.2); position:relative; overflow:hidden;\n border-radius:6px;\n }\n \n .bm-fill, .lm-fill { \n position:absolute; bottom:0; left:0; width:100%; border-radius:6px;\n background: #fff; transition:height 0.4s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.4s ease;\n }\n .bm-label, .lm-label { display:none; }\n .success-footer { margin-top: 30px; font-size: 13px; color: var(--text-muted); font-weight: 500; display: flex; align-items: center; gap: 6px; }\n .success-footer i { color: var(--success); font-size: 16px; }",
|
|
"15) {\n echo json_encode(['status' => 'error', 'message' => 'Nomor WhatsApp harus 10-15 digit angka.']);\n exit;\n }\n}\n\nif ($form_type === 'personal') {\n // Hanya validasi email, whatsapp, moto\n $required = [\n 'Email' => $email,\n 'WhatsApp' => $whatsapp,\n 'Moto' => $moto,\n ];\n foreach ($required as $label => $value) {\n if (empty($value)) {\n echo json_encode(['status' => 'error', 'message' => \"$label wajib diisi.\"]);\n exit;\n }\n }\n if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {\n echo json_encode(['status' => 'error', 'message' => 'Format email tidak valid.']);\n exit;\n }\n $waClean = preg_replace('/[^0-9]/', '', $whatsapp);\n if (strlen($waClean)",
|
|
"`;}\n if(lr){const e=lr.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';el('info-lr').innerHTML=`",
|
|
"Foto wajah Anda telah berhasil tersimpan dan terverifikasi dengan aman.",
|
|
"\"\n data-nik=\"",
|
|
"Username ini sudah digunakan.",
|
|
"');\n showCustomAlert('Koneksi gagal!');\n }\n });\n });\n\n // Handle Lock Door manually (forced POST)\n function setGlobalDoorLocked() {\n $.post('api_door_status.php', { action: 'lock' }, function(d) {\n if(d.status === 'success') {\n showCustomAlert('Pintu berhasil dikunci!');\n $('#modalDoorAction').modal('hide');\n updateUIFromState('locked'); // Force local update immediately\n }\n }, 'json');\n }\n\n function setGlobalDoorUnlocked(name) {\n $.post('api_door_status.php', { action: 'unlock' }, function(d) {\n if(d.status === 'success') {\n showScanResult('success', 'Akses Diizinkan', 'Welcome, ' + name);\n setTimeout(function() {\n $('#modalFingerprintScan').modal('hide');\n setTimeout(showDoorUnlockedModal, 400);\n updateUIFromState('unlocked'); // Force local update immediately\n }, 1500);\n }\n }, 'json');\n }\n\n // Handle Open Door Button Hover Effect (Only active if door is locked)\n $('.unlock-btn').hover(\n function() { if(!window.isDoorUnlocked) $(this).find('.btn-hover-effect').css('left', '100%'); },\n function() { if(!window.isDoorUnlocked) { $(this).find('.btn-hover-effect').css({'left': '-100%', 'transition': '0s'}); setTimeout(() => {$(this).find('.btn-hover-effect').css('transition', '0.5s');}, 50); } }\n );\n\n // Handle main Door Button Click\n $('#btnOpenDoor').on('click', function() {\n if (window.isDoorUnlocked) {\n // Door is already unlocked, clicking it forces a LOCK\n setGlobalDoorLocked();\n } else {\n // Door is locked, proceed with unlock sequence\n $('#accessPinInput').val('');\n $('#verifyPinError').css('display', 'none');\n $('#modalVerifyPin').modal('show');\n setTimeout(() => $('#accessPinInput').focus(), 500);\n }\n });\n\n // Handle PIN Submit\n $('#btnSubmitPinAccess').on('click', function() {\n var pin = $('#accessPinInput').val();\n if (pin.length !== 6) {\n $('#verifyPinErrorMsg').text(\"Masukkan 6 digit PIN secara lengkap.\");\n $('#verifyPinError').css('display', 'block');\n return;\n }\n var btn = $(this);\n var origHtml = btn.html();\n btn.prop('disabled', true).html('",
|
|
"Nomor valid' : '",
|
|
"Tidak ditemukan",
|
|
"— jika masalah terus terjadi, coba reset ESP32 ke pengaturan pabrik dan konfigurasi ulang dari awal.'\n ];\n }\n \n // Fallback status info (rendered OUTSIDE the red/yellow boxes)\n if (reason.indexOf('fallback_ok') !== -1) {\n fallbackHtml = '",
|
|
"dari router. Koneksi tidak berhasil dibuat dalam batas waktu yang ditentukan.';\n suggestions = [\n '",
|
|
"Harus 10-15 digit';\n fb.className = (v.length >= 10 && v.length",
|
|
"Pemulihan PIN 6-Digit",
|
|
"\" 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;\">",
|
|
"Batal / Ke Login",
|
|
"ESP32 sudah berhasil kembali terhubung ke jaringan WiFi yang sebelumnya pernah terhubung. Sistem kembali online seperti semula.",
|
|
"function showCustomAlert(title, message, type='warning') {\n const alertBox = document.getElementById('customAlert');\n const icon = document.getElementById('customAlertIcon');\n \n alertBox.className = 'custom-alert ' + type;\n if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';\n else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';\n else icon.className = 'mdi mdi-alert-circle custom-alert-icon';\n\n document.getElementById('customAlertTitle').innerText = title;\n document.getElementById('customAlertMsg').innerText = message;\n \n alertBox.style.display = 'flex';\n alertBox.style.animation = 'slideIn 0.3s ease-out';\n\n setTimeout(() => closeCustomAlert(), 5000); // auto close after 5s\n }\n\n function closeCustomAlert() {\n const alertBox = document.getElementById('customAlert');\n alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';\n setTimeout(() => {\n alertBox.style.display = 'none';\n }, 300);\n }\n\n document.addEventListener('DOMContentLoaded', () => {\n const setupPinGroup = (groupId, hiddenInputId) => {\n const group = document.getElementById(groupId);\n const inputs = group.querySelectorAll('.pin-box');\n const hiddenInput = document.getElementById(hiddenInputId);\n\n inputs.forEach((input, index) => {\n input.addEventListener('input', (e) => {\n // Only allow numbers\n input.value = input.value.replace(/\\D/g, '');\n \n if (input.value !== '' && index",
|
|
"Silakan ikuti langkah di bawah ini untuk memperbarui foto. Jika",
|
|
"';\n }\n }\n let avatarToUse = isRight ? adminAvatar : activeAvatarUrl;\n let onContextMenu = `oncontextmenu=\"showContextMenu(event, ${msg.id_chat}, '${msg.sender}', this); return false;\"`;\n \n let replyBlock = '';\n if (msg.reply_to_chat_id && msg.reply_message) {\n let rpSender = (msg.reply_sender === 'account') ? 'Anda' : document.getElementById('activeName').innerText;\n let strippedRpMsg = msg.reply_message.replace(/",
|
|
"yang dimasukkan.",
|
|
"').removeClass('btn-light').addClass('btn-outline-secondary').attr('title', 'Select Multiple');\n $('#btnBulkDelete').hide();\n $('.btn-row-delete').css('display', 'inline-block');\n $('.expand-arrow').css('display', 'inline-block');\n $('.bulk-cb').hide().prop('checked', false);\n }\n });\n\n $('#btnBulkDelete').on('click', function() {\n const selected = $('.bulk-cb:checked').map(function() { return $(this).val(); }).get();\n if(selected.length === 0) {\n showCustomAlert('Pilih minimal satu data untuk dihapus!', 'warning');\n return;\n }\n window.bulkDeleteIds = selected;\n document.getElementById(\"delete_id_occupant\").value = \"BULK\"; \n document.getElementById(\"delete_occupant_name\").innerText = selected.length + \" data terpilih\"; \n $(\"#modalDeleteOccupant\").modal(\"show\"); \n });\n $(\"#formDeleteOccupant\").submit(function(e) { \n e.preventDefault(); \n var val = $(\"#delete_id_occupant\").val();\n if(val === \"BULK\") {\n $(\"#btnDeleteOccupant\").prop(\"disabled\",true);\n $(\"#btnDeleteOccupant\").html('",
|
|
"';\n }\n \n let listContainer = document.getElementById('headerWarningList');\n if(listContainer) {\n listContainer.innerHTML = html;\n }\n }\n })\n .catch(err => console.error(\"Error fetching warnings for header: \", err));\n }\n\n function loadHeaderChats() {\n fetch('chat_api.php', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded'\n },\n body: 'action=get_contacts'\n })\n .then(response => response.json())\n .then(res => {\n if (res.status === 'success') {\n let html = '';\n const nowServer = new Date();\n \n // Filter hanya kontak dengan pesan yang belum terbaca\n let unreadContacts = res.data.filter(c => c.unread_count > 0);\n \n unreadContacts.slice(0, 10).forEach(c => {\n let rawLastMsg = c.last_message || '';\n let strippedMsg = rawLastMsg.replace(/",
|
|
"Ketik desa lalu pilih...",
|
|
"Mendeteksi Wajah...",
|
|
"'error', 'message' => 'design_json is required']);\n exit;\n }\n\n $hash = md5($json);\n\n if ($id > 0) {\n // Update existing\n $stmt = $pdo->prepare(\"UPDATE tft_designs SET name = ?, design_json = ?, version_hash = ? WHERE id = ?\");\n $stmt->execute([$name, $json, $hash, $id]);\n echo json_encode(['status' => 'success', 'message' => 'Design updated', 'id' => $id, 'version_hash' => $hash]);\n } else {\n // Insert new\n $stmt = $pdo->prepare(\"INSERT INTO tft_designs (name, design_json, version_hash) VALUES (?, ?, ?)\");\n $stmt->execute([$name, $json, $hash]);\n $newId = $pdo->lastInsertId();\n echo json_encode(['status' => 'success', 'message' => 'Design saved', 'id' => intval($newId), 'version_hash' => $hash]);\n }\n break;\n\n // ========== LOAD DESIGN BY ID ==========\n case 'load':\n $id = intval($_GET['id'] ?? 0);\n if ($id",
|
|
"di bawah ini untuk membuka Pintu Ruang FTM.",
|
|
"/* ── MANUAL STRIPES & BORDERS ── */\n#myTable thead th { border-bottom: 1px solid #e2e8f0 !important; }\n#myTable tbody tr td { border-bottom: none !important; /* Hapus garis horizontal bawah antar menu */ }\n\n.pill { display: inline-flex; align-items: center; font-weight: 600; font-size: 11px; padding: 3px 12px; border-radius: 20px; }\n.pill-green { background: #e8f5e9; color: #2e7d32; }\n.pill-red { background: #ffebee; color: #c62828; }\n.pill-amber { background: #fff8e1; color: #f57f17; }\n.pill-gray { background: #f5f5f5; color: #757575; }\n\n.tbl-avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; border: 1.5px solid #eee; }\n.expand-arrow { font-size: 18px; color: #bbb; transition: transform 0.25s ease, color 0.2s; }\ntr.shown .expand-arrow { transform: rotate(180deg); color: #4b7bec; }\n.details-control:hover .expand-arrow { color: #4b7bec; }\n\n/* ══════════════ DROPDOWN — Seamless UI ══════════════ */\n#myTable tbody tr.shown > td {\n border-bottom: none !important;\n border-top: none !important; /* Hapus garis atas */\n}\n#myTable tbody tr.shown + tr > td {\n padding: 0 !important;\n margin: 0 !important;\n border-bottom: none !important; \n border-top: 1px solid #e2e8f0 !important; /* Tambah garis horizontal pemisah antar Menu dengan Halaman Dropdown */\n}\n#myTable tbody tr.shown + tr > td > div,\n#myTable tbody tr.shown + tr > td > * {\n padding: 0 !important;\n margin: 0 !important;\n}\n#myTable tbody tr.shown + tr > td:first-child {\n border-left: 1px solid transparent !important; /* CHANGE DROP-DOWN COLOR HERE (Border) */\n border-bottom-left-radius: 8px;\n}\n#myTable tbody tr.shown + tr > td:last-child {\n border-right: 1px solid transparent !important; /* CHANGE DROP-DOWN COLOR HERE (Border) */\n border-bottom-right-radius: 8px;\n}\n\n#myTable tbody tr.shown > td:first-child {\n border-left: 1px solid #e2e8f0 !important;\n border-top-left-radius: 8px;\n}\n#myTable tbody tr.shown > td:last-child {\n border-right: 1px solid #e2e8f0 !important;\n border-top-right-radius: 8px;\n}\n\n@keyframes ddSlideIn {\n from { opacity: 0; transform: translateY(-4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* ── MODALS ── */\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n.confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n.confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n.delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }\n.frm-lbl { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; }\n.pin-warning { display: flex; align-items: flex-start; gap: 8px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #92400e; }\n.pin-warning i { font-size: 18px; color: #f59e0b; flex-shrink: 0; margin-top: 1px; }\n.foto-wrapper { position: relative; display: inline-block; cursor: pointer; width: 72px; height: 72px; }\n.foto-circle { width: 72px; height: 72px; border-radius: 50%; object-fit: cover; border: 2px solid #e0e0e0; transition: border-color 0.2s; }\n.foto-badge { position: absolute; bottom: 0; right: 0; width: 24px; height: 24px; border-radius: 50%; background: #4b7bec; display: flex; align-items: center; justify-content: center; border: 2px solid #fff; }\n.foto-badge i { color: #fff; font-size: 12px; }\n.foto-wrapper:hover .foto-circle { border-color: #4b7bec; }\n.btn-custom-edit {\n background: #e2e6ea;\n border: none;\n color: #212529;\n transition: background 0.2s ease;\n}\n.btn-custom-edit:hover, .btn-custom-edit:focus {\n background: #cbd3da;\n color: #212529;\n}\n.eye-toggle { cursor: pointer; background: #fff; border-left: none; }\n.eye-toggle:hover i { color: #4b7bec; }\n.code-big { font-size: 26px; letter-spacing: 8px; color: #222; font-weight: 700; border-right: none; }\n.code-input-group .form-control { border-right: none; }\n.info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; }\n.info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n.info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n.countdown-timer { font-weight: 700; color: #dc2626; font-size: 12px; margin-left: 8px; display: inline-flex; align-items: center; background: #fef2f2; padding: 3px 10px; border-radius: 6px; border: 1px solid #fecaca; white-space: nowrap; }\n@keyframes pulse-timer { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }",
|
|
"Terjadi Kesalahan Jaringan.';\n errorEl.style.display = \"block\";\n btn.disabled = false;\n });\n \n } catch (err) {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
|
|
"\\n\"\n . \"Gunakan perintah ini untuk membuka panel pembaruan profil. Melalui panel ini, Anda dapat memperbarui Nama Lengkap, Nomor Induk Kependudukan (NIK), serta Foto referensi sistem pengenalan wajah Anda.\\n\\n\"\n . \"",
|
|
"Sistem token belum siap.",
|
|
"Pendaftaran Dibatalkan / Gagal (' + err.message + ')';\n errorEl.style.display = \"block\";\n btn.disabled = false;\n }\n}\n\n// Timer Logic for Modal\nvar fingerprintTimerId = null;\nfunction startFingerprintTimer() {\n var remaining = 30 * 60; // 30 minutes in seconds\n var countdownEl = document.getElementById('fingerprintCountdown');\n function updateDisplay() {\n var m = Math.floor(remaining / 60);\n var s = remaining % 60;\n countdownEl.innerHTML = String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0');\n }\n updateDisplay();\n fingerprintTimerId = setInterval(function() {\n remaining--;\n updateDisplay();\n if (remaining",
|
|
"Tidak ada peringatan baru.",
|
|
"Wajah Anda cocok dengan data occupant",
|
|
"Pilih desa untuk memuat kode pos...",
|
|
"Belum Terhubung",
|
|
"`;}\n el('gAcc').innerHTML=h;\n}\nfunction uAcc(){\n const fp=F('fingerprint'),so=F('solenoid_lock'),rl=F('relay');\n if(fp){const e=fp.extra_data||{};el('info-fp').innerHTML=mr('mdi-account-group','Enrolled',e.enrolled_count||'0','b')+mr('mdi-check-decagram','Result',e.last_scan_result||'—',e.last_scan_result==='matched'?'g':'r')+mr('mdi-percent','Confidence',fm(e.confidence)+' %','c');el('bar-fp').className='cd-top '+sc(fp.status);el('ft-fp').innerHTML='",
|
|
"— jika router sudah mencapai batas maksimal perangkat yang terhubung (umumnya 32 device), putuskan beberapa perangkat yang tidak digunakan.',\n '",
|
|
"Password Asli",
|
|
"');\n \n // Selalu Update connected SSID\n if(res.current_ssid && res.current_ssid !== 'Unknown') {\n $('#esp32-ssid').text(res.current_ssid);\n }\n \n // Selalu Update IP Address\n if(res.esp_ip && res.esp_ip !== 'Unknown') {\n $('#esp32-ip').text(res.esp_ip);\n }\n } else if (res.status === 'connecting') {\n isWifiConnected = false;\n $('#wifi-status-text').text('WiFi Connecting...').css('color', '#ffffff');\n $('#wifi-dot').css({'background': '#f59e0b', 'box-shadow': 'none'});\n \n // Loading Spinner\n $('#esp32-status-icon-container').html('",
|
|
"Data terverifikasi dan tersimpan di server",
|
|
"\\n\"\n . \"Gunakan perintah ini untuk memulihkan akses identitas Anda. Proses ini memerlukan autentikasi PIN 6-Digit yang telah Anda miliki sebelumnya sebagai langkah validasi keamanan ganda.\\n\\n\"\n . \"📝",
|
|
"Gagal memeriksa username.",
|
|
"*/\nignore_user_abort(true);\nset_time_limit(15);\n\n// Accept args from CLI or GET\nif (php_sapi_name() === 'cli') {\n $chat_id = isset($argv[1]) ? $argv[1] : '';\n $msg_id = isset($argv[2]) ? $argv[2] : '';\n $orig_text = isset($argv[3]) ? $argv[3] : '✅ Data Berhasil diperbarui';\n} else {\n $chat_id = isset($_GET['chat_id']) ? $_GET['chat_id'] : '';\n $msg_id = isset($_GET['msg_id']) ? $_GET['msg_id'] : '';\n $orig_text = isset($_GET['text']) ? $_GET['text'] : '✅ Data Berhasil diperbarui';\n \n ob_start();\n echo \"OK\";\n $size = ob_get_length();\n header(\"Content-Length: $size\");\n header('Connection: close');\n ob_end_flush();\n if (function_exists('ob_flush')) ob_flush();\n flush();\n if (session_id()) session_write_close();\n}\n\nif (empty($chat_id) || empty($msg_id)) exit;\n\n$botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n$api = \"https://api.telegram.org/bot\" . $botToken;\n\n// Live countdown: 4, 3, 2, 1 (initial message already shows \"5 detik\")\nfor ($i = 4; $i >= 1; $i--) {\n sleep(1);\n $newText = \"",
|
|
"Notifikasi di menu sampah akan otomatis terhapus permanen setelah 30 hari.",
|
|
"Password Baru",
|
|
"Hapus Occupant?",
|
|
"menolak koneksi karena password yang dikirim tidak sesuai",
|
|
"Dekatkan ESP32 ke router WiFi target",
|
|
"Foto Berhasil Diambil",
|
|
"Nama",
|
|
"Edit Occupant",
|
|
"Semua data di database berhasil dikosongkan! Sisa tabel dan strukturnya saja.",
|
|
"Verifikasi PIN - Identia",
|
|
"\\n\\nMaaf, Anda hanya dapat mengirim pesan berupa",
|
|
"PIN",
|
|
"Dekatkan ESP32 dengan router",
|
|
"Silakan Bagikan Kontak Terlebih Dahulu untuk Berinteraksi Langsung dengan Admin",
|
|
"Please create a new password for this account.",
|
|
":root {\n --accent: #6366f1;\n --accent2: #818cf8;\n --green: #22c55e;\n --green-glow: rgba(34,197,94,0.35);\n --red: #ef4444;\n --amber: #f59e0b;\n --dark: #0f172a;\n --darker: #070b14;\n --card: rgba(15,23,42,0.85);\n }\n * { margin: 0; padding: 0; box-sizing: border-box; }\n html, body {\n height: 100%; width: 100%;\n background: var(--darker);\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;\n overflow: hidden;\n display: flex; align-items: center; justify-content: center;\n -webkit-font-smoothing: antialiased;\n }\n\n /* ═══ PHONE FRAME ═══ */\n .phone {\n width: 100%; max-width: 430px;\n height: 100vh; max-height: 100vh;\n background: var(--darker);\n display: flex; flex-direction: column;\n position: relative; overflow: hidden;\n }\n @media (min-width: 500px) {\n body { background: #030712; }\n .phone {\n height: 93vh; max-height: 850px;\n border-radius: 36px;\n border: 3px solid rgba(99,102,241,0.15);\n box-shadow:\n 0 0 0 1px rgba(99,102,241,0.08),\n 0 25px 80px rgba(0,0,0,0.7),\n inset 0 1px 0 rgba(255,255,255,0.03);\n }\n }\n\n /* ═══ HEADER ═══ */\n .header {\n display: flex; justify-content: space-between; align-items: center;\n padding: 16px 20px;\n background: linear-gradient(180deg, rgba(15,23,42,0.98), rgba(15,23,42,0.9));\n backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);\n z-index: 30; position: absolute; top: 0; left: 0; right: 0;\n border-bottom: 1px solid rgba(99,102,241,0.08);\n }\n .header-left {\n display: flex; align-items: center; gap: 10px;\n }\n .header-logo {\n width: 36px; height: 36px; border-radius: 10px;\n background: linear-gradient(135deg, var(--accent), #7c3aed);\n display: flex; align-items: center; justify-content: center;\n box-shadow: 0 2px 12px rgba(99,102,241,0.3);\n }\n .header-logo i { color: #fff; font-size: 18px; }\n .header-text h1 { font-size: 14px; font-weight: 800; color: #fff; letter-spacing: 0.2px; line-height: 1.2; }\n .header-text span { font-size: 10px; font-weight: 600; color: rgba(255,255,255,0.35); text-transform: uppercase; letter-spacing: 1.2px; }\n .header-badge {\n font-size: 10px; font-weight: 700;\n color: var(--accent2); background: rgba(99,102,241,0.1);\n padding: 5px 12px; border-radius: 8px;\n border: 1px solid rgba(99,102,241,0.15);\n max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n }\n\n /* ═══ PAGES ═══ */\n .page { flex: 1; display: flex; flex-direction: column; }\n .page-hidden { display: none !important; }\n\n /* ═══ VALIDATING ═══ */\n .page-validating {\n align-items: center; justify-content: center; gap: 20px; color: #fff;\n }\n .val-ring {\n width: 64px; height: 64px; border-radius: 50%;\n border: 3px solid rgba(99,102,241,0.12);\n border-top-color: var(--accent);\n animation: spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n .page-validating p { font-size: 13px; color: rgba(255,255,255,0.4); font-weight: 500; }\n\n /* ═══ ERROR ═══ */\n .page-error {\n align-items: center; justify-content: center;\n padding: 40px 30px; text-align: center; gap: 8px; color: #fff;\n }\n .err-icon {\n width: 88px; height: 88px; border-radius: 24px;\n display: flex; align-items: center; justify-content: center;\n margin-bottom: 12px;\n }\n .err-icon.expired {\n background: linear-gradient(135deg, rgba(245,158,11,0.12), rgba(245,158,11,0.04));\n border: 2px solid rgba(245,158,11,0.15);\n }\n .err-icon.expired i { font-size: 42px; color: var(--amber); }\n .err-icon.invalid {\n background: linear-gradient(135deg, rgba(239,68,68,0.12), rgba(239,68,68,0.04));\n border: 2px solid rgba(239,68,68,0.15);\n }\n .err-icon.invalid i { font-size: 42px; color: var(--red); }\n .page-error h2 { font-size: 22px; font-weight: 800; }\n .page-error p { font-size: 13px; color: rgba(255,255,255,0.4); line-height: 1.7; max-width: 300px; }\n\n /* ═══ CAMERA ═══ */\n .cam-area {\n flex: 1; position: relative; overflow: hidden; background: #1e2223;\n }\n .cam-area video {\n width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1);\n }\n .cam-area canvas.overlay-cv {\n position: absolute; top: 0; left: 0;\n width: 100%; height: 100%; pointer-events: none;\n }\n\n /* Oval */\n .face-oval {\n position: absolute; top: 0; left: 0; width: 100%; height: 100%;\n pointer-events: none; display: flex; align-items: center; justify-content: center;\n }\n .face-oval svg { width: 68%; max-width: 250px; height: auto; transition: 0.5s ease; }\n .face-oval.ok #ovalRing {\n stroke: var(--green); stroke-width: 3.5; stroke-dasharray: none;\n filter: drop-shadow(0 0 18px var(--green-glow));\n }\n\n /* Corners */\n .corners {\n position: absolute; top: 50%; left: 50%;\n width: 210px; height: 270px; transform: translate(-50%,-50%);\n pointer-events: none;\n }\n .corners::before, .corners::after, .corners .c::before, .corners .c::after {\n content:''; position: absolute;\n width: 30px; height: 30px;\n border-color: rgba(255,255,255,0.3); border-style: solid; border-width: 0;\n transition: all 0.4s ease;\n }\n .corners::before { top:0;left:0; border-top-width:3px;border-left-width:3px; border-radius:10px 0 0 0; }\n .corners::after { top:0;right:0; border-top-width:3px;border-right-width:3px; border-radius:0 10px 0 0; }\n .corners .c::before { bottom:0;left:0; border-bottom-width:3px;border-left-width:3px; border-radius:0 0 0 10px; }\n .corners .c::after { bottom:0;right:0; border-bottom-width:3px;border-right-width:3px; border-radius:0 0 10px 0; }\n .corners.ok::before,.corners.ok::after,.corners.ok .c::before,.corners.ok .c::after {\n border-color: var(--green);\n box-shadow: 0 0 12px var(--green-glow);\n }\n\n /* Scan line */\n .scanbar {\n position: absolute; left: 50%; width: 190px; height: 2px;\n transform: translateX(-50%);\n background: linear-gradient(90deg, transparent, rgba(99,102,241,0.6), transparent);\n animation: scanY 2.8s ease-in-out infinite;\n pointer-events: none; opacity: 0.6;\n }\n .scanbar.ok { background: linear-gradient(90deg, transparent, rgba(34,197,94,0.7), transparent); }\n @keyframes scanY {\n 0%,100% { top: calc(50% - 130px); }\n 50% { top: calc(50% + 130px); }\n }\n\n /* Loading cam overlay */\n .cam-loading {\n position: absolute; inset: 0;\n background: rgba(7,11,20,0.94);\n display: flex; flex-direction: column;\n align-items: center; justify-content: center;\n z-index: 15; gap: 18px; color: #fff;\n backdrop-filter: blur(10px);\n }\n .cam-loading .ldring {\n width: 52px; height: 52px; border-radius: 50%;\n border: 3px solid rgba(255,255,255,0.06);\n border-top-color: var(--accent2);\n animation: spin 0.7s linear infinite;\n }\n .cam-loading p { font-size: 12px; color: rgba(255,255,255,0.45); font-weight: 500; }\n\n /* ── Instructions Panel ── */\n .instructions {\n position: absolute; top: 68px; left: 50%;\n transform: translateX(-50%);\n background: rgba(7,11,20,0.88);\n backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);\n border: 1px solid rgba(99,102,241,0.1);\n border-radius: 14px; padding: 10px 16px;\n z-index: 12; max-width: 320px; width: 90%;\n }\n .instr-title {\n font-size: 10px; font-weight: 800; color: var(--accent2);\n text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 6px;\n }\n .instr-list {\n list-style: none; display: flex; flex-direction: column; gap: 4px;\n }\n .instr-list li {\n font-size: 10.5px; color: rgba(255,255,255,0.55); font-weight: 500;\n display: flex; align-items: center; gap: 6px;\n }\n .instr-list li i { font-size: 12px; color: var(--accent); flex-shrink: 0; }\n\n /* Status pill */\n .st-pill {\n position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);\n display: flex; align-items: center; gap: 8px;\n padding: 10px 22px; border-radius: 28px;\n font-size: 12px; font-weight: 700; color: #fff;\n z-index: 12; white-space: nowrap;\n backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);\n transition: all 0.4s ease;\n }\n .st-pill i { font-size: 18px; }\n .st-pill.searching {\n background: rgba(15,23,42,0.85);\n border: 1px solid rgba(99,102,241,0.15);\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n }\n .st-pill.found {\n background: rgba(22,163,74,0.88);\n border: 1px solid rgba(74,222,128,0.3);\n box-shadow: 0 4px 28px rgba(22,163,74,0.35);\n }\n .st-pill.warn {\n background: rgba(220,38,38,0.88);\n border: 1px solid rgba(248,113,113,0.25);\n box-shadow: 0 4px 20px rgba(220,38,38,0.25);\n }\n\n /* ═══ BOTTOM BAR ═══ */\n .bar-capture {\n padding: 22px 24px 30px;\n background: linear-gradient(0deg, rgba(7,11,20,0.99), rgba(7,11,20,0.75));\n display: flex; align-items: center; justify-content: center;\n position: relative; z-index: 20;\n }\n .shutter {\n width: 74px; height: 74px; border-radius: 50%;\n border: 4px solid rgba(255,255,255,0.3);\n background: transparent; cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n transition: all 0.25s ease; position: relative;\n }\n .shutter::after {\n content:''; width: 58px; height: 58px; border-radius: 50%;\n background: linear-gradient(135deg, #fff, #e2e8f0);\n transition: 0.2s;\n }\n .shutter:hover::after { transform: scale(0.93); }\n .shutter:active::after { transform: scale(0.86); background: #cbd5e1; }\n .shutter:disabled { opacity: 0.25; cursor: not-allowed; pointer-events: none; }\n .shutter-lbl {\n font-size: 9px; color: rgba(255,255,255,0.35);\n text-align: center; margin-top: 8px;\n font-weight: 800; letter-spacing: 1px; text-transform: uppercase;\n }\n\n /* After capture bar */\n .bar-after {\n padding: 22px 24px 30px;\n background: linear-gradient(0deg, rgba(7,11,20,0.99), rgba(7,11,20,0.75));\n align-items: center; justify-content: center; gap: 28px; z-index: 20;\n display: none;\n }\n .act-btn {\n display: flex; flex-direction: column; align-items: center;\n gap: 6px; border: none; background: none; cursor: pointer; transition: 0.2s;\n }\n .act-btn .ring {\n width: 60px; height: 60px; border-radius: 50%;\n display: flex; align-items: center; justify-content: center;\n font-size: 24px; transition: all 0.25s ease;\n }\n .act-btn span { font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.8px; }\n .act-btn.retry .ring { background: rgba(255,255,255,0.08); color: #fff; border: 1px solid rgba(255,255,255,0.08); }\n .act-btn.retry span { color: rgba(255,255,255,0.4); }\n .act-btn.retry:hover .ring { background: rgba(255,255,255,0.14); }\n .act-btn.save .ring {\n background: linear-gradient(135deg, #22c55e, #16a34a);\n color: #052e16;\n box-shadow: 0 4px 24px rgba(34,197,94,0.35);\n }\n .act-btn.save span { color: var(--green); }\n .act-btn.save:hover .ring { transform: scale(1.06); box-shadow: 0 6px 32px rgba(34,197,94,0.45); }\n\n /* Preview */\n .preview {\n flex: 1; position: relative; background: #1e2223;\n overflow: hidden;\n }\n .preview canvas { width: 100%; height: 100%; object-fit: cover; }\n .preview-tag {\n position: absolute; top: 74px; left: 50%; transform: translateX(-50%);\n background: rgba(22,163,74,0.92); color: #fff;\n padding: 8px 22px; border-radius: 28px;\n font-size: 11px; font-weight: 700;\n display: flex; align-items: center; gap: 7px;\n backdrop-filter: blur(12px); z-index: 5;\n box-shadow: 0 4px 20px rgba(22,163,74,0.3);\n text-transform: uppercase; letter-spacing: 0.5px;\n }\n .preview-tag i { font-size: 16px; }\n\n /* Toast */\n .toast {\n position: absolute; bottom: 96px; left: 50%; transform: translateX(-50%);\n padding: 12px 24px; border-radius: 16px;\n font-size: 12px; font-weight: 700;\n display: none; align-items: center; gap: 8px;\n z-index: 25; white-space: nowrap;\n animation: slideUp 0.3s ease;\n }\n .toast i { font-size: 17px; }\n .toast.ok { background: #065f46; color: #a7f3d0; border: 1px solid #34d399; }\n .toast.err { background: #7f1d1d; color: #fca5a5; border: 1px solid #f87171; }\n @keyframes slideUp {\n from { opacity:0; transform: translateX(-50%) translateY(12px); }\n to { opacity:1; transform: translateX(-50%) translateY(0); }\n }\n\n /* ═══ SUCCESS PAGE ═══ */\n .page-success {\n align-items: center; justify-content: center;\n padding: 40px 30px; text-align: center; color: #fff; gap: 10px;\n }\n .suc-ring {\n width: 100px; height: 100px; border-radius: 50%;\n background: linear-gradient(135deg, rgba(34,197,94,0.12), rgba(34,197,94,0.04));\n border: 2px solid rgba(34,197,94,0.2);\n display: flex; align-items: center; justify-content: center;\n animation: pulseGreen 2.5s ease-in-out infinite;\n margin-bottom: 8px;\n }\n .suc-ring i { font-size: 48px; color: var(--green); }\n @keyframes pulseGreen {\n 0%,100% { box-shadow: 0 0 0 0 rgba(34,197,94,0.15); }\n 50% { box-shadow: 0 0 0 20px rgba(34,197,94,0); }\n }\n .page-success h2 { font-size: 22px; font-weight: 900; color: var(--green); }\n .page-success p { font-size: 13px; color: rgba(255,255,255,0.4); line-height: 1.7; max-width: 280px; }\n .suc-foto {\n width: 110px; height: 110px; border-radius: 50%;\n object-fit: cover; border: 3px solid var(--green);\n box-shadow: 0 4px 28px rgba(34,197,94,0.3);\n margin-top: 12px;\n }\n .suc-check {\n display: flex; align-items: center; gap: 6px;\n font-size: 11px; color: rgba(255,255,255,0.3); font-weight: 600;\n margin-top: 8px;\n }\n .suc-check i { color: var(--green); font-size: 14px; }\n\n /* ═══ BRIGHTNESS METER ═══ */\n .brightness-meter {\n position: absolute; top: 172px; right: 16px;\n display: flex; flex-direction: column; align-items: center; gap: 4px;\n z-index: 12;\n }\n .bm-bar {\n width: 4px; height: 60px; border-radius: 4px;\n background: rgba(255,255,255,0.1);\n position: relative; overflow: hidden;\n }\n .bm-fill {\n position: absolute; bottom: 0; left: 0; width: 100%;\n border-radius: 4px; transition: height 0.5s ease, background 0.5s ease;\n }\n .bm-label {\n font-size: 8px; font-weight: 800; text-transform: uppercase;\n letter-spacing: 1px; writing-mode: vertical-rl;\n text-orientation: mixed; color: rgba(255,255,255,0.3);\n }\n .bm-icon { font-size: 14px; transition: color 0.3s; }",
|
|
"Setup Your Secure PIN",
|
|
"\" data-id=\"",
|
|
"Menyimpan...';\n \n// fetch(\"tambah_account.php\", { method: \"POST\", body: new FormData(this) })\n// .then(function(r){ return r.json(); })\n// .then(function(d) {\n// btn.disabled = false; btn.innerHTML = '",
|
|
"Memuat kamera & model deteksi wajah...",
|
|
"Foto Diambil",
|
|
"Browser ini tidak mendukung WebAuthn (Passkey / Sidik Jari).",
|
|
"Edit",
|
|
"Ketik kecamatan lalu pilih...",
|
|
"Gagal dari server:",
|
|
"Simpan';\n// if (d.status === \"success\") { \n// $(\"#modalTambah\").modal(\"hide\"); \n// document.getElementById(\"formTambahAccount\").reset(); \n// showCustomAlert(\"Akun berhasil ditambahkan.\");\n// setTimeout(function(){ location.reload(); }, 1500);\n// } else { \n// showCustomAlert(d.message || \"Gagal menyimpan data.\"); \n// }\n// }).catch(function(){ \n// btn.disabled=false; \n// btn.innerHTML='",
|
|
"Pesan ini akan dihapus dalam \" . $i . \" detik...",
|
|
"Menghapus...');\n let promises = window.bulkDeleteIds.map(id => {\n let fd = new FormData();\n fd.append('id_account', id);\n return fetch('delete_account.php', { method: 'POST', body: fd }).then(r => r.json());\n });\n Promise.all(promises).then(() => {\n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n setTimeout(function(){ $(\"#modalDeleteAccount\").modal(\"hide\"); location.reload(); }, 1200); \n });\n } else {\n $.post(\"delete_account.php\", $(this).serialize(), function(d) { \n if (d.status === \"success\") { \n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n $(\"#btnDeleteAccount\").prop(\"disabled\",true); \n setTimeout(function(){ $(\"#modalDeleteAccount\").modal(\"hide\"); location.reload(); }, 1200); \n } else { \n showCustomAlert(d.message); \n } \n }, \"json\"); \n }\n });\n\n $('#modalTambah').on('hidden.bs.modal', function(){ \n document.getElementById(\"formTambahAccount\").reset(); \n });\n \n $('#modalFingerprint').on('shown.bs.modal', function(){ startFingerprintTimer(); });\n $('#modalFingerprint').on('hidden.bs.modal', function(){ stopFingerprintTimer(); location.reload(); });\n\n if ($.fn.DataTable.isDataTable('#myTable')) { \n $('#myTable').DataTable().destroy(); \n }\n $('#myTable').DataTable({ \"order\": [[0, 'asc']] });\n\n $('#inputPasswordField').on('input', function() {\n var pass = $(this).val();\n var meter = $('#pwStrengthMeterAdd');\n var bar = $('#pwStrengthBarAdd');\n var text = $('#pwStrengthTextAdd');\n\n if(pass.length > 0) {\n meter.show();\n var hasLower = /[a-z]/.test(pass);\n var hasUpper = /[A-Z]/.test(pass);\n var hasNumber = /\\d/.test(pass);\n var hasSymbol = /[^A-Za-z0-9]/.test(pass);\n var typesCount = (hasLower?1:0) + (hasUpper?1:0) + (hasNumber?1:0) + (hasSymbol?1:0);\n \n if (pass.length >= 8 && typesCount === 4) {\n bar.css({width: '100%', backgroundColor: '#10b981'}); // Kuat\n text.text('Password Kuat').css('color', '#10b981');\n } else if (pass.length >= 6 && typesCount >= 2) {\n bar.css({width: '66%', backgroundColor: '#f59e0b'}); // Sedang\n text.text('Password Sedang').css('color', '#f59e0b');\n } else {\n bar.css({width: '33%', backgroundColor: '#ef4444'}); // Lemah\n text.text('Password Lemah').css('color', '#ef4444');\n }\n } else {\n meter.hide();\n text.text('');\n }\n });\n\n // ── Face Detection on Photo Upload (Add Modal) ──\n var faceModelLoadedAdd = false;\n faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models').then(function() {\n faceModelLoadedAdd = true;\n });\n \n $('#fotoInputAdd').on('change', function() {\n var file = this.files[0];\n if (!file) {\n document.getElementById('faceDetectPreviewAdd').style.display = 'none';\n return;\n }\n \n var preview = document.getElementById('faceDetectPreviewAdd');\n var badge = document.getElementById('faceDetectBadgeAdd');\n \n preview.style.display = 'block';\n badge.style.cssText = 'padding:6px 12px; border-radius:6px; font-size:12px; font-weight:600; display:inline-block; width:100%; text-align:center; background:#e2e8f0; color:#475569; border:1px solid #cbd5e1;';\n badge.innerHTML = '",
|
|
"Data Occupant",
|
|
"Verifikasi Wajah",
|
|
"/* ========================================================= */\n/* INBOX LIKE UI CSS */\n/* ========================================================= */\n.email-wrapper {\n display: flex;\n flex-direction: row; /* Pastikan selalu ke samping */\n background: #ffffff;\n height: 600px;\n overflow: hidden;\n border-radius: 12px;\n}\n\n/* SIDEBAR */\n.email-sidebar {\n width: 250px;\n background: #ffffff;\n border-right: 1px solid #f0f0f0;\n padding: 20px 0; /* Padding vertikal saja agar full-width horizontally */\n flex-shrink: 0;\n overflow-y: auto;\n}\n.sidebar-header {\n padding: 20px 20px 15px 20px;\n}\n.email-sidebar .nav-item {\n margin-bottom: 2px;\n}\n.email-sidebar .nav-item .nav-link {\n color: #4a4a4a;\n padding: 12px 20px; /* Jarak kiri/kanan sama dengan header */\n border-radius: 0; /* Hindari radius agar flat edge-to-edge */\n font-size: 0.95rem;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n transition: all 0.2s;\n}\n.email-sidebar .nav-item .nav-link i {\n font-size: 1.25rem;\n margin-right: 15px;\n color: #8c909a;\n width: 20px;\n text-align: center;\n}\n.email-sidebar .nav-item.active .nav-link {\n background-color: #f0f4f9; /* Biru muda sangat tipis */\n color: #4b49ac;\n font-weight: 500;\n}\n.email-sidebar .nav-item.active .nav-link i {\n color: #4b49ac;\n}\n.email-sidebar .nav-item .nav-link:hover:not(.active) {\n background-color: #fafbfd;\n}\n.badge {\n padding: 4px 8px;\n font-size: 0.75rem;\n border-radius: 20px;\n margin-left: auto;\n}\n\n/* Indicators in Labels */\n.sidebar-labels-wrapper {\n margin-top: 35px;\n}\n.sidebar-labels-header {\n padding: 0 20px;\n margin-bottom: 15px;\n}\n\n.indicator-label {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n display: inline-block;\n margin-right: 12px;\n}\n.indicator-primary { background: #4b49ac; } /* Sesuai request user warna biru */\n.indicator-warning { background: #f59e0b; } /* Warna Kuning */\n.indicator-danger { background: #ef4444; } /* Warna Merah */\n.indicator-success { background: #10b981; } /* Warna Hijau */\n\n/* MAIN BODY */\n.email-main-body {\n flex-grow: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n min-width: 0; /* Mencegah overflow flex child */\n background-color: #ffffff; /* Ubah ke putih */\n}\n.email-search {\n display: flex;\n align-items: center;\n background: #f4f5fA;\n border-radius: 6px;\n padding: 0 15px;\n max-width: 600px;\n}\n.email-search input {\n height: 40px;\n box-shadow: none !important;\n}\n\n/* LIST ITEMS */\n.scroll-area {\n overflow-y: auto;\n flex-grow: 1;\n}\n.email-list-item {\n display: flex;\n align-items: center;\n padding: 12px 20px;\n border-bottom: 1px solid #ebedf2;\n cursor: pointer;\n background: #f2f3f3;\n transition: background 0.2s;\n position: relative;\n}\n.email-list-item:nth-child(even) {\n background: #ffffff;\n}\n.email-list-item:hover {\n background: #fafbfc !important;\n box-shadow: inset 2px 0 0 #cfd8dc;\n}\n.email-list-item.font-weight-bold {\n /* Style tetap tapi background biarkan selang-seling */\n}\n.email-list-item.read {\n /* Biarkan transparan/selang-seling secara utuh */\n}\n.email-list-actions {\n display: flex;\n align-items: center;\n z-index: 2;\n}\n.email-list-detail {\n cursor: pointer;\n}\n.email-star {\n font-size: 1.25rem;\n cursor: pointer;\n}\n.email-list-content {\n font-size: 0.95rem;\n}\n\n.email-list-hover-actions {\n display: none;\n position: absolute;\n right: 20px;\n background: #fdfdfd;\n box-shadow: -10px 0 10px #fdfdfd;\n padding-left: 10px;\n}\n.email-list-item:hover .email-list-hover-actions {\n display: flex;\n align-items: center;\n}\n.email-list-item:hover .email-list-time {\n display: none;\n}\n.btn-icon {\n width: 32px;\n height: 32px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n margin-left: 5px;\n background: transparent;\n}\n.btn-icon:hover {\n background: #ebedf2;\n}\n\n/* Custom Scrollbar for inner areas */\n.scroll-area::-webkit-scrollbar {\n width: 6px;\n}\n.scroll-area::-webkit-scrollbar-track {\n background: transparent;\n}\n.scroll-area::-webkit-scrollbar-thumb {\n background-color: #d1d5db;\n border-radius: 10px;\n}\n\n/* Checkbox fixing */\n.custom-control-label::before {\n border-radius: 4px;\n border: 1px solid #c9cdd4;\n}\n\n@media (max-width: 768px) {\n .email-sidebar { display: none; position: absolute; height: 100%; z-index: 10; box-shadow: 2px 0 10px rgba(0,0,0,0.1); }\n .email-sidebar.show { display: block; }\n .email-wrapper { height: auto; min-height: 80vh; }\n .email-list-sender { width: 100px !important; min-width: 100px !important; font-size: 13px !important; }\n .email-list-content span { font-size: 12px !important; }\n .email-list-item { padding: 10px 12px; }\n #paginationControls .text-small { display: none !important; }\n #filterInputs { flex-wrap: wrap; }\n .email-detail-header { flex-wrap: wrap; gap: 8px; }\n .detail-subject { font-size: 0.9rem !important; }\n .email-detail-body { margin: 0 10px !important; padding: 16px !important; }\n}\n\n/* Modals */\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n.confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n.confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n.delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }",
|
|
"\"IDLE\",\n \"ssid\" => \"\",\n \"password\" => \"\",\n \"scan_results\" => [],\n \"status\" => \"offline\",\n \"current_ssid\" => \"Unknown\",\n \"last_seen\" => 0,\n \"esp_ip\" => \"Unknown\",\n \"change_status\" => \"idle\",\n \"rssi\" => -90\n ];\n file_put_contents($stateFile, json_encode($initialState));\n}\n$state = json_decode(file_get_contents($stateFile), true);\n\nif (!is_array($state)) {\n $state = [\n \"command\" => \"IDLE\",\n \"ssid\" => \"\",\n \"password\" => \"\",\n \"scan_results\" => [],\n \"status\" => \"offline\",\n \"current_ssid\" => \"Unknown\",\n \"last_seen\" => 0,\n \"esp_ip\" => \"Unknown\",\n \"change_status\" => \"idle\",\n \"rssi\" => -90\n ];\n}\n\n$action = isset($_GET['action']) ? $_GET['action'] : (isset($_POST['action']) ? $_POST['action'] : '');\n\n// ==========================================\n// ENDPOINTS FOR DASHBOARD (AJAX)\n// ==========================================\n\nif ($action == 'trigger_scan') {\n // Tell ESP32 to scan\n $state['command'] = 'SCAN';\n $state['scan_results'] = []; // Clear previous results\n file_put_contents($stateFile, json_encode($state));\n echo json_encode([\"success\" => true, \"message\" => \"Scan triggered\"]);\n exit;\n}\n\nif ($action == 'get_scan_results') {\n // Dashboard polls this to get results\n if ($state['command'] == 'SCAN_COMPLETE') {\n echo json_encode([\"success\" => true, \"results\" => $state['scan_results']]);\n \n // Reset command to IDLE after reading\n $state['command'] = 'IDLE';\n file_put_contents($stateFile, json_encode($state));\n } else {\n echo json_encode([\"success\" => false, \"message\" => \"Scanning in progress\"]);\n }\n exit;\n}\n\nif ($action == 'change_wifi') {\n $new_ssid = isset($_POST['ssid']) ? $_POST['ssid'] : '';\n $new_pass = isset($_POST['password']) ? $_POST['password'] : '';\n \n if (empty($new_ssid)) {\n echo json_encode([\"success\" => false, \"message\" => \"SSID cannot be empty\"]);\n exit;\n }\n\n $state['command'] = 'CHANGE';\n $state['ssid'] = $new_ssid;\n $state['password'] = $new_pass;\n $state['change_status'] = 'processing';\n $state['change_message'] = 'Menunggu ESP32 memproses pergantian WiFi...';\n file_put_contents($stateFile, json_encode($state));\n \n echo json_encode([\"success\" => true, \"message\" => \"Change command queued\"]);\n exit;\n}\n\nif ($action == 'get_status') {\n $time_diff = time() - $state['last_seen'];\n $is_online = $time_diff",
|
|
"Scan QR Code di bawah untuk mendaftar sidik jari via smartphone Anda.",
|
|
"— pindahkan ESP32 agar berada dalam jarak 5-10 meter dari router tanpa penghalang besar. Sinyal ideal adalah di atas -65 dBm.',\n '",
|
|
"`;}\n if(bt){const e=bt.extra_data||{};const pct=parseInt(e.charge_percent)||0;const bst=(e.bat_state||'standby').toUpperCase();const bcl=pct>50?'hi':pct>20?'mid':'low';const pcol=pct>50?'#059669':pct>20?'#d97706':'#dc2626';\n h+=`",
|
|
":root {\n --bg-color: #ffffff;\n --text-main: #202124;\n --text-muted: #5f6368;\n --btn-dark: #4b49ac;\n --btn-light: #f8f9fa;\n --border: #dadce0;\n }\n \n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: 'Google Sans', -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n background-color: var(--bg-color);\n color: var(--text-main);\n overflow-x: hidden;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n }\n\n /* Fixed Canvas Background */\n #particle-canvas {\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n z-index: -1;\n pointer-events: none;\n }\n\n /* Top Navbar matching the screenshot */\n .navbar-top {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 20px 40px;\n background: transparent;\n z-index: 100;\n position: relative;\n }\n\n .nav-left {\n display: flex;\n align-items: center;\n gap: 32px;\n }\n\n .brand-logo {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 20px;\n font-weight: 500;\n color: var(--text-main);\n text-decoration: none;\n letter-spacing: -0.5px;\n }\n \n .brand-logo img {\n height: 28px;\n width: auto;\n }\n\n .nav-menu {\n display: flex;\n gap: 24px;\n }\n\n .nav-menu a {\n text-decoration: none;\n color: var(--text-muted);\n font-size: 15px;\n font-weight: 400;\n transition: color 0.2s;\n }\n .nav-menu a:hover {\n color: var(--text-main);\n }\n .nav-menu a i {\n font-size: 12px;\n margin-left: 4px;\n }\n\n .nav-right a.btn-nav-download {\n background: var(--btn-dark);\n color: white;\n padding: 10px 20px;\n border-radius: 100px;\n text-decoration: none;\n font-size: 15px;\n font-weight: 500;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n }\n .nav-right a.btn-nav-download:hover {\n background: #4b49ac;\n }\n\n /* Hero Section */\n .hero {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 0 20px;\n position: relative;\n margin-top: -60px; /* Slight offset to center visually */\n }\n\n .hero-brand {\n display: flex;\n align-items: center;\n gap: 10px;\n font-size: 24px;\n color: var(--text-main);\n margin-bottom: 30px;\n font-weight: 400;\n }\n .hero-brand img {\n height: 32px;\n }\n\n .hero h1 {\n font-size: clamp(2.5rem, 4.5vw, 4.5rem);\n font-weight: 400;\n letter-spacing: -2.5px;\n line-height: 1.1;\n color: var(--text-main);\n max-width: 900px;\n margin-bottom: 40px;\n min-height: 140px; /* Prevent layout shift during typing */\n }\n\n .hero-actions {\n display: flex;\n gap: 16px;\n justify-content: center;\n }\n\n .btn-action {\n padding: 14px 28px;\n font-size: 16px;\n border-radius: 100px;\n text-decoration: none;\n font-weight: 500;\n transition: all 0.2s;\n display: inline-flex;\n align-items: center;\n gap: 10px;\n }\n\n .btn-dark {\n background: var(--btn-dark);\n color: white;\n border: 1px solid var(--btn-dark);\n }\n .btn-dark:hover {\n background: #4b49ac;\n border-color: #4b49ac;\n }\n\n .btn-light {\n background: var(--btn-light);\n color: var(--text-main);\n border: 1px solid var(--border);\n }\n .btn-light:hover {\n background: #f1f3f4;\n border-color: #d2d2d2;\n }\n\n /* Typing cursor - Blur/Glow Gradient (samar & lembut) */\n .cursor {\n display: inline-block;\n width: 5px;\n height: 1.05em;\n \n /* Gradasi halus — biru dominan 55%, lalu menyambung smooth */\n background: linear-gradient(\n to bottom, \n rgba(71, 71, 161, 0.45) 0%, \n rgba(71, 71, 161, 0.45) 55%, \n rgba(0, 128, 0, 0.45) 70%, \n rgba(255, 193, 0, 0.45) 85%, \n rgba(255, 0, 0, 0.45) 100%\n );\n \n vertical-align: middle;\n animation: cursorBlink 1.2s step-end infinite;\n \n /* Jarak antara teks dan garis ketikan */\n margin-left: 10px; \n margin-top: -6px;\n border-radius: 5px;\n \n /* Blur putih tipis */\n filter: \n drop-shadow(0 0 3px rgba(255, 255, 255, 0.8))\n blur(2.5px);\n \n /* Glow putih tipis */\n box-shadow: \n 0 0 5px 2px rgba(255, 255, 255, 0.7),\n 0 0 10px 4px rgba(255, 255, 255, 0.4);\n }\n \n @keyframes cursorBlink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0; }\n }\n\n @media (max-width: 768px) {\n .nav-menu { display: none; }\n .hero h1 { min-height: 180px; letter-spacing: -1px; }\n .hero-actions { flex-direction: column; }\n }",
|
|
"Status",
|
|
"Simpan'; showCustomAlert(\"Koneksi ke server gagal.\"); });\n});\n\n// Edit\ndocument.getElementById(\"formEditOccupant\").addEventListener(\"submit\", function(e) {\n e.preventDefault();\n// Removed edit password validation\n var btn = document.getElementById(\"btnEditOccupant\"); btn.disabled = true; btn.innerHTML = '",
|
|
"').removeClass('btn-light').addClass('btn-outline-secondary').attr('title', 'Select Multiple');\n $('#btnBulkDelete').hide();\n $('.btn-row-delete').css('display', 'inline-flex');\n $('.bulk-cb').hide().prop('checked', false);\n }\n });\n\n $('#btnBulkDelete').on('click', function() {\n const selected = $('.bulk-cb:checked').map(function() { return $(this).val(); }).get();\n if(selected.length === 0) {\n showCustomAlert('Pilih minimal satu data untuk dihapus!', 'warning');\n return;\n }\n window.bulkDeleteIds = selected;\n document.getElementById(\"delete_id_account\").value = \"BULK\"; \n document.getElementById(\"delete_account_name\").innerText = selected.length + \" data terpilih\"; \n $(\"#modalDeleteAccount\").modal(\"show\"); \n });\n\n $(\"#formTambahAccount\").submit(function(e) { \n e.preventDefault(); \n \n // Check face verification\n if (document.getElementById('faceVerifiedFlagAdd').value !== '1') {\n showCustomAlert('Upload foto profil dengan wajah yang terdeteksi terlebih dahulu.', 'warning');\n return;\n }\n \n var pw1 = document.getElementById(\"inputPasswordField\").value;\n var pw2 = document.getElementById(\"confirmPasswordField\").value;\n \n const pwValid = (pw1.length >= 8 && /[a-z]/.test(pw1) && /[A-Z]/.test(pw1) && /\\d/.test(pw1) && /[^A-Za-z0-9]/.test(pw1));\n if (!pwValid) {\n showCustomAlert(\"Password belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!\", \"warning\");\n document.getElementById(\"inputPasswordField\").focus();\n return;\n }\n \n if (pw1 !== pw2) {\n document.getElementById(\"passwordMismatch\").style.display = \"block\";\n return;\n } else {\n document.getElementById(\"passwordMismatch\").style.display = \"none\";\n }\n \n var btn = document.getElementById(\"btnSubmitAccount\"); \n btn.disabled = true; \n btn.innerHTML = '",
|
|
":root {\n --accent: #6366f1; --accent2: #818cf8; --accent-light: #e0e7ff;\n --green: #22c55e; --green-glow: rgba(34,197,94,0.2);\n --red: #ef4444; --amber: #f59e0b;\n --bg: #f8fafc; --bg2: #ffffff; --card: rgba(255,255,255,0.92);\n --text: #1e293b; --text2: #64748b; --text3: #94a3b8;\n --border: rgba(99,102,241,0.1); --shadow: rgba(0,0,0,0.06);\n }\n * { margin:0; padding:0; box-sizing:border-box; user-select:none; -webkit-user-select:none; }\n html, body {\n height:100%; width:100%; background:var(--bg);\n font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;\n overflow:hidden; display:flex; align-items:center; justify-content:center;\n -webkit-font-smoothing:antialiased;\n }\n .phone {\n width:100%; max-width:430px; height:100vh; max-height:100vh;\n background:var(--bg2); display:flex; flex-direction:column;\n position:relative; overflow:hidden;\n }\n @media (min-width:500px) {\n body { background:#f1f5f9; }\n .phone {\n height:93vh; max-height:850px; border-radius:36px;\n border:1px solid rgba(99,102,241,0.08);\n box-shadow: 0 25px 80px rgba(99,102,241,0.08), 0 8px 32px var(--shadow), 0 0 0 1px rgba(255,255,255,0.8) inset;\n }\n }\n /* HEADER */\n .header {\n display:flex; justify-content:space-between; align-items:center;\n padding:16px 20px; background:rgba(255,255,255,0.88);\n backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);\n z-index:30; position:absolute; top:0; left:0; right:0;\n border-bottom:1px solid rgba(0,0,0,0.04);\n }\n .header-left { display:flex; align-items:center; gap:10px; }\n .header-logo {\n width:36px; height:36px; border-radius:10px;\n background:linear-gradient(135deg,var(--accent),#7c3aed);\n display:flex; align-items:center; justify-content:center;\n box-shadow:0 2px 12px rgba(99,102,241,0.25);\n }\n .header-logo i { color:#fff; font-size:18px; }\n .header-text h1 { font-size:14px; font-weight:800; color:var(--text); letter-spacing:0.2px; line-height:1.2; }\n .header-text span { font-size:10px; font-weight:600; color:var(--text3); text-transform:uppercase; letter-spacing:1.2px; }\n .header-badge {\n font-size:10px; font-weight:700; color:var(--accent);\n background:var(--accent-light); padding:5px 12px; border-radius:8px;\n border:1px solid rgba(99,102,241,0.12);\n max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\n }\n /* SECURITY BAR */\n .security-bar {\n position:absolute; top:66px; left:0; right:0; z-index:25;\n padding:6px 16px; display:flex; align-items:center; gap:6px;\n background:rgba(255,255,255,0.85); backdrop-filter:blur(12px);\n border-bottom:1px solid rgba(0,0,0,0.03);\n }\n .sec-chip {\n display:inline-flex; align-items:center; gap:4px;\n padding:3px 8px; border-radius:6px; font-size:8.5px; font-weight:700;\n text-transform:uppercase; letter-spacing:0.5px;\n }\n .sec-chip.active { background:rgba(34,197,94,0.1); color:#16a34a; }\n .sec-chip.pending { background:rgba(245,158,11,0.1); color:#d97706; }\n .sec-chip i { font-size:10px; }\n /* PAGES */\n .page { flex:1; display:flex; flex-direction:column; }\n .page-hidden { display:none !important; }\n /* VALIDATING */\n .page-validating { align-items:center; justify-content:center; gap:20px; color:var(--text); }\n .val-ring {\n width:64px; height:64px; border-radius:50%;\n border:3px solid rgba(99,102,241,0.1); border-top-color:var(--accent);\n animation:spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform:rotate(360deg); } }\n .page-validating p { font-size:13px; color:var(--text3); font-weight:500; }\n /* ERROR */\n .page-error {\n align-items:center; justify-content:center;\n padding:40px 30px; text-align:center; gap:8px; color:var(--text);\n }\n .err-icon {\n width:88px; height:88px; border-radius:24px;\n display:flex; align-items:center; justify-content:center; margin-bottom:12px;\n }\n .err-icon.expired { background:rgba(245,158,11,0.08); border:2px solid rgba(245,158,11,0.12); }\n .err-icon.expired i { font-size:42px; color:var(--amber); }\n .err-icon.invalid { background:rgba(239,68,68,0.08); border:2px solid rgba(239,68,68,0.12); }\n .err-icon.invalid i { font-size:42px; color:var(--red); }\n .page-error h2 { font-size:22px; font-weight:800; color:var(--text); }\n .page-error p { font-size:13px; color:var(--text2); line-height:1.7; max-width:300px; }\n .btn-retry-lib {\n margin-top:18px; padding:12px 32px; border-radius:14px; border:none;\n background:linear-gradient(135deg,var(--accent),#7c3aed); color:#fff;\n font-size:13px; font-weight:700; cursor:pointer;\n box-shadow:0 4px 16px rgba(99,102,241,0.3); transition:all 0.25s;\n }\n .btn-retry-lib:hover { transform:translateY(-1px); box-shadow:0 6px 24px rgba(99,102,241,0.4); }\n /* CAMERA */\n .cam-area { flex:1; position:relative; overflow:hidden; background:#f1f5f9; }\n .cam-area video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }\n .cam-area canvas.overlay-cv { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }\n .face-oval {\n position:absolute; top:0; left:0; width:100%; height:100%;\n pointer-events:none; display:flex; align-items:center; justify-content:center;\n }\n .face-oval svg { width:68%; max-width:250px; height:auto; transition:0.5s ease; }\n .face-oval.ok #ovalRing { stroke:var(--green); stroke-width:3.5; stroke-dasharray:none; filter:drop-shadow(0 0 18px var(--green-glow)); }\n .corners {\n position:absolute; top:50%; left:50%; width:210px; height:270px;\n transform:translate(-50%,-50%); pointer-events:none;\n }\n .corners::before,.corners::after,.corners .c::before,.corners .c::after {\n content:''; position:absolute; width:30px; height:30px;\n border-color:rgba(255,255,255,0.5); border-style:solid; border-width:0;\n transition:all 0.4s ease;\n }\n .corners::before { top:0;left:0; border-top-width:3px;border-left-width:3px; border-radius:10px 0 0 0; }\n .corners::after { top:0;right:0; border-top-width:3px;border-right-width:3px; border-radius:0 10px 0 0; }\n .corners .c::before { bottom:0;left:0; border-bottom-width:3px;border-left-width:3px; border-radius:0 0 0 10px; }\n .corners .c::after { bottom:0;right:0; border-bottom-width:3px;border-right-width:3px; border-radius:0 0 10px 0; }\n .corners.ok::before,.corners.ok::after,.corners.ok .c::before,.corners.ok .c::after {\n border-color:var(--green); box-shadow:0 0 12px var(--green-glow);\n }\n .scanbar {\n position:absolute; left:50%; width:190px; height:2px; transform:translateX(-50%);\n background:linear-gradient(90deg,transparent,rgba(99,102,241,0.5),transparent);\n animation:scanY 2.8s ease-in-out infinite; pointer-events:none; opacity:0.6;\n }\n .scanbar.ok { background:linear-gradient(90deg,transparent,rgba(34,197,94,0.6),transparent); }\n @keyframes scanY { 0%,100%{top:calc(50% - 130px);} 50%{top:calc(50% + 130px);} }\n /* CAM LOADING */\n .cam-loading {\n position:absolute; inset:0; background:rgba(255,255,255,0.96);\n display:flex; flex-direction:column; align-items:center; justify-content:center;\n z-index:15; gap:14px; color:var(--text); backdrop-filter:blur(10px);\n }\n .cam-loading .ldring {\n width:52px; height:52px; border-radius:50%;\n border:3px solid rgba(99,102,241,0.1); border-top-color:var(--accent);\n animation:spin 0.7s linear infinite;\n }\n .cam-loading p { font-size:12px; color:var(--text3); font-weight:500; }\n .cam-loading .load-progress {\n width:160px; height:4px; border-radius:4px; background:rgba(99,102,241,0.08); overflow:hidden; margin-top:4px;\n }\n .cam-loading .load-progress-fill {\n height:100%; width:0%; border-radius:4px;\n background:linear-gradient(90deg,var(--accent),var(--accent2));\n transition:width 0.5s ease;\n }\n /* INSTRUCTIONS */\n .instructions {\n position:absolute; top:94px; left:50%; transform:translateX(-50%);\n background:rgba(255,255,255,0.92); backdrop-filter:blur(16px);\n border:1px solid rgba(0,0,0,0.05); border-radius:14px;\n padding:10px 16px; z-index:12; max-width:320px; width:90%;\n box-shadow:0 4px 20px var(--shadow);\n }\n .instr-title { font-size:10px; font-weight:800; color:var(--accent); text-transform:uppercase; letter-spacing:1.5px; margin-bottom:6px; }\n .instr-list { list-style:none; display:flex; flex-direction:column; gap:4px; }\n .instr-list li { font-size:10.5px; color:var(--text2); font-weight:500; display:flex; align-items:center; gap:6px; }\n .instr-list li i { font-size:12px; color:var(--accent); flex-shrink:0; }\n /* STATUS PILL */\n .st-pill {\n position:absolute; bottom:16px; left:50%; transform:translateX(-50%);\n display:flex; align-items:center; gap:8px; padding:10px 22px; border-radius:28px;\n font-size:12px; font-weight:700; z-index:12; white-space:nowrap;\n backdrop-filter:blur(20px); transition:all 0.4s ease;\n }\n .st-pill i { font-size:18px; }\n .st-pill.searching { background:rgba(255,255,255,0.9); color:var(--text2); border:1px solid rgba(0,0,0,0.06); box-shadow:0 4px 20px var(--shadow); }\n .st-pill.found { background:rgba(22,163,74,0.92); color:#fff; border:1px solid rgba(74,222,128,0.3); box-shadow:0 4px 28px rgba(22,163,74,0.2); }\n .st-pill.warn { background:rgba(220,38,38,0.92); color:#fff; border:1px solid rgba(248,113,113,0.25); box-shadow:0 4px 20px rgba(220,38,38,0.15); }\n /* BOTTOM BARS */\n .bar-capture {\n padding:22px 24px 30px; background:linear-gradient(0deg,rgba(255,255,255,0.99),rgba(255,255,255,0.85));\n display:flex; align-items:center; justify-content:center; position:relative; z-index:20;\n border-top:1px solid rgba(0,0,0,0.04);\n }\n .shutter {\n width:74px; height:74px; border-radius:50%;\n border:4px solid rgba(99,102,241,0.25); background:transparent; cursor:pointer;\n display:flex; align-items:center; justify-content:center;\n transition:all 0.25s ease; position:relative;\n }\n .shutter::after {\n content:''; width:58px; height:58px; border-radius:50%;\n background:linear-gradient(135deg,var(--accent),#7c3aed);\n transition:0.2s; box-shadow:0 4px 16px rgba(99,102,241,0.3);\n }\n .shutter:hover::after { transform:scale(0.93); }\n .shutter:active::after { transform:scale(0.86); }\n .shutter:disabled { opacity:0.25; cursor:not-allowed; pointer-events:none; }\n .shutter-lbl { font-size:9px; color:var(--text3); text-align:center; margin-top:8px; font-weight:800; letter-spacing:1px; text-transform:uppercase; }\n .bar-after {\n padding:22px 24px 30px; background:linear-gradient(0deg,rgba(255,255,255,0.99),rgba(255,255,255,0.85));\n align-items:center; justify-content:center; gap:28px; z-index:20; display:none;\n border-top:1px solid rgba(0,0,0,0.04);\n }\n .act-btn { display:flex; flex-direction:column; align-items:center; gap:6px; border:none; background:none; cursor:pointer; transition:0.2s; }\n .act-btn .ring { width:60px; height:60px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:24px; transition:all 0.25s ease; }\n .act-btn span { font-size:9px; font-weight:800; text-transform:uppercase; letter-spacing:0.8px; }\n .act-btn.retry .ring { background:rgba(99,102,241,0.06); color:var(--text2); border:1px solid rgba(0,0,0,0.06); }\n .act-btn.retry span { color:var(--text3); }\n .act-btn.retry:hover .ring { background:rgba(99,102,241,0.12); }\n .act-btn.save .ring { background:linear-gradient(135deg,#22c55e,#16a34a); color:#fff; box-shadow:0 4px 24px rgba(34,197,94,0.3); }\n .act-btn.save span { color:var(--green); }\n .act-btn.save:hover .ring { transform:scale(1.06); }\n /* PREVIEW */\n .preview { flex:1; position:relative; background:#f1f5f9; overflow:hidden; }\n .preview canvas { width:100%; height:100%; object-fit:cover; }\n .preview-tag {\n position:absolute; top:74px; left:50%; transform:translateX(-50%);\n background:rgba(22,163,74,0.92); color:#fff; padding:8px 22px; border-radius:28px;\n font-size:11px; font-weight:700; display:flex; align-items:center; gap:7px;\n backdrop-filter:blur(12px); z-index:5; box-shadow:0 4px 20px rgba(22,163,74,0.2);\n text-transform:uppercase; letter-spacing:0.5px;\n }\n .preview-tag i { font-size:16px; }\n .preview-watermark {\n position:absolute; bottom:12px; right:12px; font-size:8px; color:rgba(255,255,255,0.5);\n font-weight:600; z-index:5; text-align:right; line-height:1.4;\n }\n /* TOAST */\n .toast {\n position:absolute; bottom:96px; left:50%; transform:translateX(-50%);\n padding:12px 24px; border-radius:16px; font-size:12px; font-weight:700;\n display:none; align-items:center; gap:8px; z-index:25; white-space:nowrap; animation:slideUp 0.3s ease;\n }\n .toast i { font-size:17px; }\n .toast.ok { background:#dcfce7; color:#166534; border:1px solid #86efac; }\n .toast.err { background:#fee2e2; color:#991b1b; border:1px solid #fca5a5; }\n @keyframes slideUp { from{opacity:0;transform:translateX(-50%) translateY(12px);} to{opacity:1;transform:translateX(-50%) translateY(0);} }\n /* SUCCESS */\n .page-success { align-items:center; justify-content:center; padding:40px 30px; text-align:center; color:var(--text); gap:10px; }\n .suc-ring {\n width:100px; height:100px; border-radius:50%;\n background:rgba(34,197,94,0.08); border:2px solid rgba(34,197,94,0.15);\n display:flex; align-items:center; justify-content:center;\n animation:pulseGreen 2.5s ease-in-out infinite; margin-bottom:8px;\n }\n .suc-ring i { font-size:48px; color:var(--green); }\n @keyframes pulseGreen { 0%,100%{box-shadow:0 0 0 0 rgba(34,197,94,0.1);} 50%{box-shadow:0 0 0 20px rgba(34,197,94,0);} }\n .page-success h2 { font-size:22px; font-weight:900; color:var(--green); }\n .page-success p { font-size:13px; color:var(--text2); line-height:1.7; max-width:280px; }\n .suc-foto { width:110px; height:110px; border-radius:50%; object-fit:cover; border:3px solid var(--green); box-shadow:0 4px 28px rgba(34,197,94,0.15); margin-top:12px; }\n .suc-check { display:flex; align-items:center; gap:6px; font-size:11px; color:var(--text3); font-weight:600; margin-top:8px; }\n .suc-check i { color:var(--green); font-size:14px; }\n .suc-countdown { font-size:11px; color:var(--text3); margin-top:12px; font-weight:600; }\n /* BRIGHTNESS METER */\n .brightness-meter { position:absolute; top:192px; right:16px; display:flex; flex-direction:column; align-items:center; gap:4px; z-index:12; }\n .bm-bar { width:4px; height:60px; border-radius:4px; background:rgba(255,255,255,0.2); position:relative; overflow:hidden; }\n .bm-fill { position:absolute; bottom:0; left:0; width:100%; border-radius:4px; transition:height 0.5s ease,background 0.5s ease; }\n .bm-label { font-size:8px; font-weight:800; text-transform:uppercase; letter-spacing:1px; writing-mode:vertical-rl; text-orientation:mixed; color:rgba(255,255,255,0.5); }\n .bm-icon { font-size:14px; transition:color 0.3s; color:rgba(255,255,255,0.7); }\n /* LIVENESS METER */\n .liveness-meter { position:absolute; top:192px; left:16px; display:flex; flex-direction:column; align-items:center; gap:4px; z-index:12; }\n .lm-icon { font-size:14px; transition:color 0.3s; color:rgba(255,255,255,0.7); }\n .lm-bar { width:4px; height:60px; border-radius:4px; background:rgba(255,255,255,0.2); position:relative; overflow:hidden; }\n .lm-fill { position:absolute; bottom:0; left:0; width:100%; border-radius:4px; transition:height 0.5s ease,background 0.5s ease; }\n .lm-label { font-size:8px; font-weight:800; text-transform:uppercase; letter-spacing:1px; writing-mode:vertical-rl; text-orientation:mixed; color:rgba(255,255,255,0.5); }",
|
|
"Enter password",
|
|
"Tutup",
|
|
"Confirm Password Baru",
|
|
"';\n } else {\n var h='';\n evts.forEach(function(ev){\n var ts=ev.date||'';\n var jam=ts.length>=16?ts.substring(11,16):'00:00';\n h+='",
|
|
"');\n $('#fingerIcon').css('color', '#6366f1');\n $('#scanLine').css('background', '#6366f1').css('box-shadow', '0 0 8px #6366f1');\n\n verifyFingerprintServer(rawIdBase64);\n\n } catch (err) {\n showScanResult('error', 'Autentikasi Dibatalkan', err.message);\n }\n }\n\n function verifyFingerprintServer(credentialId) {\n $.ajax({\n url: 'verify_door_fingerprint.php',\n type: 'POST',\n dataType: 'json',\n data: { credential_id: credentialId },\n success: function(response) {\n if(response.status === 'success') {\n // Instead of showing unlocked directly, first alert the API\n setGlobalDoorUnlocked(response.name);\n } else {\n showScanResult('error', 'Akses Ditolak', response.message);\n }\n },\n error: function() {\n showScanResult('error', 'Koneksi Terputus', 'Gagal memverifikasi ke server.');\n }\n });\n }\n\n function showScanResult(type, title, message) {\n $('#scanLine').removeClass('scanning').css('opacity', '0');\n \n if (type === 'success') {\n $('#fingerIcon').css({ 'color': '#10b981', 'transform': 'scale(1.1)' }).removeClass('mdi-fingerprint').addClass('mdi-check-circle');\n $('#scanRing').css('border-color', 'rgba(16, 185, 129, 0.3)');\n $('#fingerStatusText').html('",
|
|
"Edit Address",
|
|
"Gagal memicu pemindaian.",
|
|
"Link (tautan) invalid atau sesi sudah berakhir. Silakan minta link verifikasi wajah terbaru dari Admin.",
|
|
"(function(){\n'use strict';\nconst API='api_monitoring.php',POLL=3000,MP=30;\nlet comps=[],busy=false,built=false;\nconst CH={},HD={};\n\nfunction tick(){const n=new Date();document.getElementById('clk').textContent=[n.getHours(),n.getMinutes(),n.getSeconds()].map(v=>String(v).padStart(2,'0')).join(':')}\nsetInterval(tick,1000);tick();\n\nasync function pull(){\n if(busy)return;busy=true;document.getElementById('bR').classList.add('spin');\n try{const r=await fetch(API+'?action=status'),j=await r.json();\n if(j.success){comps=j.data;uSum(j.summary);if(!built){buildAll();built=true;}else{updateAll();}cn(true);}\n }catch(e){cn(false);}\n busy=false;document.getElementById('bR').classList.remove('spin');\n}\nfunction uSum(s){$('sOn',s.online);$('sWr',s.warning);$('sEr',s.error);$('sOf',s.offline);}\nfunction $(id,v){document.getElementById(id).textContent=v;}\nfunction cn(ok){document.getElementById('cB').className='mcon '+(ok?'on':'off');document.getElementById('cT').textContent=ok?'Connected':'Offline';}\nfunction el(id){return document.getElementById(id);}\nfunction F(k){return comps.find(c=>c.component_key===k);}\nfunction E(c,k){return(c&&c.extra_data&&c.extra_data[k])||'0';}\nfunction tA(d){if(!d)return'Never';const s=Math.floor((Date.now()-new Date(d).getTime())/1000);if(s",
|
|
"Periksa ejaan nama SSID",
|
|
"'+tA(c.last_seen);\n}\n\n// ACCESS\nfunction bAcc(){\n const fp=F('fingerprint'),so=F('solenoid_lock'),rl=F('relay');let h='';\n if(fp){const e=fp.extra_data||{};\n h+=`",
|
|
"— tekan tombol Scan dan pilih jaringan WiFi target kembali, pastikan jaringan masih aktif dan terdeteksi.',\n '",
|
|
"PIN (6 Digit)",
|
|
"Verifikasi PIN",
|
|
"Email",
|
|
"\");\n}\n\n// Validasi Token\n$manifestPath = __DIR__ . '/manifest_edit_tokens.json';\nif (!file_exists($manifestPath)) {\n die(\"",
|
|
"Pastikan cahaya terang",
|
|
"Periksa MAC filtering pada router",
|
|
"Silakan scan QR code yang valid dari admin untuk melakukan registrasi dan verifikasi wajah.",
|
|
"mengingat PIN",
|
|
"'error', 'message' => 'Sesi tidak valid. Silakan login ulang.']);\n exit;\n}\n\nif ($_SERVER['REQUEST_METHOD'] !== 'POST') {\n echo json_encode(['status' => 'error', 'message' => 'Method tidak diizinkan.']);\n exit;\n}\n\n$id_account = $_SESSION['user_id'];\n$form_type = $_POST['form_type'] ?? '';\n\n// Ambil data existing dulu agar tidak ter-overwrite dengan kosong\n$existing = $pdo->prepare(\"SELECT * FROM profile WHERE id_account = ?\");\n$existing->execute([$id_account]);\n$old = $existing->fetch(PDO::FETCH_ASSOC);\n\n// Merge: pakai POST value jika ada & tidak kosong, otherwise pakai existing\n$email = trim($_POST['email'] ?? ($old['email'] ?? ''));\n$moto = trim($_POST['moto'] ?? ($old['moto'] ?? ''));\n$provinsi = trim($_POST['provinsi'] ?? ($old['provinsi'] ?? ''));\n$kabupaten = trim($_POST['kabupaten'] ?? ($old['kabupaten'] ?? ''));\n$kecamatan = trim($_POST['kecamatan'] ?? ($old['kecamatan'] ?? ''));\n$desa = trim($_POST['desa'] ?? ($old['desa'] ?? ''));\n$kode_pos = trim($_POST['kode_pos'] ?? ($old['kode_pos'] ?? ''));\n$facebook = trim($_POST['facebook'] ?? ($old['facebook'] ?? ''));\n$instagram = trim($_POST['instagram'] ?? ($old['instagram'] ?? ''));\n$linkedin = trim($_POST['linkedin'] ?? ($old['linkedin'] ?? ''));\n$whatsapp = trim($_POST['whatsapp'] ?? ($old['whatsapp'] ?? ''));\n\n// Validasi berdasarkan form_type\nif ($form_type === 'social') {\n // Hanya validasi social media fields\n $required = [\n 'Facebook' => $facebook,\n 'Instagram' => $instagram,\n 'WhatsApp' => $whatsapp,\n ];\n foreach ($required as $label => $value) {\n if (empty($value)) {\n echo json_encode(['status' => 'error', 'message' => \"$label wajib diisi.\"]);\n exit;\n }\n }\n $waClean = preg_replace('/[^0-9]/', '', $whatsapp);\n if (strlen($waClean)",
|
|
"Data terverifikasi dan tersimpan",
|
|
"';\n el.classList.remove('msg-sending');\n }\n }\n },\n error: function() {\n let el = document.getElementById(tempId);\n if (el) {\n el.querySelector('.msg-status').innerHTML = '",
|
|
"`;}\n el('gPow').innerHTML=h;\n try{if(ps)initCh('cPS',ps);}catch(e){console.warn(e);}\n try{if(sd)initCh('cSD',sd);}catch(e){console.warn(e);}\n try{if(bt)initCh('cBT',bt);}catch(e){console.warn(e);}\n}\nfunction uPow(){\n const ps=F('power_supply'),sd=F('step_down'),bt=F('battery');\n if(ps){const e=ps.extra_data||{};el('pV').textContent=fm(e.voltage_out)+' V';el('pA').textContent=fm(e.current_out)+' A';el('pW').textContent=fm(e.power_watts)+' W';el('info-ps').innerHTML=mr('mdi-flash','V In',fm(e.voltage_in)+' V','o')+mr('mdi-flash','V Out',fm(e.voltage_out)+' V','o');el('bar-ps').className='cd-top '+sc(ps.status);el('ft-ps').innerHTML='",
|
|
"' + (d.message || 'Gagal menyimpan ke server');\n errorEl.style.display = \"block\";\n btn.disabled = false;\n }\n }).catch(e => {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
|
|
"Ya, Keluar",
|
|
"Save Data",
|
|
"`;}\n if(lg){const e=lg.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';\n h+=`",
|
|
"Monitor dan kelola sistem IoT Smart Door Anda dari sini.",
|
|
"Sidik jari berhasil ditambahkan.",
|
|
"Anda dapat menutup halaman ini.",
|
|
"Upload foto profil dengan wajah yang terdeteksi terlebih dahulu.",
|
|
".auth .brand-logo img { width: 140px; max-width: 100%; }\n .auth-form-light { border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }\n .custom-alert {\n position: fixed; top: 20px; right: 20px; background: #fff;\n border-left: 4px solid #fcd34d; box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n padding: 15px 20px; border-radius: 4px; z-index: 9999;\n display: none; align-items: center; gap: 12px; min-width: 300px;\n max-width: 400px; animation: slideIn 0.3s ease-out;\n }\n .custom-alert.error { border-left-color: #ef4444; }\n .custom-alert.success { border-left-color: #10b981; }\n @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }\n @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }\n .custom-alert-icon { font-size: 24px; }\n .custom-alert.error .custom-alert-icon { color: #ef4444; }\n .custom-alert.success .custom-alert-icon { color: #10b981; }\n .custom-alert.warning .custom-alert-icon { color: #f59e0b; }\n .custom-alert-content { flex: 1; text-align: left; }\n .custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }\n .custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }\n .custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }\n .custom-alert-close:hover { color: #333; }\n .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; }\n .submit-btn {\n 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;\n }\n .info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n .info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n body, .container-scroller, .page-body-wrapper, .content-wrapper { background-color: #f8f9fa !important; }\n @media (max-width: 480px) {\n .custom-alert { top: 10px; right: 10px; left: 10px; min-width: unset; max-width: unset; }\n }\n .success-checkmark { text-align: center; padding: 30px 10px; animation: scaleIn 0.5s ease; }\n @keyframes scaleIn { 0% { transform: scale(0.5); opacity: 0; } 50% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }\n \n .foto-wrapper:hover .foto-circle { border-color: #4747a1 !important; }\n .foto-wrapper:active { transform: scale(0.95); }",
|
|
"let currentOccupantId = 0;\n\nfunction fetchContacts() {\n $.post('chat_api.php', { action: 'get_contacts' }, function(res) {\n if (res.status === 'success') {\n let html = '';\n res.data.forEach(c => {\n let isActive = c.id_occupant == currentOccupantId ? 'active' : '';\n let lastMsg = c.last_message ? c.last_message : 'Belum ada pesan';\n let unread = c.unread_count > 0 ? `",
|
|
"utama dalam semua state (apapun yang terjadi) */\n .sidebar .nav .nav-item,\n .sidebar .nav .nav-item.active,\n .sidebar .nav .nav-item:hover,\n .sidebar .nav .nav-item.hover-open {\n background: transparent !important;\n }\n \n /* Untuk Identia dark/light theme khusus */\n .sidebar-light-theme .sidebar .nav .nav-item,\n .sidebar-light-theme .sidebar .nav .nav-item.active {\n background: transparent !important;\n }\n \n /* 2. EKSTRIM: Pindahkan background biru HANYA ke kotakan tombol Menu Utama SAJA (saat Active / Open) */\n .sidebar .nav .nav-item.active > .nav-link,\n .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"] {\n background: transparent !important; \n }\n \n .sidebar .nav .nav-item.active,\n .sidebar .nav .nav-item.active[aria-expanded=\"true\"],\n .sidebar-light-theme .sidebar .nav .nav-item.active,\n .sidebar-light-theme .sidebar .nav .nav-item.active[aria-expanded=\"true\"] {\n background: transparent !important; \n }\n \n /* Aturan Utama! Background biru HANYA pada .nav-link, BUKAN pada .nav-item */\n .sidebar .nav .nav-item.active > .nav-link,\n .sidebar .nav .nav-item.active > .nav-link[aria-expanded=\"true\"],\n .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"],\n .sidebar-light-theme .sidebar .nav .nav-item.active > .nav-link,\n .sidebar-light-theme .sidebar .nav .nav-item.active > .nav-link[aria-expanded=\"true\"] {\n background: #4B49AC !important;\n margin-bottom: 0 !important;\n \n border-radius: 8px !important;\n \n overflow: hidden !important;\n box-shadow: none !important;\n border: none !important;\n clip-path: inset(0px round 8px) !important;\n }\n\n /* ========================================================= */\n /* FIX: Sidebar minimized (icon-only) mode */\n /* ========================================================= */\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link {\n display: block !important;\n text-align: center !important;\n padding-left: .5rem !important;\n padding-right: .5rem !important;\n position: static !important;\n }\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link i.menu-icon {\n display: block !important;\n margin: 0 auto !important;\n width: auto !important;\n min-width: auto !important;\n font-size: 18px !important;\n }\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-title,\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link .badge,\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link .menu-sub-title {\n display: none !important;\n }\n .sidebar-icon-only .sidebar .nav .nav-item .nav-link i.menu-arrow {\n display: none !important;\n }\n /* Active state: full width, no radius */\n .sidebar-icon-only .sidebar .nav .nav-item.active > .nav-link,\n .sidebar-icon-only .sidebar .nav .nav-item.active > .nav-link[aria-expanded=\"true\"],\n .sidebar-icon-only .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"] {\n border-radius: 0 !important;\n clip-path: none !important;\n }\n /* Hover: subtle background, no white text on transparent */\n .sidebar-icon-only .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link {\n background: #4B49AC !important;\n border-radius: 0 !important;\n clip-path: none !important;\n }\n .sidebar-icon-only .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link i.menu-icon {\n color: #fff !important;\n }\n /* Hover-open menu title popup */\n .sidebar-icon-only .sidebar .nav .nav-item.hover-open .nav-link .menu-title {\n display: flex !important;\n align-items: center !important;\n background: #4B49AC !important;\n color: #fff !important;\n padding: 0.5rem 1.4rem !important;\n left: 70px !important;\n position: absolute !important;\n text-align: left !important;\n top: 0 !important;\n bottom: 0 !important;\n width: 190px !important;\n z-index: 1 !important;\n line-height: 1.8 !important;\n border-radius: 0 5px 5px 0 !important;\n box-shadow: 4px 0px 7px 0px rgba(0,0,0,0.08) !important;\n }\n /* Hover-open submenu popup */\n .sidebar-icon-only .sidebar .nav .nav-item.hover-open .collapse,\n .sidebar-icon-only .sidebar .nav .nav-item.hover-open .collapsing {\n display: block !important;\n background: #fff !important;\n border-radius: 0 0 5px 0 !important;\n position: absolute !important;\n left: 70px !important;\n width: 190px !important;\n box-shadow: 4px 4px 7px 0px rgba(0,0,0,0.08) !important;\n }\n \n /* 3. Pastikan teks dan ikon Menu Utama tetap berwarna putih jika aktif */\n .sidebar .nav .nav-item.active > .nav-link .menu-title,\n .sidebar .nav .nav-item.active > .nav-link i.menu-icon,\n .sidebar .nav .nav-item.active > .nav-link i.menu-arrow,\n .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"] .menu-title,\n .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"] i.menu-icon,\n .sidebar .nav .nav-item > .nav-link[aria-expanded=\"true\"] i.menu-arrow {\n color: #fff !important;\n }\n \n /* 4. Reset background Container Sub-menu agar SELALU transparan di kondisi APAPUN */\n .sidebar .nav .nav-item .collapse,\n .sidebar .nav .nav-item .collapsing,\n .sidebar .nav .nav-item .sub-menu {\n background: transparent !important;\n }\n \n /* 5. Setel Teks Sub-menu kembali ke abu-abu gelap normal di semua kondisi */\n .sidebar .nav .nav-item .sub-menu .nav-item .nav-link,\n .sidebar .nav .nav-item .collapse .sub-menu .nav-item .nav-link {\n color: #6c7383 !important;\n background: transparent !important;\n font-weight: 400 !important;\n }\n \n /* Matikan gaya titik peluru list bawaan HTML */\n .sidebar .nav .sub-menu {\n list-style: none !important;\n }\n .sidebar .nav .sub-menu .nav-item {\n list-style-type: none !important;\n }\n .sidebar .nav .sub-menu .nav-item::before {\n display: none !important; \n content: \"\" !important;\n }\n \n /* 6. Matikan pewarnaan khusus peluru (.) bawaan .active agar kembali normal */\n .sidebar .nav .nav-item.active .sub-menu .nav-item .nav-link::before,\n .sidebar .nav .nav-item.active .collapse .sub-menu .nav-item .nav-link::before {\n content: \"\" !important; /* Hapus content kotak/icon jelek */\n color: #a3a4a5 !important;\n }\n /* Restorasi bentuk bulat bawaan Identia via ::before di tag \\",
|
|
"Token tidak ditemukan.",
|
|
"Menunggu Sensor...';\n statusEl.style.display = \"block\";\n \n if (!window.PublicKeyCredential) {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
|
|
"';\n res.data.forEach(d=>{opts+=`",
|
|
"Data Berhasil diperbarui!",
|
|
"Menghapus...');\n let promises = window.bulkDeleteIds.map(id => {\n let fd = new FormData();\n fd.append('id_occupant', id);\n return fetch('delete_occupant.php', { method: 'POST', body: fd }).then(r => r.json());\n });\n Promise.all(promises).then(() => {\n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n setTimeout(function(){ $(\"#modalDeleteOccupant\").modal(\"hide\"); location.reload(); }, 1200); \n });\n } else {\n $.post(\"delete_occupant.php\", $(this).serialize(), function(d) { \n if (d.status === \"success\") { \n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n $(\"#btnDeleteOccupant\").prop(\"disabled\",true); \n setTimeout(function(){ $(\"#modalDeleteOccupant\").modal(\"hide\"); location.reload(); }, 1200); \n } else { \n showCustomAlert(d.message); \n } \n }, \"json\"); \n }\n });\n // Start timer when modal opens\n $('#modalCodeRegister').on('shown.bs.modal', function(){ startCodeRegisterTimer(); });\n // Stop timer + reload when modal closes\n $('#modalCodeRegister').on('hidden.bs.modal', function(){ stopCodeRegisterTimer(); location.reload(); });\n $('#modalTambah').on('hidden.bs.modal', function(){ document.getElementById(\"formTambahOccupant\").reset(); document.getElementById(\"previewFoto\").src = \"images/faces/user.jpg\"; document.getElementById(\"passwordMismatch\").style.display = \"none\"; });\n $('#modalEdit').on('hidden.bs.modal', function(){ document.getElementById(\"editPasswordMismatch\").style.display = \"none\"; });\n $('#modalEditFoto').on('hidden.bs.modal', function(){ location.reload(); });\n\n // DataTable\n if ($.fn.DataTable.isDataTable('#myTable')) { $('#myTable').DataTable().destroy(); }\n var table = $('#myTable').DataTable({ \"columnDefs\": [{ \"orderable\": false, \"targets\": 5 }, { \"width\": \"40px\", \"targets\": 5 }], \"order\": [[0, 'asc']] });\n\n // Child row\n $('#myTable tbody').on('click', 'td.details-control', function(e) {\n if(e.target.tagName === 'INPUT') return; // Let checkbox work\n var tr = $(this).closest('tr'), row = table.row(tr);\n if (row.child.isShown()) { row.child.hide(); tr.removeClass('shown'); return; }\n table.rows('.shown').every(function() { this.child.hide(); $(this.node()).removeClass('shown'); });\n\n var d = { id: $(this).data(\"id\"), name: $(this).data(\"name\"), nik: $(this).data(\"nik\"), no_hp: $(this).data(\"no_hp\"), foto: $(this).data(\"foto\"), hasfoto: $(this).data(\"hasfoto\"), fingerid: $(this).data(\"fingerid\"), fingerok: $(this).data(\"fingerok\"), code: $(this).data(\"code\"), progress: $(this).data(\"progress\"), status: $(this).data(\"status\"), access: $(this).data(\"access\") };\n\n var progPill = d.progress === 'used' ? '",
|
|
"Ini bukanlah Bot otomatis yang bisa menjawab pesan secara otomatis",
|
|
"Upload Foto",
|
|
"— pastikan antena router menghadap vertikal (tegak). Untuk jangkauan multi-lantai, miringkan satu antena horizontal dan satu vertikal.',\n '",
|
|
"Coba dari perangkat lain",
|
|
"\" style=\"border-radius:6px; font-weight:bold; width:32px; padding: 4px 0; font-size:14px; display:inline-flex; align-items:center; justify-content:center;\" disabled title=\"Tekan untuk menyimpan perubahan\">",
|
|
"Gunakan sensor biometrik perangkat Anda saat ini.",
|
|
"Menyimpan...';\n \n fetch(\"edit_account.php\", { method: \"POST\", body: new FormData(this) })\n .then(function(r){ return r.json(); })\n .then(function(d) {\n btn.disabled = false; btn.innerHTML = '",
|
|
"Aktivitas sistem dan peringatan keamanan",
|
|
"15 detik namun tidak mendapat respons",
|
|
"Format email tidak valid';\n fb.className = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v) ? 'pf-fb valid' : 'pf-fb invalid';\n});\n\ndocument.getElementById('edit_whatsapp').addEventListener('blur', function() {\n const fb = document.getElementById('whatsappFeedback');\n const v = this.value.trim().replace(/[^0-9]/g, '');\n if (!v) { fb.className = 'pf-fb'; return; }\n fb.innerHTML = (v.length >= 10 && v.length",
|
|
"Foto wajah Anda telah berhasil tersimpan. Anda dapat menutup halaman ini.",
|
|
"(async function() {\n var MODEL_URLS = 'js/face-api/models';\n \n var pgCamera = document.getElementById('pgCamera');\n var pgSuccess = document.getElementById('pgSuccess');\n var camLoading = document.getElementById('camLoading');\n var loadProgress = document.getElementById('loadProgress');\n var loadText = document.getElementById('loadText');\n var stPill = document.getElementById('stPill');\n var stIcon = document.getElementById('stIcon');\n var stText = document.getElementById('stText');\n var vid = document.getElementById('vid');\n var ovCanvas = document.getElementById('ovCanvas');\n var refImg = document.getElementById('refImg');\n var oval = document.getElementById('oval');\n var distFill = document.getElementById('distFill');\n \n var brightMeter = document.getElementById('brightMeter');\n var bmFill = document.getElementById('bmFill');\n var bmIcon = document.getElementById('bmIcon');\n var liveMeter = document.getElementById('liveMeter');\n var lmFill = document.getElementById('lmFill');\n var lmIcon = document.getElementById('lmIcon');\n \n var refDescriptor = null;\n var stream = null;\n var detectionLoopInterval = null;\n var brightInterval = null;\n \n var livenessScore = 0;\n var livenessFrames = 0;\n var prevLandmarks = null;\n \n function setProgress(pct) { loadProgress.style.width = pct + '%'; }\n function setStatus(type, txt) {\n stPill.className = 'st-pill';\n if (type === 'search') { stPill.classList.add('searching'); stIcon.className = 'mdi mdi-face-recognition'; }\n if (type === 'ok') { stPill.classList.add('found'); stIcon.className = 'mdi mdi-check-circle'; }\n if (type === 'err') { stPill.classList.add('warn'); stIcon.className = 'mdi mdi-alert-circle'; }\n stText.textContent = txt;\n }\n\n try {\n setProgress(20);\n loadText.textContent = 'Memuat model AI...';\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URLS),\n faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URLS),\n faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URLS)\n ]);\n \n setProgress(50);\n loadText.textContent = 'Menghitung ciri wajah referensi...';\n \n // Wait for image to load before processing\n if (!refImg.complete || refImg.naturalWidth === 0) {\n await new Promise((resolve, reject) => {\n refImg.onload = resolve;\n refImg.onerror = () => reject(new Error(\"Failed memuat foto referensi dari database.\"));\n if (refImg.complete && refImg.naturalWidth === 0) {\n reject(new Error(\"Foto referensi rusak atau format tidak valid.\"));\n }\n });\n }\n \n // We relax the score threshold for the reference image in case of lighting/angle issues\n const refDetection = await faceapi.detectSingleFace(refImg, new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.1 })).withFaceLandmarks().withFaceDescriptor();\n if (!refDetection) {\n alert('Tidak dapat mendeteksi wajah pada foto referensi. Mohon perbarui foto occupant.');\n window.location.href = 'new_occupant.php';\n return;\n }\n refDescriptor = refDetection.descriptor;\n \n setProgress(70);\n loadText.textContent = 'Memulai kamera...';\n \n if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {\n throw new Error(\"Kamera tidak bisa diakses. Browser mungkin meminta HTTPS.\");\n }\n var isMobile = window.innerWidth",
|
|
"=this.history.length-1)return;this.historyIdx++;this.restoreHistory();}\n\n restoreHistory(){\n this.screens=JSON.parse(this.history[this.historyIdx]);\n if(this.currentScreen>=this.screens.length)this.currentScreen=0;\n this.selectedId=null;\n this.renderCanvas();\n this.renderScreenTabs();\n this.renderHierarchy();\n this.clearProps();\n }\n\n // ===== EXPORT JSON =====\n getDesignJSON(){\n return {projectName:document.getElementById('projectName').value,orientation:this.orientation,screens:this.screens};\n }\n\n // ===== API =====\n save(){\n const data=this.getDesignJSON();\n const fd=new FormData();\n fd.append('action','save');\n fd.append('id',this.designId||0);\n fd.append('name',data.projectName||'Untitled');\n fd.append('design_json',JSON.stringify(data));\n fetch('api_sync.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{\n if(res.status==='success'){this.designId=res.id;alert('✅ Saved!');this.loadList();}\n else alert('❌ '+res.message);\n });\n }\n\n loadList(){\n fetch('api_sync.php?action=list').then(r=>r.json()).then(res=>{\n if(res.status==='success'){\n let opts='",
|
|
"/* ── MANUAL STRIPES & BORDERS ── */\n #myTable { border-spacing: 0 12px !important; border-collapse: separate !important; }\n #myTable thead th { border-bottom: 1px solid #e2e8f0 !important; padding-bottom: 8px !important; }\n #myTable tbody tr td { border-bottom: none !important; padding-top: 6px !important; padding-bottom: 6px !important; }\n\n .pill { display: inline-flex; align-items: center; font-weight: 600; font-size: 11px; padding: 3px 12px; border-radius: 20px; }\n .pill-green { background: #e8f5e9; color: #2e7d32; }\n .pill-red { background: #ffebee; color: #c62828; }\n .pill-amber { background: #fff8e1; color: #f57f17; }\n .pill-gray { background: #f5f5f5; color: #757575; }\n\n .mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n .confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n .confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n .delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }\n .frm-lbl { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; }\n .pin-warning { display: flex; align-items: flex-start; gap: 8px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #92400e; }\n .pin-warning i { font-size: 18px; color: #f59e0b; flex-shrink: 0; margin-top: 1px; }\n\n .eye-toggle { cursor: pointer; background: #fff; border-left: none; }\n .eye-toggle:hover i { color: #4b7bec; }\n .info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; }\n .info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n .info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n\n /* Button Pulse Animation */\n @keyframes pulse-green {\n 0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); }\n 70% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); }\n 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }\n }\n .pulse-btn {\n animation: pulse-green 2s infinite;\n }\n\n /* Scanner Animation */\n @keyframes scan {\n 0% { top: 0; opacity: 0; }\n 10% { opacity: 1; }\n 90% { opacity: 1; }\n 100% { top: 100%; opacity: 0; }\n }\n .scanning {\n animation: scan 2s cubic-bezier(0.53, 0.21, 0.29, 0.67) infinite;\n }\n\n /* Modal Tweaks */\n .modal-backdrop.show {\n opacity: 0.6;\n backdrop-filter: blur(4px);\n }",
|
|
"Posisikan wajah dalam bingkai oval",
|
|
"const video = document.getElementById('camera-feed');\n const canvas = document.getElementById('photo-canvas');\n const overlay = document.getElementById('canvas-overlay');\n const btnCapture = document.getElementById('btn-capture');\n const btnRetake = document.getElementById('btn-retake');\n const locText = document.getElementById('loc-text');\n const timeText = document.getElementById('time-text');\n const photoData = document.getElementById('photo_data');\n const liveMetadata = document.getElementById('live-metadata');\n const scanLine = document.getElementById('scan-line');\n const faceBox = document.getElementById('face-box');\n const faceLabel = document.getElementById('face-label');\n\n let currentStream = null;\n let placeName = \"Lokasi belum didapat\";\n\n setTimeout(() => {\n faceLabel.innerText = \"Wajah Terdeteksi\";\n faceLabel.style.color = \"#fff\";\n faceLabel.style.background = \"#10b981\"; // success green\n faceBox.style.borderColor = \"#10b981\";\n }, 3000);\n\n async function startCamera() {\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n video: { facingMode: 'user' },\n audio: false\n });\n currentStream = stream;\n video.srcObject = stream;\n } catch (err) {\n locText.innerText = \"Kamera tidak tersedia.\";\n document.querySelector('.cam-loader').innerText = \"Kamera tidak aktif.\";\n scanLine.style.display = 'none';\n faceBox.style.display = 'none';\n }\n }\n\n function getLocation() {\n if (navigator.geolocation) {\n navigator.geolocation.getCurrentPosition(\n async (position) => {\n let lat = position.coords.latitude;\n let lon = position.coords.longitude;\n locText.innerText = `Memproses lokasi...`;\n try {\n let res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`);\n let data = await res.json();\n if (data && data.display_name) {\n placeName = data.display_name.split(\",\").slice(0, 3).join(\",\");\n locText.innerText = placeName;\n }\n } catch (e) {\n placeName = `GPS: ${lat.toFixed(4)}, ${lon.toFixed(4)}`;\n locText.innerText = placeName;\n }\n },\n (error) => {\n locText.innerText = \"Izin lokasi ditolak\";\n }\n );\n } else {\n locText.innerText = \"GPS tidak didukung\";\n }\n }\n\n function updateTime() {\n const now = new Date();\n const options = { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' };\n // avoid timezone text wrapping issues or missing\n timeText.innerText = now.toLocaleDateString('id-ID', options) + ' ' + now.toLocaleTimeString('id-ID');\n }\n\n btnCapture.addEventListener('click', () => {\n if (!video.videoWidth) return;\n\n canvas.width = video.videoWidth;\n canvas.height = video.videoHeight;\n const ctx = canvas.getContext('2d');\n \n ctx.translate(canvas.width, 0);\n ctx.scale(-1, 1);\n ctx.drawImage(video, 0, 0, canvas.width, canvas.height);\n \n ctx.translate(canvas.width, 0);\n ctx.scale(-1, 1);\n \n ctx.fillStyle = \"rgba(0, 0, 0, 0.5)\"; \n ctx.fillRect(0, canvas.height - 100, canvas.width, 100);\n \n let fontSize = Math.max(14, Math.floor(canvas.width * 0.035));\n ctx.font = `${fontSize}px Arial`;\n ctx.fillStyle = \"white\";\n ctx.textBaseline = \"middle\";\n \n const currentTime = new Date().toLocaleString('id-ID');\n ctx.fillText(`📍 ${placeName}`, 20, canvas.height - 70);\n ctx.fillText(`📅 ${currentTime}`, 20, canvas.height - 40);\n \n ctx.fillStyle = \"#34d399\";\n ctx.fillText(`Verified: ${document.getElementById('occupant_id').value}`, 20, canvas.height - 15);\n \n const dataURL = canvas.toDataURL('image/jpeg', 0.85);\n overlay.src = dataURL;\n overlay.style.display = 'block';\n \n video.style.display = 'none';\n btnCapture.style.display = 'none';\n liveMetadata.style.display = 'none';\n scanLine.style.display = 'none';\n faceBox.style.display = 'none';\n btnRetake.style.display = 'inline-block';\n \n photoData.value = dataURL;\n });\n\n btnRetake.addEventListener('click', () => {\n overlay.style.display = 'none';\n video.style.display = 'block';\n btnCapture.style.display = 'flex';\n liveMetadata.style.display = 'flex';\n scanLine.style.display = 'block';\n faceBox.style.display = 'flex';\n btnRetake.style.display = 'none';\n photoData.value = '';\n });\n\n document.getElementById('reportForm').addEventListener('submit', (e) => {\n if(!photoData.value) {\n alert('Silakan ambil foto kegiatan terlebih dahulu!');\n e.preventDefault();\n }\n });\n\n startCamera();\n getLocation();\n setInterval(updateTime, 1000);",
|
|
"Mendaftar sidik jari untuk login lebih cepat tanpa password.",
|
|
"// ── Full Camera System (registrasi_foto.php style) ──\n let editStream = null, editDetectLoop = null, editFaceOk = false, editDataURL = null;\n let editBrightInterval = null, editLivenessScore = 0;\n let editPrevLandmarks = null, editLivenessFrames = 0;\n let editModelsLoaded = false;\n\n const editVid = document.getElementById('editVid');\n const editOvCanvas = document.getElementById('editOvCanvas');\n const editOval = document.getElementById('editOval');\n const editCamLoading = document.getElementById('editCamLoading');\n const editStPill = document.getElementById('editStPill');\n const editStIcon = document.getElementById('editStIcon');\n const editStText = document.getElementById('editStText');\n const editBtnShutter = document.getElementById('editBtnShutter');\n const editCamArea = document.getElementById('editCamArea');\n const editPreviewWrap = document.getElementById('editPreviewWrap');\n const editCapCanvas = document.getElementById('editCapCanvas');\n const editBarAfter = document.getElementById('editBarAfter');\n const editBmFill = document.getElementById('editBmFill');\n const editBmIcon = document.getElementById('editBmIcon');\n const editLmFill = document.getElementById('editLmFill');\n const editLmIcon = document.getElementById('editLmIcon');\n const editLoadProgress = document.getElementById('editLoadProgress');\n const editLoadText = document.getElementById('editLoadText');\n\n function editSetProgress(pct) { editLoadProgress.style.width = Math.min(100, pct) + '%'; }\n function editSetStatus(type, txt) {\n editStPill.className = 'ecam-st-pill';\n if (type === 'search') { editStPill.classList.add('searching'); editStIcon.className = 'mdi mdi-face-recognition'; }\n if (type === 'ok') { editStPill.classList.add('found'); editStIcon.className = 'mdi mdi-check-circle'; }\n if (type === 'err') { editStPill.classList.add('warn'); editStIcon.className = 'mdi mdi-alert-circle'; }\n editStText.textContent = txt;\n }\n\n function startCamera() {\n document.getElementById('cameraOverlay').style.display = 'flex';\n editCamLoading.style.display = 'flex';\n editLoadText.textContent = 'Memuat kamera & model deteksi wajah...';\n editSetProgress(5);\n editCamArea.style.display = 'block';\n editPreviewWrap.style.display = 'none';\n editBarAfter.style.display = 'none';\n document.querySelector('.ecam-hud').style.display = 'flex';\n editFaceOk = false;\n editDataURL = null;\n editLivenessScore = 0;\n editLivenessFrames = 0;\n editPrevLandmarks = null;\n editLmFill.style.height = '0%';\n\n initEditCamera();\n }\n\n async function initEditCamera() {\n try {\n if (!editModelsLoaded) {\n editSetProgress(20);\n editLoadText.textContent = 'Memuat model deteksi wajah...';\n await Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models'),\n faceapi.nets.faceLandmark68Net.loadFromUri('js/face-api/models')\n ]);\n editModelsLoaded = true;\n }\n editSetProgress(50);\n editLoadText.textContent = 'Memulai kamera...';\n\n const isMobile = window.innerWidth",
|
|
"`;}\n if(bz){const e=bz.extra_data||{};const bp=e.buzzer_state==='beeping';\n h+=`",
|
|
"\"\n data-time=\"",
|
|
"Tambah Occupant",
|
|
"Catatan: Setelah berhasil, Anda dapat mengakses menu bantuan dengan mengetik /help. Menu tersebut menyediakan akses mandiri secara ringkas untuk:",
|
|
"Loginkan Username Anda",
|
|
"Password Baru (Opsional)",
|
|
"SET PIN",
|
|
"Sistem otomatis merekam kehadiran Anda. Silakan foto dan deskripsikan kegiatan Anda.",
|
|
"yang sah.\\n\\n\"\n . \"Saat ini, Anda sudah",
|
|
"Coba Lagi",
|
|
"Hapus Permanen?",
|
|
"Jangan bagikan password ini kepada siapapun!",
|
|
"Pilih Occupant",
|
|
"Pilih Bulan",
|
|
"QR hanya bisa digunakan",
|
|
"Klik ikon di atas untuk generate QR Code",
|
|
"Form Tidak Lengkap",
|
|
"LinkedIn belum diisi",
|
|
"'+tA(so.last_seen);}\n if(rl){const e=rl.extra_data||{};const isOn=e.relay_state==='on';const cir=e.circuit_connected==='yes';el('info-rl').innerHTML=`",
|
|
"\"\n data-foto=\"",
|
|
"';\n };\n xhr.send(fd);\n }\n\n // ── Events ──\n if (btnShutter) btnShutter.addEventListener('click', capture);\n if (btnRetake) btnRetake.addEventListener('click', retake);\n if (btnSave) btnSave.addEventListener('click', save);\n\n // ── Init ──\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', validateToken);\n } else {\n validateToken();\n }\n\n // ── PWA Service Worker ──\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('sw_foto.js').catch(function() {});\n }\n\n // ── Smooth instruction fade ──\n var ip = document.getElementById('instrPanel');\n if (ip) ip.style.transition = 'opacity 0.5s ease';\n})();",
|
|
"Verifikasi Berhasil!",
|
|
"Pesan ini akan dihapus dalam 5 detik...",
|
|
".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; }\n .edit-cam-overlay * { box-sizing:border-box; }\n .ecam-header { display:flex; justify-content:space-between; align-items:center; padding:20px; z-index:30; position:absolute; top:0; left:0; right:0; }\n .ecam-area { flex:1; position:relative; overflow:hidden; background:#1e2223; }\n .ecam-area video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }\n .ecam-area canvas.ecam-overlay-cv { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }\n .ecam-face-oval { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:10; }\n .ecam-face-oval svg { width:100%; height:100%; transition:0.4s ease; filter:drop-shadow(0 4px 12px rgba(0,0,0,0.3)); }\n .ecam-face-oval #editOvalRing { stroke:white; stroke-width:3; transition:all 0.4s ease; }\n .ecam-face-oval.ok #editOvalRing { stroke:#4747a1; stroke-width:5; filter:drop-shadow(0 0 12px rgba(71,71,161,0.4)); }\n .ecam-face-oval #editOvalRingOut { stroke:rgba(255,255,255,0.4); stroke-width:8; transition:all 0.4s ease; }\n .ecam-face-oval.ok #editOvalRingOut { stroke:rgba(71,71,161,0.4); }\n .ecam-loading { position:absolute; inset:0; background:rgba(255,255,255,0.95); backdrop-filter:blur(10px); display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:25; gap:20px; }\n .ecam-loading .ecam-ldring { width:48px; height:48px; border-radius:50%; border:3px solid #e6e6f2; border-top-color:#4747a1; animation:ecamSpin 0.8s cubic-bezier(0.4,0,0.2,1) infinite; }\n @keyframes ecamSpin { to { transform:rotate(360deg); } }\n .ecam-loading p { font-size:14px; color:#64748b; font-weight:600; text-align:center; }\n .ecam-load-progress { width:200px; height:6px; background:rgba(71,71,161,0.15); border-radius:4px; overflow:hidden; }\n .ecam-load-progress-fill { height:100%; width:0%; background:#4747a1; transition:width 0.4s ease; border-radius:4px; }\n .ecam-st-pill {\n position:absolute; top:80px; left:50%; transform:translateX(-50%);\n display:flex; align-items:center; gap:8px; padding:10px 20px; border-radius:30px;\n font-size:13px; font-weight:700; z-index:12; white-space:nowrap;\n background:rgba(255,255,255,0.85); border:1px solid rgba(255,255,255,0.5); color:#0f172a;\n backdrop-filter:blur(8px); transition:all 0.3s;\n }\n .ecam-st-pill i { font-size:18px; }\n .ecam-st-pill.searching { color:#64748b; }\n .ecam-st-pill.searching i { color:#4747a1; }\n .ecam-st-pill.found { background:#10b981; color:#fff; border-color:#10b981; }\n .ecam-st-pill.found i { color:#fff; }\n .ecam-st-pill.warn { background:#ef4444; color:#fff; border-color:#ef4444; }\n .ecam-st-pill.warn i { color:#fff; }\n .ecam-hud { position:absolute; bottom:0; left:0; right:0; z-index:20; display:flex; flex-direction:column; align-items:center; padding-bottom:30px; pointer-events:none; }\n .ecam-shutter { width:76px; height:76px; border-radius:50%; pointer-events:auto; border:4px solid rgba(255,255,255,0.4); background:rgba(255,255,255,0.1); cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all 0.3s; position:relative; backdrop-filter:blur(4px); }\n .ecam-shutter::after {\n content:''; width:56px; height:56px; border-radius:50%;\n background:#fff; transition:all 0.2s;\n }\n .ecam-shutter:active::after { transform:scale(0.8); background:#4747a1; }\n .ecam-shutter:disabled { opacity:0.6; cursor:not-allowed; pointer-events:none; }\n .ecam-preview { flex:1; position:relative; background:#1e2223; overflow:hidden; display:none; }\n .ecam-preview canvas { width:100%; height:100%; object-fit:cover; }\n .ecam-preview-tag { position:absolute; top:85px; left:50%; transform:translateX(-50%); background:#4747a1; color:#fff; padding:10px 20px; border-radius:30px; font-size:13px; font-weight:700; display:flex; align-items:center; gap:8px; z-index:5; box-shadow:0 4px 15px rgba(71,71,161,0.4); }\n .ecam-bar-after { position:absolute; bottom:0; left:0; right:0; z-index:40; padding:30px 0; background:rgba(255,255,255,0.95); backdrop-filter:blur(10px); display:none; align-items:center; justify-content:center; gap:40px; border-top:1px solid rgba(226,232,240,0.8); border-top-left-radius:24px; border-top-right-radius:24px; pointer-events:auto; }\n .ecam-act-btn { display:flex; flex-direction:column; align-items:center; gap:8px; border:none; background:none; cursor:pointer; transition:0.2s; pointer-events:auto; outline:none; }\n .ecam-act-btn .ring { width:56px; height:56px; border-radius:18px; display:flex; align-items:center; justify-content:center; font-size:24px; transition:all 0.2s; }\n .ecam-act-btn span { font-size:11px; font-weight:700; color:#0f172a; text-transform:uppercase; letter-spacing:0.5px; }\n .ecam-act-btn.retry .ring { background:#f1f5f9; color:#64748b; border:1px solid #e2e8f0; }\n .ecam-act-btn.save .ring { background:#4747a1; color:#fff; }\n .ecam-bm, .ecam-lm { position:absolute; top:50%; transform:translateY(-50%); display:flex; flex-direction:column; align-items:center; gap:12px; z-index:12; padding:14px 10px; background:none; border-radius:0; backdrop-filter:none; }\n .ecam-bm { right:6px; }\n .ecam-lm { left:6px; }\n .ecam-bm i, .ecam-lm i { font-size:16px; color:#fff; }\n .ecam-meter-bar { width:6px; height:100px; background:rgba(255,255,255,0.2); position:relative; overflow:hidden; border-radius:6px; }\n .ecam-meter-fill { position:absolute; bottom:0; left:0; width:100%; border-radius:6px; background:#fff; transition:height 0.4s cubic-bezier(0.4,0,0.2,1), background-color 0.4s ease; }",
|
|
"');\n $('#esp32-ssid').text('Unknown');\n $('#esp32-ip').text('Unknown');\n \n } else {\n // Offline\n isWifiConnected = false;\n $('#wifi-status-text').text('WiFi Disconnected').css('color', '#ffffff');\n $('#wifi-dot').css({'background': '#ef4444', 'box-shadow': 'none'});\n \n $('#esp32-status-icon-container').html('",
|
|
"Tandai Semua Dibaca",
|
|
"'+tA(bz.last_seen);}\n if(lg){const e=lg.extra_data||{};const on=e.led_state==='on';const bl=e.blink_mode==='blink';el('info-lg').innerHTML=`",
|
|
"VERIFY PIN",
|
|
"Pastikan pencahayaan cukup terang",
|
|
"Verifikasi Wajah — Identia",
|
|
"— jika SSID disembunyikan (hidden network) di pengaturan router, aktifkan broadcast SSID terlebih dahulu agar ESP32 dapat menemukannya.',\n '",
|
|
"Mencocokkan identitas wajah...",
|
|
"Berhasil",
|
|
"Gagal terkirim",
|
|
"Pilih Chat",
|
|
"\";\n \n // Meminta kontak dari user menggunakan ReplyKeyboardMarkup\n $keyboard = [\n 'keyboard' => [\n [\n ['text' => '📱 Bagikan Kontak Saya', 'request_contact' => true]\n ]\n ],\n 'resize_keyboard' => true,\n 'is_persistent' => true\n ];\n \n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'HTML',\n 'reply_markup' => json_encode($keyboard)\n ];\n \n file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n exit;\n }\n \n // Konfirmasi / Cek Nomor HP\n if (isset($update[\"message\"][\"contact\"])) {\n $phoneNumber = $update[\"message\"][\"contact\"][\"phone_number\"];\n \n // Normalisasi nomor HP (telegram biasa mengirim format internasional, misal 628... atau +628...\n // Kita ubah menjadi 08... menyesuaikan format lokal\n $normalizedPhone = preg_replace('/^\\+?62/', '0', $phoneNumber);\n \n // Cek database di tabel occupant\n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE no_hp = ? OR no_hp = ? OR no_hp = ?\");\n $stmt->execute([\n $phoneNumber, \n $normalizedPhone, \n \"62\" . ltrim($normalizedPhone, '0')\n ]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n // Nomor terdaftar, simpan chat_id ke database\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN telegram_id VARCHAR(100) NULL AFTER no_hp\"); } catch(PDOException $e) {}\n \n $updateStmt = $pdo->prepare(\"UPDATE occupant SET telegram_id = ? WHERE id_occupant = ?\");\n $updateStmt->execute([$chatId, $occupant['id_occupant']]);\n \n $text = \"✅",
|
|
"Silakan klik button",
|
|
"Simpan",
|
|
"';\n \n $.post('chat_api.php', { action: 'get_messages', id_occupant: id }, function(res) {\n if(res.status === 'success') {\n document.getElementById('activeName').innerText = res.occupant.name;\n let occStatStr = 'Status tidak diketahui';\n if (res.occupant.last_active) {\n let lastActStr = res.occupant.last_active.replace(/-/g, '/');\n let actDate = new Date(lastActStr);\n let diffSec = (new Date() - actDate) / 1000;\n let diffDays = diffSec / (60 * 60 * 24);\n \n if (diffSec",
|
|
"Kosongkan jika tidak ingin mengubah foto.",
|
|
"Cek profil",
|
|
".\\n\\n\"\n . \"Penting untuk diketahui:",
|
|
"7 days)\ntry {\n // Karena kita tidak punya timestamp khusus 'kapan dibaca', kita hitung dari waktu notifikasi dibuat (time) + sudah is_read\n $pdo->exec(\"UPDATE notifications SET is_deleted = 1, deleted_at = NOW() WHERE is_deleted = 0 AND is_read = 1 AND time",
|
|
"Menyimpan...';\n btn.disabled = true;\n document.getElementById('btnBack').disabled = true;\n\n const form = document.getElementById('editForm');\n const formData = new FormData(form);\n\n fetch('', {\n method: 'POST',\n body: formData\n })\n .then(response => response.json())\n .then(data => {\n if (data.status === 'success') {\n // Success\n document.getElementById('step2-foto').style.display = 'none';\n document.getElementById('step3-success').style.display = 'block';\n \n let countdown = 10;\n const cText = document.getElementById('countdownText');\n setInterval(() => {\n countdown--;\n cText.innerText = countdown;\n if(countdown",
|
|
"' + msg;\n toast.style.display = 'flex';\n }\n\n function setProgress(pct) {\n if (loadProgress) loadProgress.style.width = Math.min(100, pct) + '%';\n }\n\n\n\n // ── Anti-screenshot / right-click ──\n document.addEventListener('contextmenu', function(e) { e.preventDefault(); });\n\n // ── Step 1: Validate Token ──\n function validateToken() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', 'validate_foto_token.php?token=' + encodeURIComponent(TOKEN), true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.valid) {\n occupantId = d.id_occupant;\n document.getElementById('nameTag').textContent = d.name;\n showPage(pgCamera);\n initCamera();\n startTokenCheck();\n } else {\n document.getElementById('errTitle').textContent = 'Link Invalid';\n document.getElementById('errMsg').textContent = \n (d.message && d.message.indexOf('digunakan') !== -1) ? 'Link tautan ini sudah pernah digunakan atau kadaluwarsa. Silakan minta link terbarunya.' : 'Link tidak valid atau sesi berakhir.';\n showPage(pgError);\n }\n } catch(e) {\n document.getElementById('errTitle').textContent = 'Kesalahan';\n document.getElementById('errMsg').textContent = 'Format respons tidak valid.';\n showPage(pgError);\n }\n };\n xhr.onerror = function() {\n document.getElementById('errTitle').textContent = 'Koneksi Failed';\n document.getElementById('errMsg').textContent = 'Tidak dapat terhubung ke server.';\n showPage(pgError);\n };\n xhr.send();\n }\n\n // ── Periodic token validity check ──\n function startTokenCheck() {\n tokenCheckInterval = setInterval(function() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', 'validate_foto_token.php?token=' + encodeURIComponent(TOKEN), true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (false && !sessionSaved) {\n clearInterval(tokenCheckInterval);\n stopEverything();\n document.getElementById('errTitle').textContent = 'Token Kadaluwarsa';\n document.getElementById('errMsg').textContent = d.message || 'Token sudah tidak valid.';\n showPage(pgError);\n }\n } catch(e) {}\n };\n xhr.send();\n }, 30000);\n }\n\n function stopEverything() {\n clearInterval(detectLoop);\n clearInterval(brightInterval);\n if (stream) stream.getTracks().forEach(function(t) { t.stop(); });\n }\n\n // ── Step 2: Load models + camera ──\n var modelLoaded = false, landmarkLoaded = false, cameraStarted = false;\n\n function initCamera() {\n setProgress(5);\n loadText.textContent = 'Memuat library deteksi wajah...';\n\n // face-api.js loaded locally via",
|
|
"Hanya 1 wajah saja",
|
|
"');\n formData.append('credential_id', credId);\n\n const res = await fetch('save_fingerprint_account.php', {\n method: 'POST',\n body: formData\n });\n const result = await res.json();\n\n if (result.status === 'success') {\n showCustomAlert(\"Berhasil\", \"Sidik jari berhasil didaftarkan. Anda dapat menggunakannya untuk login.\", \"success\");\n setTimeout(() => { window.location.href = 'login.php'; }, 2000);\n } else {\n showCustomAlert(\"Gagal\", \"Gagal dari server: \" + result.message, \"error\");\n }\n }\n } catch(err) {\n console.error(err);\n showCustomAlert(\"Dibatalkan\", \"Proses dibatalkan atau sidik jari gagal dibaca.\", \"warning\");\n }\n });",
|
|
"Masukkan PIN 6 digit untuk verifikasi akses.",
|
|
"Menyimpan...';\n fetch(\"tambah_occupant.php\", { method: \"POST\", body: new FormData(this) }).then(function(r){ return r.json(); }).then(function(d) {\n btn.disabled = false; btn.innerHTML = '",
|
|
"\";\n }\n }\n return \"[Attachment Failed]\";\n}\n\n// Hapus Webhook jika sebelumnya pernah diset (Webhook dan Polling tidak bisa bersamaan)\nrequest($website . \"/deleteWebhook\");\n\n$offset = 0;\n\nwhile (true) {\n // Ambil data (Long Polling)\n $response = request($website . \"/getUpdates\", [\n 'offset' => $offset,\n 'timeout' => 20 // Tahan koneksi sampai 20 detik jika tak ada pesan baru\n ]);\n\n if (!empty($response['ok']) && !empty($response['result'])) {\n foreach ($response['result'] as $update) {\n $offset = $update['update_id'] + 1; // Update offset agar pesan ini tidak ditarik lagi\n \n if (isset($update[\"message\"])) {\n $chatId = $update[\"message\"][\"chat\"][\"id\"];\n $username = isset($update[\"message\"][\"from\"][\"first_name\"]) ? $update[\"message\"][\"from\"][\"first_name\"] : \"User\";\n \n echo \"[\".date('Y-m-d H:i:s').\"] Pesan dari $username ($chatId): \";\n \n // Command /start\n if (isset($update[\"message\"][\"text\"]) && $update[\"message\"][\"text\"] === \"/start\") {\n echo \"/start\\n\";\n $text = \"Halo! Saya adalah *Identia*.\\n\\n\"\n . \"Penting untuk diketahui: *Ini bukanlah Bot otomatis yang bisa menjawab secara otomatis*. Ini adalah *saluran interaksi langsung* untuk terhubung secara personal dengan Admin Identia.\\n\\n\"\n . \"Namun, untuk memulai obrolan langsung dengan Admin, kami perlu memverifikasi data Anda terlebih dahulu.\\n\\n\"\n . \"👇 *Silakan klik tombol di bawah ini untuk Membagikan Kontak Anda* agar kami dapat mencocokkan nomor handphone Anda dengan sistem kami.\";\n \n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'Markdown',\n 'reply_markup' => [\n 'keyboard' => [\n [ ['text' => '📱 Bagikan Kontak Saya', 'request_contact' => true] ]\n ],\n 'resize_keyboard' => true,\n 'one_time_keyboard' => true\n ]\n ];\n request($website.\"/sendMessage\", $postData);\n continue; // lanjut ke update berikutnya\n }\n \n // Konfirmasi / Cek Nomor HP\n if (isset($update[\"message\"][\"contact\"])) {\n $phoneNumber = $update[\"message\"][\"contact\"][\"phone_number\"];\n echo \"[Share Contact: $phoneNumber]\\n\";\n \n $normalizedPhone = preg_replace('/^\\+?62/', '0', $phoneNumber);\n \n // Cek database di tabel occupant\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN telegram_id VARCHAR(100) NULL AFTER no_hp\"); } catch(PDOException $e) {}\n \n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE no_hp = ? OR no_hp = ? OR no_hp = ?\");\n $stmt->execute([\n $phoneNumber, \n $normalizedPhone, \n \"62\" . ltrim($normalizedPhone, '0')\n ]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n $updateStmt = $pdo->prepare(\"UPDATE occupant SET telegram_id = ? WHERE id_occupant = ?\");\n $updateStmt->execute([$chatId, $occupant['id_occupant']]);\n \n $text = \"✅ *Verifikasi Berhasil!*\\n\\n\"\n . \"Nomor Anda telah terverifikasi sebagai Penghuni *Identia* yang sah.\\n\\n\"\n . \"Saat ini, Anda sudah *Terhubung Langsung* dengan saluran pribadi Admin *Identia*.\\n\\n\"\n . \"Silakan sampaikan pertanyaan, laporan kendala, atau pesan Anda di sini kapan pun Anda butuhkan. Admin akan membalas pesan Anda dari Control Panel sesegera mungkin.\";\n\n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'Markdown',\n 'reply_markup' => ['remove_keyboard' => true]\n ];\n request($website.\"/sendMessage\", $postData);\n } else {\n $postData = [\n 'chat_id' => $chatId,\n 'text' => \"❌ Maaf, nomor handphone Anda ($phoneNumber) tidak ditemukan pada daftar penghuni Identia.\\n\\nPastikan Anda mendaftar melalui Admin terlebih dahulu.\"\n ];\n request($website.\"/sendMessage\", $postData);\n }\n continue;\n }\n \n // Handle Teks Biasa (Occupant membalas/chat ke Admin)\n if (isset($update[\"message\"])) {\n $msgObj = $update[\"message\"];\n $textMessage = \"\";\n $captionStr = isset($msgObj[\"caption\"]) ? htmlspecialchars($msgObj[\"caption\"]) : \"\";\n \n if (isset($msgObj[\"text\"])) {\n $textMessage = htmlspecialchars($msgObj[\"text\"]);\n echo \"Teks: $textMessage\\n\";\n } else {\n // Kalau bukan text biasa, kita abaikan saja agar tidak tersimpan di database\n continue; \n }\n \n if (!empty($textMessage)) {\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN telegram_id VARCHAR(100) NULL AFTER no_hp\"); } catch(PDOException $e) {}\n \n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE telegram_id = ?\");\n $stmt->execute([$chatId]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n $id_account = 1; \n try { $pdo->exec(\"ALTER TABLE chats ADD COLUMN reply_to_chat_id INT NULL AFTER telegram_msg_id\"); } catch(PDOException $e) {}\n \n $replyToId = null;\n if (isset($msgObj['reply_to_message']) && isset($msgObj['reply_to_message']['message_id'])) {\n $tgReplyId = $msgObj['reply_to_message']['message_id'];\n // Temukan chat lokal berdasarkan telegram_msg_id\n $qRep = $pdo->prepare(\"SELECT id_chat FROM chats WHERE telegram_msg_id = ? LIMIT 1\");\n $qRep->execute([$tgReplyId]);\n $foundRep = $qRep->fetch();\n if ($foundRep) $replyToId = $foundRep['id_chat'];\n }\n \n $insertStmt = $pdo->prepare(\"INSERT INTO chats (id_account, id_occupant, sender, message, is_read, telegram_msg_id, reply_to_chat_id) VALUES (?, ?, 'occupant', ?, 0, ?, ?)\");\n $insertStmt->execute([$id_account, $occupant['id_occupant'], $textMessage, $msgObj['message_id'], $replyToId]);\n \n // ALL PREVIOUS MESSAGES TO THIS USER ARE NOW READ\n try {\n $pdo->exec(\"UPDATE chats SET is_read = 1 WHERE id_occupant = {$occupant['id_occupant']} AND sender = 'account'\");\n } catch(PDOException $e) {}\n \n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN last_active DATETIME NULL\"); } catch(PDOException $e) {}\n $updateLastActive = $pdo->prepare(\"UPDATE occupant SET last_active = NOW() WHERE id_occupant = ?\");\n $updateLastActive->execute([$occupant['id_occupant']]);\n \n } else {\n $postData = [\n 'chat_id' => $chatId,\n 'text' => \"Sesi chat Anda belum terhubung dengan Identia.\\n\\nSilakan kirim perintah /start lalu klik tombol 'Bagikan Kontak Saya' terlebih dahulu untuk memulai obrolan dengan Admin.\"\n ];\n request($website.\"/sendMessage\", $postData);\n }\n }\n }\n }\n \n // Handle Pesan Diedit (Occupant mengedit pesan di Telegram)\n if (isset($update[\"edited_message\"])) {\n $chatId = $update[\"edited_message\"][\"chat\"][\"id\"];\n \n if (isset($update[\"edited_message\"][\"text\"])) {\n $editedText = $update[\"edited_message\"][\"text\"];\n $messageId = $update[\"edited_message\"][\"message_id\"];\n \n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE telegram_id = ?\");\n $stmt->execute([$chatId]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n $newText = htmlspecialchars($editedText) . \"",
|
|
"Verifikasi';\n btn.disabled = false;\n \n if (d.status === \"success\") {\n $(\"#modalVerifyPin\").modal(\"hide\");\n document.getElementById(\"revealedPassword\").value = d.password;\n $(\"#modalShowPassword\").modal(\"show\");\n } else {\n document.getElementById(\"verifyPinErrorMsg\").innerText = d.message || \"PIN salah!\";\n document.getElementById(\"verifyPinError\").style.display = \"block\";\n }\n }, \"json\").fail(function() {\n btn.innerHTML = '",
|
|
"Edit Personal Information",
|
|
"*Minimal 8 karakter, huruf besar, huruf kecil, angka & simbol.",
|
|
"Pantau status perangkat secara real-time,",
|
|
"`;}\n}\n\n// POWER\nfunction bPow(){\n const ps=F('power_supply'),sd=F('step_down'),bt=F('battery');let h='';\n if(ps){const e=ps.extra_data||{};\n h+=`",
|
|
"Tidak ada kontak.",
|
|
"Berhasil Terhubung!",
|
|
"QR Registrasi Foto",
|
|
"— router yang sudah berjalan lama mungkin mengalami memory leak atau channel congestion. Restart bisa menyelesaikan masalah ini.',\n '",
|
|
"— jika ESP32 harus ditempatkan jauh dari router, pasang repeater WiFi di titik tengah untuk memperkuat jangkauan sinyal.',\n '",
|
|
"document.getElementById('registerFinger').addEventListener('click', async () => {\n try {\n if (!window.PublicKeyCredential) {\n showCustomAlert(\"Tidak Didukung\", \"Browser ini tidak mendukung WebAuthn (Passkey / Sidik Jari).\", \"error\");\n return;\n }\n\n const publicKey = {\n challenge: new Uint8Array(32),\n rp: { \n name: \"Identia App\",\n id: window.location.hostname \n },\n user: {\n id: Uint8Array.from(\"uid_",
|
|
"exec(\"ALTER TABLE history \n ADD COLUMN is_read TINYINT(1) DEFAULT 0,\n ADD COLUMN is_starred TINYINT(1) DEFAULT 0,\n ADD COLUMN is_deleted TINYINT(1) DEFAULT 0\");\n} catch (PDOException $e) {\n // Abaikan jika kolom sudah ada\n}\n\n// Konfigurasi Folder & Pagination\n$folder = isset($_GET['folder']) ? $_GET['folder'] : 'inbox'; // inbox, starred, trash\n$limit = 20; \n$page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;\nif ($page",
|
|
"Akun:",
|
|
"Loginkan 6 Digit PIN dari akun",
|
|
"/* ── MODALS ── */\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n\n.pf-avatar {\n width: 64px; height: 64px; border-radius: 50%; overflow: hidden;\n border: 2px solid #e5e9f0; flex-shrink: 0;\n}\n.pf-avatar img { width: 100%; height: 100%; object-fit: cover; }\n\n.pf-soc-icon {\n width: 34px; height: 34px; border-radius: 50%; border: 1px solid #d4d4d8;\n display: inline-flex; align-items: center; justify-content: center;\n color: #333; font-size: 14px; text-decoration: none; transition: all .2s;\n background: #fff;\n}\n.pf-soc-icon:hover:not(.disabled) {\n background: #4B49AC; border-color: #4B49AC; color: #fff; text-decoration: none;\n}\n.pf-soc-icon.disabled {\n background: #f5f5f5; border-color: #e5e9f0; color: #ccc;\n cursor: not-allowed; pointer-events: none; opacity: 0.6;\n}\n\n.pf-edit-btn {\n font-size: 13px; font-weight: 500; color: #333; background: #fff;\n border: 1px solid #d4d4d8; border-radius: 6px; padding: 6px 16px;\n cursor: pointer; transition: all .15s; display: inline-flex; align-items: center;\n}\n.pf-edit-btn:hover { background: #f5f5f5; border-color: #bbb; }\n\n.pf-lbl {\n display: block; font-size: 12px; font-weight: 500; color: #4B49AC;\n margin-bottom: 4px;\n}\n.pf-val {\n font-size: 14px; font-weight: 400; color: #1a1a1a; margin-bottom: 0;\n}\n\n.pf-flbl {\n font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; display: block;\n}\n.pf-fb {\n font-size: 11px; margin-top: 3px; display: none; font-weight: 500;\n}\n.pf-fb.valid { display: block; color: #10b981; }\n.pf-fb.invalid { display: block; color: #ef4444; }\n\n/* Searchable Select */\n.ss-wrap { position: relative; }\n.ss-input { cursor: text; }\n.ss-input:disabled { background: #f8fafc; color: #aaa; cursor: not-allowed; }\n.ss-dropdown {\n display: none; position: absolute; top: 100%; left: 0; right: 0; z-index: 1060;\n background: #fff; border: 1px solid #e5e9f0; border-top: none; border-radius: 0 0 6px 6px;\n max-height: 200px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.08);\n}\n.ss-dropdown.show { display: block; }\n.ss-item {\n padding: 8px 14px; font-size: 13px; color: #333; cursor: pointer;\n border-bottom: 1px solid #f5f5f5; transition: background .1s;\n}\n.ss-item:last-child { border-bottom: none; }\n.ss-item:hover, .ss-item.active { background: #f0f4ff; color: #4B49AC; }\n.ss-empty { padding: 12px 14px; font-size: 12px; color: #aaa; text-align: center; }\n.ss-loading { padding: 12px 14px; font-size: 12px; color: #888; text-align: center; }\n.ss-loading::before {\n content: ''; display: inline-block; width: 14px; height: 14px;\n border: 2px solid #e5e9f0; border-top-color: #4B49AC; border-radius: 50%;\n animation: pfSpin .5s linear infinite; vertical-align: middle; margin-right: 6px;\n}\n@keyframes pfSpin { to { transform: rotate(360deg); } }",
|
|
")\"\n data-id=\"",
|
|
"Gagal Terhubung!",
|
|
"Jadwal kegiatan dan agenda mendatang,",
|
|
"Mencari wajah...",
|
|
"Pilih",
|
|
"Ambil Ulang",
|
|
"' + ssid);\n $('#saveStep4').fadeIn(300);\n \n // Perbarui kartu ESP32 di Dashboard\n $('#esp32-ssid').text(ssid);\n \n // Trigger Notification\n $.post('api_notifications.php', {\n action: 'create_notification',\n type: 'wifi_change',\n title: 'WiFi Terhubung',\n message: 'Berhasil berpindah ke WiFi: ' + ssid\n });\n }, 600);\n }\n }, 'json').fail(function() {\n // Jika API API PHP gak bisa diakses (misal server mati)\n if (elapsedWait >= maxTimeout) {\n clearInterval(changeCheckInterval);\n $('.save-step').hide();\n $('#saveStep2 .save-bar-fill').css({'width': '0%', 'transition': 'none'}); // Reset\n $('#saveErrorDisplay').text('Kesalahan koneksi ke server PHP API.');\n $('#wifiSaveModalDialog').removeClass('modal-sm').addClass('modal-md');\n $('#saveStep5').fadeIn(300);\n }\n });\n }, 2500); // Check every 2.5 seconds\n } else {\n alert(\"Gagal menyimpan: \" + res.message);\n }\n }, 'json');\n });\n\n // Close fullscreen save modal and refresh on OK\n $('#btnCloseSaveModal').on('click', function() {\n $('#wifiSaveModal').modal('hide');\n // location.reload(); // Optional: reload page to ensure clean state\n });\n \n // Retry button on failed modal\n $('#btnRetrySaveModal').on('click', function() {\n $('#wifiSaveModal').modal('hide');\n });\n\n // SIMULATE WIFI DISCONNECT (Toggle locally for test, but actual logic relies on polling below)\n var isWifiConnected = true;\n $('#wifi-status-text').closest('p').css('cursor', 'pointer').on('click', function() {\n // Toggle for visual testing ONLY\n $(this).toggleClass('manual-override');\n });\n\n // ===== Real-time ESP32 Status Polling =====\n function pollESP32Status() {\n $.post('api_wifi.php', { action: 'get_status' }, function(res) {\n \n if ($('#wifi-status-text').closest('p').hasClass('manual-override')) return; // skip if manually toggled\n \n if(res.status === 'online') {\n if(!isWifiConnected || $('#wifi-status-text').text() !== 'WiFi Connected') {\n isWifiConnected = true;\n $('#wifi-status-text').text('WiFi Connected').css('color', '#ffffff');\n $('#wifi-dot').css({'background': '#10b981', 'box-shadow': 'none'});\n }\n \n // Update RSSI Signal Strength Icon\n var rssi = parseInt(res.rssi) || -90;\n var signalPct = Math.min(Math.max(2 * (rssi + 100), 0), 100); \n var sigIcon = signalPct > 70 ? 'mdi-wifi-strength-4' : (signalPct > 40 ? 'mdi-wifi-strength-2' : 'mdi-wifi-strength-1');\n var sigColor = signalPct > 70 ? '#10b981' : (signalPct > 40 ? '#f59e0b' : '#ef4444');\n $('#esp32-status-icon-container').html('",
|
|
"SIMPAN KATA SANDI",
|
|
"Password does not meet criteria (min. 8 chars, uppercase, lowercase, numbers, and symbols)!",
|
|
"kekuatan sinyal terlalu lemah",
|
|
"Berhasil dihapus.",
|
|
"Semua sistem terpantau disini !",
|
|
"di sebelah nama SSID untuk mencari WiFi di sekitar ESP32.",
|
|
"agar sistem dapat memverifikasi identitas Anda terlebih dahulu sebelum fitur ini dapat digunakan.\";\n } else {\n $text = \"Sesi chat Anda belum terhubung dengan Identia.\\n\\n👇",
|
|
"70) {\n lmFill.style.background = '#22c55e';\n lmIcon.style.color = '#22c55e';\n updateSecChip(secLive, true);\n } else if (livenessScore > 30) {\n lmFill.style.background = '#f59e0b';\n lmIcon.style.color = '#f59e0b';\n } else {\n lmFill.style.background = '#ef4444';\n lmIcon.style.color = '#ef4444';\n }\n }\n\n if (livenessScore >= 50) {\n setStatus('ok', 'Wajah terdeteksi — Siap!');\n btnShutter.disabled = false;\n } else {\n setStatus('search', 'Mendeteksi liveness... Tetap diam');\n btnShutter.disabled = true;\n }\n }\n } else if (results.length > 1) {\n setFaceNotOk();\n setStatus('err', 'Terdeteksi lebih dari 1 wajah');\n updateSecChip(secFace, false);\n btnShutter.disabled = true;\n } else {\n setFaceNotOk();\n setStatus('search', 'Posisikan wajah Anda...');\n btnShutter.disabled = true;\n updateSecChip(secFace, false);\n }\n }).catch(function(){});\n }, 300);\n }\n\n function setFaceNotOk() {\n faceOk = false;\n oval.classList.remove('ok');\n corners.classList.remove('ok');\n scanbar.classList.remove('ok');\n btnShutter.disabled = true;\n }\n\n // ── Brightness Check ──\n function startBrightnessCheck() {\n var tmpCanvas = document.createElement('canvas');\n var tmpCtx = tmpCanvas.getContext('2d');\n brightInterval = setInterval(function() {\n if (vid.paused || vid.ended || !vid.videoWidth) return;\n tmpCanvas.width = 80; tmpCanvas.height = 60;\n tmpCtx.drawImage(vid, 0, 0, 80, 60);\n var data = tmpCtx.getImageData(0, 0, 80, 60).data;\n var sum = 0, count = data.length / 4;\n for (var i = 0; i",
|
|
"// Eye Toggle Logic\n var toggles = document.querySelectorAll('.eye-toggle');\n toggles.forEach(function(toggle) {\n toggle.addEventListener('click', function() {\n var targetId = this.getAttribute('data-target');\n var input = document.getElementById(targetId);\n var icon = this.querySelector('i');\n if (input.type === 'password') {\n input.type = 'text';\n icon.classList.remove('mdi-eye-off');\n icon.classList.add('mdi-eye');\n } else {\n input.type = 'password';\n icon.classList.remove('mdi-eye');\n icon.classList.add('mdi-eye-off');\n }\n });\n });\n\n // Username Verification Tooltip Logic\n const usernameInput = document.getElementById('usernameInput');\n const usernameTooltip = document.getElementById('usernameTooltip');\n const passwordInput = document.getElementById('loginPassword');\n const loginForm = document.getElementById('loginForm');\n let checkTimeout;\n\n function checkUsername() {\n const username = usernameInput.value.trim();\n if (username === '') {\n usernameTooltip.style.display = 'none';\n usernameInput.setCustomValidity(''); \n passwordInput.disabled = false;\n return;\n }\n\n const formData = new FormData();\n formData.append('username', username);\n\n fetch('check_username.php', {\n method: 'POST',\n body: formData\n })\n .then(res => res.json())\n .then(data => {\n if (data.status === 'available') {\n // Available berarti belum terdaftar (error untuk login)\n usernameTooltip.style.display = 'flex';\n usernameInput.setCustomValidity('USERNAME TIDAK TERSEDIA');\n passwordInput.value = '';\n passwordInput.disabled = true;\n } else if (data.status === 'taken') {\n // Taken berarti username terdaftar di DB (valid untuk login)\n usernameTooltip.style.display = 'none';\n usernameInput.setCustomValidity('');\n passwordInput.disabled = false;\n }\n })\n .catch(err => console.error(err));\n }\n\n if (usernameInput) {\n usernameInput.addEventListener('input', function() {\n usernameTooltip.style.display = 'none';\n clearTimeout(checkTimeout);\n checkTimeout = setTimeout(checkUsername, 500); \n });\n usernameInput.addEventListener('blur', checkUsername);\n usernameInput.addEventListener('focus', function() {\n usernameTooltip.style.display = 'none';\n });\n }\n \n // --- High-Fidelity 3D Sphere Particle System (Antigravity Replica) ---",
|
|
"/* ── MANUAL STRIPES & BORDERS ── */\n#myTable thead th { border-bottom: 1px solid #e2e8f0 !important; }\n#myTable tbody tr td { border-bottom: none !important; }\n\n.pill { display: inline-flex; align-items: center; font-weight: 600; font-size: 11px; padding: 3px 12px; border-radius: 20px; }\n.pill-green { background: #e8f5e9; color: #2e7d32; }\n.pill-red { background: #ffebee; color: #c62828; }\n.pill-amber { background: #fff8e1; color: #f57f17; }\n.pill-gray { background: #f5f5f5; color: #757575; }\n\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n.confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n.confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n.delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }\n.frm-lbl { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; }\n.pin-warning { display: flex; align-items: flex-start; gap: 8px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #92400e; }\n.pin-warning i { font-size: 18px; color: #f59e0b; flex-shrink: 0; margin-top: 1px; }\n\n.eye-toggle { cursor: pointer; background: #fff; border-left: none; }\n.eye-toggle:hover i { color: #4b7bec; }\n.info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; }\n.info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n.info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }",
|
|
"Your old password is shown below. Please enter and confirm your new password to update it.",
|
|
"').removeClass('btn-light').addClass('btn-outline-secondary').attr('title', 'Select Multiple');\n $('#btnBulkDelete').hide();\n $('.btn-row-delete').css('display', 'inline-flex');\n $('.bulk-cb').hide().prop('checked', false);\n }\n });\n\n $('#btnBulkDelete').on('click', function() {\n const selected = $('.bulk-cb:checked').map(function() { return $(this).val(); }).get();\n if(selected.length === 0) {\n showCustomAlert('Pilih minimal satu data untuk dihapus!', 'warning');\n return;\n }\n window.bulkDeleteIds = selected;\n document.getElementById(\"delete_id_occupant\").value = \"BULK\"; \n document.getElementById(\"delete_occupant_name\").innerText = selected.length + \" data terpilih\"; \n $(\"#modalDeleteOccupant\").modal(\"show\"); \n });\n});\n\ndocument.getElementById(\"formTambahOccupant\").addEventListener(\"submit\", function(e) {\n e.preventDefault();\n var btn = document.getElementById(\"btnSubmitOccupant\"); btn.disabled = true; btn.innerHTML = '",
|
|
"Konfirmasi password terbaru",
|
|
"\", 'parse_mode'=>'HTML', 'reply_markup' => json_encode($btn)];\n \n $res = file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n $resData = json_decode($res, true);\n if (isset($resData['result']['message_id'])) {\n $tkns[$token]['msg_id'] = $resData['result']['message_id'];\n $tkns[$token]['reply_markup'] = json_encode($btn);\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n \n // Trigger auto delete via background CLI process\n runBackground(__DIR__ . '/auto_delete.php', [$chatId, $resData['result']['message_id'], $token, 'pin', $origText]);\n } else {\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n }\n exit;\n }\n \n if ($textMessage === '/forgot_password') {\n $token = bin2hex(random_bytes(16));\n $manPath = __DIR__ . '/manifest_forgot_tokens.json';\n $tkns = file_exists($manPath) ? (json_decode(file_get_contents($manPath), true) ?: []) : [];\n // Delete previous message for same occupant\n foreach ($tkns as $oldTk => $oldData) {\n if (isset($oldData['id_occupant']) && $oldData['id_occupant'] == $id_occupant && isset($oldData['msg_id'])) {\n @file_get_contents($website.\"/deleteMessage?chat_id=$chatId&message_id={$oldData['msg_id']}\");\n unset($tkns[$oldTk]);\n }\n }\n $tkns[$token] = ['id_occupant'=>$id_occupant, 'created_at'=>time(), 'expires_at'=>time() + 300];\n \n $expireTime = date('H:i:s', time() + 300);\n $webAppUrl = $baseUrl . \"/forgot.php?token=\" . $token;\n $btn = ['inline_keyboard' => [[['text' => 'Verifikasi PIN 6-Digit', 'web_app' => ['url' => $webAppUrl]]]]];\n $origText = \"Akses panel pemulihan telah disiapkan.\";\n $postData = ['chat_id' => $chatId, 'text' => $origText . \"\\n⏳",
|
|
"\", 'parse_mode'=>'HTML', 'reply_markup' => json_encode($btn)];\n \n $res = file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n $resData = json_decode($res, true);\n if (isset($resData['result']['message_id'])) {\n $tkns[$token]['msg_id'] = $resData['result']['message_id'];\n $tkns[$token]['reply_markup'] = json_encode($btn);\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n \n // Trigger auto delete via background CLI process\n runBackground(__DIR__ . '/auto_delete.php', [$chatId, $resData['result']['message_id'], $token, 'edit', $origText]);\n } else {\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n }\n exit;\n }\n \n // --- Simpan Pesan Normal ke Admin ---\n // Account ID default biasanya di set ke 1\n $id_account = 1; \n \n // Cek apakah pesan ini adalah sebuah reply\n try { $pdo->exec(\"ALTER TABLE chats ADD COLUMN reply_to_chat_id INT NULL AFTER telegram_msg_id\"); } catch(PDOException $e) {}\n \n $replyToId = null;\n if (isset($update['message']['reply_to_message']) && isset($update['message']['reply_to_message']['message_id'])) {\n $tgReplyId = $update['message']['reply_to_message']['message_id'];\n // Temukan chat lokal berdasarkan telegram_msg_id\n $qRep = $pdo->prepare(\"SELECT id_chat FROM chats WHERE telegram_msg_id = ? LIMIT 1\");\n $qRep->execute([$tgReplyId]);\n $foundRep = $qRep->fetch();\n if ($foundRep) $replyToId = $foundRep['id_chat'];\n }\n \n $insertStmt = $pdo->prepare(\"INSERT INTO chats (id_account, id_occupant, sender, message, is_read, telegram_msg_id, reply_to_chat_id) VALUES (?, ?, 'occupant', ?, 0, ?, ?)\");\n $insertStmt->execute([$id_account, $occupant['id_occupant'], $textMessage, $update['message']['message_id'], $replyToId]);\n \n // ALL PREVIOUS MESSAGES TO THIS USER ARE NOW READ\n try {\n $pdo->exec(\"UPDATE chats SET is_read = 1 WHERE id_occupant = {$occupant['id_occupant']} AND sender = 'account'\");\n } catch(PDOException $e) {}\n \n // Update last_active\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN last_active DATETIME NULL\"); } catch(PDOException $e) {}\n $updateLastActive = $pdo->prepare(\"UPDATE occupant SET last_active = NOW() WHERE id_occupant = ?\");\n $updateLastActive->execute([$occupant['id_occupant']]);\n \n } else {\n // Belum terhubung & memaksa untuk kirim kontak\n if (in_array($textMessage, ['/help', '/forgot_pin', '/forgot_password', '/edit_data'])) {\n $text = \"⚠️",
|
|
"\");\n}\n\n// Proses jika form disubmit via AJAX\nif ($_SERVER['REQUEST_METHOD'] === 'POST') {\n header('Content-Type: application/json');\n $name = trim($_POST['name']);\n $nik_nip = isset($_POST['nik_nip']) ? trim($_POST['nik_nip']) : '';\n \n if (empty($name)) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Nama wajib diisi\"]);\n exit;\n }\n\n $fotoPath = $occupant['foto']; // Default bawaan\n if (isset($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {\n $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];\n $ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));\n\n if (!in_array($ext, $allowed)) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Photo format not supported\"]);\n exit;\n }\n\n if ($_FILES['foto']['size'] > 5 * 1024 * 1024) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Max photo size is 5MB\"]);\n exit;\n }\n\n $uploadDir = __DIR__ . '/uploads/';\n if (!is_dir($uploadDir)) {\n mkdir($uploadDir, 0755, true);\n }\n\n $fotoName = 'occ_' . time() . '_' . random_int(1000, 9999) . '.' . $ext;\n $targetPath = $uploadDir . $fotoName;\n\n if (move_uploaded_file($_FILES['foto']['tmp_name'], $targetPath)) {\n $fotoPath = 'uploads/' . $fotoName;\n \n // Hapus foto lama jika ada dan bukan bawaan sistem\n if (!empty($occupant['foto']) && file_exists(__DIR__ . '/' . $occupant['foto'])) {\n @unlink(__DIR__ . '/' . $occupant['foto']);\n }\n } else {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Failed mengunggah foto. Periksa izin folder.\"]);\n exit;\n }\n }\n\n // Handle base64 foto_data from camera capture\n $fotoData = isset($_POST['foto_data']) ? $_POST['foto_data'] : '';\n if (!empty($fotoData) && strpos($fotoData, 'data:image') === 0) {\n $uploadDir = __DIR__ . '/uploads/';\n if (!is_dir($uploadDir)) {\n mkdir($uploadDir, 0755, true);\n }\n\n // Decode base64\n $parts = explode(',', $fotoData, 2);\n if (count($parts) === 2) {\n $imgBinary = base64_decode($parts[1]);\n if ($imgBinary !== false) {\n $fotoName = 'occ_' . time() . '_' . random_int(1000, 9999) . '.jpg';\n $targetPath = $uploadDir . $fotoName;\n \n if (file_put_contents($targetPath, $imgBinary)) {\n // Hapus foto lama\n if (!empty($occupant['foto']) && file_exists(__DIR__ . '/' . $occupant['foto'])) {\n @unlink(__DIR__ . '/' . $occupant['foto']);\n }\n $fotoPath = 'uploads/' . $fotoName;\n }\n }\n }\n }\n\n try {\n $stmtUpdate = $pdo->prepare(\"UPDATE occupant SET name = ?, nik_nip = ?, foto = ? WHERE id_occupant = ?\");\n $stmtUpdate->execute([$name, $nik_nip, $fotoPath, $id_occupant]);\n\n // Success, invalidate token dan update pesan telegram\n if (isset($tokenData['msg_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $chatId = '';\n $tstmt = $pdo->prepare(\"SELECT telegram_id FROM occupant WHERE id_occupant = ?\");\n $tstmt->execute([$id_occupant]);\n $tdata = $tstmt->fetch(PDO::FETCH_ASSOC);\n if ($tdata && $tdata['telegram_id']) {\n $chatId = $tdata['telegram_id'];\n @file_get_contents(\"https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['msg_id']}\");\n \n // Send success message that auto-deletes after 5 seconds with live countdown\n $successLabel = \"✅ Data Success diperbarui\";\n $successText = \"",
|
|
"Memuat data...",
|
|
"Tautan ini tidak valid atau sudah digunakan.",
|
|
"Menghapus...');\n $.post('history_ajax.php', { action: 'hard_delete', ids: deleteIds }, function(res) {\n if(res.status === 'success') {\n $('#deleteHistorySuccessAlert').css('display', 'block');\n setTimeout(function() { window.location.reload(); }, 1200);\n } else {\n alert(res.message || 'Gagal menghapus');\n $('#btnDeleteHistoryConfirm').prop('disabled', false).html('Hapus');\n }\n }, 'json').fail(function() {\n alert('Koneksi gagal');\n $('#btnDeleteHistoryConfirm').prop('disabled', false).html('Hapus');\n });\n }\n});\n\n// Toggle Filter Inputs Display\nfunction toggleFilterInputs() {\n let type = document.getElementById('filterType').value;\n document.getElementById('filterDateRow').style.display = (type === 'date') ? 'block' : 'none';\n document.getElementById('filterMonthRow').style.display = (type === 'month') ? 'block' : 'none';\n document.getElementById('filterYearRow').style.display = (type === 'month' || type === 'year') ? 'block' : 'none';\n \n // Clear values when type changes\n if(type !== '",
|
|
"/* ── MANUAL STRIPES & BORDERS ── */\n#myTable thead th { border-bottom: 1px solid #e2e8f0 !important; }\n#myTable tbody tr td { border-bottom: none !important; /* Hapus garis horizontal bawah antar menu */ }\n\n.pill { display: inline-flex; align-items: center; font-weight: 600; font-size: 11px; padding: 3px 12px; border-radius: 20px; }\n.pill-green { background: #e8f5e9; color: #2e7d32; }\n.pill-red { background: #ffebee; color: #c62828; }\n.pill-amber { background: #fff8e1; color: #f57f17; }\n.pill-gray { background: #f5f5f5; color: #757575; }\n\n.tbl-avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; border: 1.5px solid #eee; }\n.expand-arrow { font-size: 18px; color: #bbb; transition: transform 0.25s ease, color 0.2s; }\ntr.shown .expand-arrow { transform: rotate(180deg); color: #4b7bec; }\n.details-control:hover .expand-arrow { color: #4b7bec; }\n\n/* ══════════════ DROPDOWN — Seamless UI ══════════════ */\n#myTable tbody tr.shown > td {\n border-bottom: none !important;\n border-top: none !important; /* Hapus garis atas */\n}\n#myTable tbody tr.shown + tr > td {\n padding: 0 !important;\n margin: 0 !important;\n border-bottom: none !important; \n border-top: 1px solid #e2e8f0 !important; /* Tambah garis horizontal pemisah antar Menu dengan Halaman Dropdown */\n background-color: #fafafa !important;\n}\n#myTable tbody tr.shown + tr > td > div,\n#myTable tbody tr.shown + tr > td > * {\n padding: 0 !important;\n margin: 0 !important;\n}\n#myTable tbody tr.shown + tr > td:first-child {\n border-left: 1px solid #fafafa !important;\n border-bottom-left-radius: 8px;\n}\n#myTable tbody tr.shown + tr > td:last-child {\n border-right: 1px solid #fafafa !important;\n border-bottom-right-radius: 8px;\n}\n\n#myTable tbody tr.shown > td:first-child {\n border-left: 1px solid #e2e8f0 !important;\n border-top-left-radius: 8px;\n}\n#myTable tbody tr.shown > td:last-child {\n border-right: 1px solid #e2e8f0 !important;\n border-top-right-radius: 8px;\n}\n\n@keyframes ddSlideIn {\n from { opacity: 0; transform: translateY(-4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* ── MODALS ── */\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n.confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n.confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n.delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }\n.frm-lbl { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; }\n.pin-warning { display: flex; align-items: flex-start; gap: 8px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #92400e; }\n.pin-warning i { font-size: 18px; color: #f59e0b; flex-shrink: 0; margin-top: 1px; }\n.foto-wrapper { position: relative; display: inline-block; cursor: pointer; width: 72px; height: 72px; }\n.foto-circle { width: 72px; height: 72px; border-radius: 50%; object-fit: cover; border: 2px solid #e0e0e0; transition: border-color 0.2s; }\n.foto-badge { position: absolute; bottom: 0; right: 0; width: 24px; height: 24px; border-radius: 50%; background: #4b7bec; display: flex; align-items: center; justify-content: center; border: 2px solid #fff; }\n.foto-badge i { color: #fff; font-size: 12px; }\n.foto-wrapper:hover .foto-circle { border-color: #4b7bec; }\n.btn-custom-edit {\n background: #e2e6ea;\n border: none;\n color: #212529;\n transition: background 0.2s ease;\n}\n.btn-custom-edit:hover, .btn-custom-edit:focus {\n background: #cbd3da;\n color: #212529;\n}\n.eye-toggle { cursor: pointer; background: #fff; border-left: none; }\n.eye-toggle:hover i { color: #4b7bec; }\n.code-big { font-size: 26px; letter-spacing: 8px; color: #222; font-weight: 700; border-right: none; }\n.code-input-group .form-control { border-right: none; }\n.info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; }\n.info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n.info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n.countdown-timer { font-weight: 700; color: #dc2626; font-size: 12px; margin-left: 8px; display: inline-flex; align-items: center; background: #fef2f2; padding: 3px 10px; border-radius: 6px; border: 1px solid #fecaca; white-space: nowrap; }\n@keyframes pulse-timer { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }",
|
|
"\", 'parse_mode'=>'HTML', 'reply_markup' => json_encode($btn)];\n \n $res = file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n $resData = json_decode($res, true);\n if (isset($resData['result']['message_id'])) {\n $tkns[$token]['msg_id'] = $resData['result']['message_id'];\n $tkns[$token]['reply_markup'] = json_encode($btn);\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n \n // Trigger auto delete via background CLI process\n runBackground(__DIR__ . '/auto_delete.php', [$chatId, $resData['result']['message_id'], $token, 'forgot', $origText]);\n } else {\n file_put_contents($manPath, json_encode($tkns, JSON_PRETTY_PRINT));\n }\n exit;\n }\n \n if ($textMessage === '/edit_data') {\n $token = bin2hex(random_bytes(16));\n $manPath = __DIR__ . '/manifest_edit_tokens.json';\n $tkns = file_exists($manPath) ? (json_decode(file_get_contents($manPath), true) ?: []) : [];\n // Delete previous message for same occupant\n foreach ($tkns as $oldTk => $oldData) {\n if (isset($oldData['id_occupant']) && $oldData['id_occupant'] == $id_occupant && isset($oldData['msg_id'])) {\n @file_get_contents($website.\"/deleteMessage?chat_id=$chatId&message_id={$oldData['msg_id']}\");\n unset($tkns[$oldTk]);\n }\n }\n $tkns[$token] = ['id_occupant'=>$id_occupant, 'created_at'=>time(), 'expires_at'=>time() + 300];\n \n $expireTime = date('H:i:s', time() + 300);\n $webAppUrl = $baseUrl . \"/edit_data.php?token=\" . $token;\n $btn = ['inline_keyboard' => [[['text' => 'Perbarui Data Profil', 'web_app' => ['url' => $webAppUrl]]]]];\n $origText = \"Sistem Pembaruan Data Identitas telah disediakan untuk Anda.\";\n $postData = ['chat_id' => $chatId, 'text' => $origText . \"\\n⏳",
|
|
"Confirm Password",
|
|
"Foto Belum Diverifikasi",
|
|
"Dilindungi dengan skema watermark digital",
|
|
"\"\n data-hasfoto=\"",
|
|
"Gagal memuat QR",
|
|
"Memverifikasi...';\n btn.disabled = true;\n \n $.post(\"verify_pin_account.php\", { id_account: id, pin: pin }, function(d) {\n btn.innerHTML = '",
|
|
"Nama lengkap",
|
|
"\" alt=\"foto\" class=\"ml-2\" style=\"width: 24px; height: 24px; object-fit: cover; border-radius: 4px; border: 1px solid #ddd;\">",
|
|
"Batal",
|
|
"`;}\n if(sd){const e=sd.extra_data||{};const tmp=parseFloat(e.temperature)||0;\n h+=`",
|
|
"Menghubungkan ke jaringan baru...",
|
|
"Menyimpan...';\n btn.disabled = true;\n\n const formData = new FormData(this);\n\n fetch('', {\n method: 'POST',\n body: formData\n })\n .then(response => response.json())\n .then(data => {\n if (data.status === 'success') {\n document.getElementById('step2-password').style.display = 'none';\n document.getElementById('step3-success').style.display = 'block';\n \n if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.HapticFeedback) {\n window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');\n }\n\n // Auto close after 10 seconds\n let count = 10;\n const cText = document.getElementById('countdownText');\n const cInterval = setInterval(() => {\n count--;\n cText.innerText = count;\n if(count",
|
|
"`;}\n if(rl){const e=rl.extra_data||{};const isOn=e.relay_state==='on';const cir=e.circuit_connected==='yes';\n h+=`",
|
|
"Kelola data akun operasional,",
|
|
".pin-input-group {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 5px;\n }\n .pin-box {\n width: 45px;\n height: 55px;\n text-align: center;\n font-size: 24px;\n border: none;\n border-bottom: 2px solid #ccc;\n background: transparent;\n outline: none;\n transition: border-color 0.3s;\n }\n .pin-box:focus {\n border-bottom-color: #4B49AC;\n }\n\n .custom-alert {\n position: fixed;\n top: 20px;\n right: 20px;\n background: #fff;\n border-left: 4px solid #fcd34d;\n box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n padding: 15px 20px;\n border-radius: 4px;\n z-index: 9999;\n display: none;\n align-items: center;\n gap: 12px;\n min-width: 300px;\n max-width: 400px;\n animation: slideIn 0.3s ease-out;\n }\n .custom-alert.error { border-left-color: #ef4444; }\n .custom-alert.success { border-left-color: #10b981; }\n @keyframes slideIn {\n from { transform: translateX(100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n }\n @keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n }\n .custom-alert-icon { font-size: 24px; }\n .custom-alert.error .custom-alert-icon { color: #ef4444; }\n .custom-alert.success .custom-alert-icon { color: #10b981; }\n .custom-alert.warning .custom-alert-icon { color: #f59e0b; }\n .custom-alert-content { flex: 1; text-align: left; }\n .custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }\n .custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }\n .custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }\n .custom-alert-close:hover { color: #333; }\n\n /* Style for the custom tooltip to look native */\n .custom-tooltip-wrapper {\n position: relative;\n display: block;\n }\n .custom-tooltip {\n position: absolute;\n top: 100%;\n left: 50%;\n transform: translateX(-50%);\n background-color: white;\n color: #333;\n padding: 8px 15px;\n border-radius: 4px;\n box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n font-size: 14px;\n white-space: nowrap;\n z-index: 1000;\n margin-top: 8px;\n border: 1px solid #ddd;\n display: none; /* hidden by default */\n align-items: center;\n gap: 8px;\n }\n .custom-tooltip::before {\n content: '';\n position: absolute;\n bottom: 100%;\n left: 50%;\n transform: translateX(-50%);\n border-width: 8px;\n border-style: solid;\n border-color: transparent transparent #ddd transparent;\n }\n .custom-tooltip::after {\n content: '';\n position: absolute;\n bottom: 100%;\n left: 50%;\n transform: translateX(-50%);\n border-width: 7px;\n border-style: solid;\n border-color: transparent transparent white transparent;\n }\n .custom-tooltip .error-icon {\n background-color: #ff9800;\n color: white;\n width: 20px;\n height: 20px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 3px;\n font-weight: bold;\n font-family: serif;\n }\n\n .info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; margin-bottom: 1rem; }\n .info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n .info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n \n body, .container-scroller, .page-body-wrapper, .content-wrapper {\n background-color: #f8f9fa !important;\n }",
|
|
"Verifikasi Identitas",
|
|
"— pastikan tidak ada kesalahan ketik, perhatikan huruf besar/kecil (case-sensitive), spasi, dan karakter khusus seperti @, #, !, dsb.',\n '",
|
|
"Nama Kegiatan",
|
|
"Sentuh sensor pada perangkat.",
|
|
"ESP32 juga gagal kembali ke WiFi sebelumnya. Saat ini ESP32 memancarkan hotspot konfigurasi. Hubungkan ke WiFi",
|
|
"Sidik Jari Berhasil Disimpan!",
|
|
"Your profile data has been updated.",
|
|
"karena",
|
|
"Ulangi Password",
|
|
"alasan yang tidak dapat diidentifikasi secara spesifik",
|
|
"Password dan Confirm Password tidak sama!",
|
|
"Password belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!",
|
|
"Email address",
|
|
"ingin memperbarui foto, klik",
|
|
"`;}\n el('gInd').innerHTML=h;\n}\nfunction uInd(){\n const tf=F('tft_screen'),bz=F('buzzer'),lr=F('led_red'),lg=F('led_green');\n if(tf){const e=tf.extra_data||{};el('info-tf').innerHTML=mr('mdi-page-layout-body','Page',e.current_page||'Standby','b')+mr('mdi-brightness-6','Brightness',(e.brightness||100)+'%','c');el('ft-tf').innerHTML='",
|
|
"';\n data.occupant.forEach(item => {\n const sub = item.no_hp ? item.no_hp : 'No phone';\n html += `",
|
|
"Menyimpan pengaturan ESP32...",
|
|
"Berhasil!",
|
|
"Lihat semua jadwal disini !",
|
|
"Foto diambil via QR code setelah data tersimpan",
|
|
"ESP32 gagal terhubung ke jaringan",
|
|
"Preview Foto",
|
|
"Menyimpan Sidik Jari...';\n \n let fd = new FormData();\n fd.append('id_account', accountId);\n fd.append('credential_id', rawIdBase64);\n \n fetch(\"save_fingerprint_account.php\", { method: \"POST\", body: fd })\n .then(r => r.json())\n .then(d => {\n if(d.status === 'success') {\n statusEl.style.display = \"none\";\n successWrapper.style.display = \"block\";\n btn.style.display = \"none\";\n } else {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
|
|
"// Init Telegram WebApp\n window.Telegram.WebApp.ready();\n window.Telegram.WebApp.expand();\n\n let customAlertTimeout;\n let closeAlertTimeout;\n\n function showCustomAlert(title, message, type='warning') {\n const alertBox = document.getElementById('customAlert');\n const icon = document.getElementById('customAlertIcon');\n \n clearTimeout(customAlertTimeout);\n clearTimeout(closeAlertTimeout);\n\n alertBox.className = 'custom-alert ' + type;\n if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';\n else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';\n else icon.className = 'mdi mdi-alert-circle custom-alert-icon';\n\n document.getElementById('customAlertTitle').innerText = title;\n document.getElementById('customAlertMsg').innerText = message;\n \n alertBox.style.display = 'flex';\n alertBox.style.animation = 'slideIn 0.3s ease-out';\n\n customAlertTimeout = setTimeout(() => closeCustomAlert(), 5000);\n }\n\n function closeCustomAlert() {\n const alertBox = document.getElementById('customAlert');\n alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';\n clearTimeout(customAlertTimeout);\n clearTimeout(closeAlertTimeout);\n closeAlertTimeout = setTimeout(() => {\n alertBox.style.display = 'none';\n }, 300);\n }\n\n // Logic Input PIN (mirip di forgot_password.php)\n const pinInputs = document.querySelectorAll('.pin-box');\n \n pinInputs.forEach((input, index) => {\n input.addEventListener('input', function(e) {\n // Allow only numbers\n this.value = this.value.replace(/[^0-9]/g, '');\n \n if (this.value !== '') { // Jika diisi\n this.classList.remove('input-error');\n if (index",
|
|
"Koneksi Dipulihkan:",
|
|
"Memverifikasi Data...",
|
|
"Input ulang password dengan benar",
|
|
"t.stop());\n editCamArea.style.display = 'none';\n editPreviewWrap.style.display = 'block';\n document.querySelector('.ecam-hud').style.display = 'none';\n editBarAfter.style.display = 'flex';\n }\n\n function editRetake() {\n editDataURL = null;\n editLivenessScore = 0; editLivenessFrames = 0; editPrevLandmarks = null;\n editLmFill.style.height = '0%';\n editPreviewWrap.style.display = 'none';\n editCamArea.style.display = 'block';\n editBarAfter.style.display = 'none';\n document.querySelector('.ecam-hud').style.display = 'flex';\n editCamLoading.style.display = 'flex';\n editLoadText.textContent = 'Memuat ulang kamera...';\n editSetProgress(50);\n initEditCamera();\n }\n\n function editUseFoto() {\n if (!editDataURL) return;\n document.getElementById('previewFoto').src = editDataURL;\n document.getElementById('fotoData').value = editDataURL;\n stopEditCamera();\n showCustomAlert('Success', 'Foto berhasil diambil. Klik Save Data untuk menyimpan.', 'success');\n }\n\n function stopEditCamera() {\n document.getElementById('cameraOverlay').style.display = 'none';\n clearInterval(editDetectLoop);\n clearInterval(editBrightInterval);\n if (editStream) { editStream.getTracks().forEach(t => t.stop()); editStream = null; }\n }\n\n editBtnShutter.addEventListener('click', editCapture);\n document.getElementById('editBtnRetake').addEventListener('click', editRetake);\n document.getElementById('editBtnUseFoto').addEventListener('click', editUseFoto);\n\n document.getElementById('btnSimpan').addEventListener('click', function() {\n // Validate step 1 data again just in case\n const name = document.getElementById('name').value.trim();\n if(name === '') {\n showCustomAlert('Incomplete Form', 'Full Name wajib diisi.', 'warning');\n document.getElementById('step2-foto').style.display = 'none';\n document.getElementById('step1-data').style.display = 'block';\n return;\n }\n\n const btn = document.getElementById('btnSimpan');\n const originalText = btn.innerHTML;\n btn.innerHTML = '",
|
|
"Selesai",
|
|
"Gagal memindai jaringan (Timeout). Pastikan ESP32 menyala.",
|
|
"';\n loading.style.display = 'block';\n });\n}\n\nfunction copyQrFotoLink() {\n navigator.clipboard.writeText(document.getElementById('qrFotoLinkText').value);\n showCustomAlert('Link berhasil disalin!');\n}\n\nfunction downloadQrFoto() {\n var qrCanvas = document.querySelector('#qrFotoCanvas canvas');\n if (!qrCanvas) { alert(\"QR Code belum ter-generate.\"); return; }\n \n var imgUrl = createCompositeQrImage(qrCanvas);\n var dlLink = document.createElement('a');\n dlLink.download = 'QR_Edit_Foto.jpg';\n dlLink.href = imgUrl.replace(\"image/jpeg\", \"image/octet-stream\");\n dlLink.click();\n}",
|
|
"Loginkan kata sandi baru untuk akun Anda",
|
|
"— jauhkan ESP32 dan router dari perangkat elektronik yang menghasilkan interferensi seperti microwave, telepon cordless, atau perangkat Bluetooth yang aktif.'\n ];\n } else if (reason.indexOf('timeout') !== -1 || elapsedWait >= maxTimeout) {\n errMsg = 'Waktu Tunggu Habis (Connection Timeout)';\n reasonText = '",
|
|
"\");\n}\n\n// Proses jika form disubmit via AJAX\nif ($_SERVER['REQUEST_METHOD'] === 'POST') {\n header('Content-Type: application/json');\n $action = isset($_POST['action']) ? $_POST['action'] : '';\n\n if ($action === 'verify_pin') {\n $pin = isset($_POST['pin']) ? trim($_POST['pin']) : '';\n if (empty($pin)) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"PIN wajib diisi.\"]);\n exit;\n }\n\n $stmt = $pdo->prepare(\"SELECT pin FROM occupant WHERE id_occupant = ?\");\n $stmt->execute([$id_occupant]);\n $data = $stmt->fetch(PDO::FETCH_ASSOC);\n\n if ($data && $data['pin'] === $pin) {\n echo json_encode([\"status\" => \"success\", \"message\" => \"PIN Valid. Silakan masukkan kata sandi baru.\"]);\n } else {\n echo json_encode([\"status\" => \"error\", \"message\" => \"PIN tidak valid!\"]);\n }\n exit;\n }\n\n if ($action === 'reset_password') {\n $password = isset($_POST['password']) ? $_POST['password'] : '';\n $confirm_password = isset($_POST['confirm_password']) ? $_POST['confirm_password'] : '';\n\n if (empty($password) || empty($confirm_password)) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Semua kolom wajib diisi.\"]);\n exit;\n }\n if ($password !== $confirm_password) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Passwords do not match.\"]);\n exit;\n }\n\n // Add password columns to occupant if they don't exist yet\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN password VARCHAR(255) NULL AFTER no_hp\"); } catch(PDOException $e) {}\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN password_enc VARCHAR(255) NULL AFTER password\"); } catch(PDOException $e) {}\n\n $password_hash = password_hash($password, PASSWORD_DEFAULT);\n\n try {\n $stmt = $pdo->prepare(\"UPDATE occupant SET password = ?, password_enc = ? WHERE id_occupant = ?\");\n if ($stmt->execute([$password_hash, $password, $id_occupant])) {\n \n // Edit Telegram Message to disable the inline button\n if (isset($tokenData['msg_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $chatId = $occupant['telegram_id'] ?? $tokenData['id_occupant'];\n // We need actual telegram_id. \n $tstmt = $pdo->prepare(\"SELECT telegram_id FROM occupant WHERE id_occupant = ?\");\n $tstmt->execute([$id_occupant]);\n $tdata = $tstmt->fetch(PDO::FETCH_ASSOC);\n if ($tdata && $tdata['telegram_id']) {\n $chatId = $tdata['telegram_id'];\n @file_get_contents(\"https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['msg_id']}\");\n \n // Send success message that auto-deletes after 5 seconds with live countdown\n $successLabel = \"✅ Data Success di simpan\";\n $successText = \"",
|
|
"Forgot Password",
|
|
"' + (d.message || 'Gagal generate QR') + '",
|
|
"Create a 6-digit PIN for additional security.",
|
|
"';\n btnSave.disabled = true;\n\n var fd = new FormData();\n fd.append('token', TOKEN);\n fd.append('foto', dataURL);\n\n var xhr = new XMLHttpRequest();\n xhr.open('POST', 'save_foto.php', true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.status === 'success') {\n showPage(pgSuccess);\n if (d.foto) {\n document.getElementById('sucFoto').src = d.foto;\n document.getElementById('sucFoto').style.display = 'block';\n }\n } else {\n showToast('err', d.message || 'Gagal menyimpan');\n btnSave.disabled = false;\n btnSave.querySelector('.ring').innerHTML = '",
|
|
"Ketik provinsi lalu pilih...",
|
|
"function copyFingerQrLink() {\n var v = document.getElementById(\"fingerQrLinkText\").value;\n navigator.clipboard.writeText(v).then(function(){ showCustomAlert('Link berhasil disalin!'); });\n}\n\nfunction createCompositeQrImage(qrCanvas) {\n var width = 800;\n var height = 1000;\n var composite = document.createElement('canvas');\n composite.width = width;\n composite.height = height;\n var ctx = composite.getContext('2d');\n \n // 1. Draw Background\n ctx.fillStyle = '#f8fafc';\n ctx.fillRect(0, 0, width, height);\n \n // 2. Add Decorative Top Gradient Bar\n var gradient = ctx.createLinearGradient(0, 0, width, 0);\n gradient.addColorStop(0, '#4b49ac');\n gradient.addColorStop(1, '#94a3b8');\n ctx.fillStyle = gradient;\n ctx.fillRect(0, 0, width, 15);\n \n // 3. Draw Header Details\n ctx.fillStyle = '#1e293b';\n ctx.font = 'bold 36px Arial, sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('IDENTITAS DIGITAL', width / 2, 80);\n \n ctx.fillStyle = '#64748b';\n ctx.font = '22px Arial, sans-serif';\n ctx.fillText('Akses Cepat & Aman', width / 2, 120);\n \n // 4. White Box Container\n var boxWidth = 600;\n var boxHeight = 700;\n var boxX = (width - boxWidth) / 2;\n var boxY = 160;\n \n ctx.fillStyle = '#ffffff';\n ctx.shadowColor = 'rgba(0, 0, 0, 0.08)';\n ctx.shadowBlur = 30;\n ctx.shadowOffsetY = 15;\n ctx.beginPath();\n ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 20);\n ctx.fill();\n ctx.shadowColor = 'transparent';\n \n // 5. Draw QR Code centered inside the container\n var qrSize = 460;\n var x = (width - qrSize) / 2;\n var y = boxY + 80;\n ctx.drawImage(qrCanvas, x, y, qrSize, qrSize);\n \n // Border around QR Code\n ctx.strokeStyle = '#e2e8f0';\n ctx.lineWidth = 4;\n ctx.strokeRect(x - 10, y - 10, qrSize + 20, qrSize + 20);\n \n ctx.fillStyle = '#334155';\n ctx.font = 'bold 24px Arial, sans-serif';\n ctx.fillText('Scan untuk Mendaftar Sidik Jari', width / 2, y - 25);\n \n // 6. Add Footer Note\n ctx.fillStyle = '#1e293b';\n ctx.font = 'bold 26px Arial, sans-serif';\n ctx.fillText('SKY DASH APP', width / 2, y + qrSize + 70);\n \n ctx.fillStyle = '#64748b';\n ctx.font = '20px Arial, sans-serif';\n ctx.fillText('Powered by Identity System', width / 2, y + qrSize + 105);\n \n return composite.toDataURL(\"image/jpeg\", 1.0);\n}\n\nfunction downloadFingerQr() {\n var qrCanvas = document.querySelector('#fingerprintQrCanvas canvas');\n if (!qrCanvas) { alert(\"QR Code belum ter-generate.\"); return; }\n var imgUrl = createCompositeQrImage(qrCanvas);\n var dlLink = document.createElement('a');\n dlLink.download = 'QR_Fingerprint_Account.jpg';\n dlLink.href = imgUrl.replace(\"image/jpeg\", \"image/octet-stream\");\n dlLink.click();\n}\n\nfunction showFingerprintModal(accountId) {\n document.getElementById(\"fingerprintAccountId\").value = accountId;\n $(\"#modalFingerprint\").modal(\"show\");\n \n document.getElementById(\"fingerprintQrLoadingView\").style.display = 'block';\n document.getElementById(\"fingerprintQrView\").style.display = 'none';\n document.getElementById(\"fingerprintQrDefaultView\").style.display = 'none';\n \n fetch(\"generate_finger_token_account.php?id=\" + accountId)\n .then(r => r.json())\n .then(d => {\n document.getElementById(\"fingerprintQrLoadingView\").style.display = 'none';\n if(d.status === 'success') {\n document.getElementById(\"fingerprintQrView\").style.display = 'block';\n document.getElementById(\"fingerQrLinkText\").value = d.url;\n var canvas = document.getElementById(\"fingerprintQrCanvas\");\n canvas.innerHTML = '';\n new QRCode(canvas, {\n text: d.url, width: 150, height: 150,\n colorDark: '#0f172a', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.H\n });\n } else {\n document.getElementById(\"fingerprintQrDefaultView\").style.display = 'block';\n }\n }).catch(e => {\n document.getElementById(\"fingerprintQrLoadingView\").style.display = 'none';\n document.getElementById(\"fingerprintQrDefaultView\").style.display = 'block';\n });\n}\n\nasync function startWebAuthnRegistration() {\n var btn = document.getElementById(\"btnScanFingerHere\");\n var statusEl = document.getElementById(\"webAuthnStatus\");\n var errorEl = document.getElementById(\"webAuthnError\");\n var successWrapper = document.getElementById(\"webAuthnStatusWrapper\");\n var accountId = document.getElementById(\"fingerprintAccountId\").value;\n \n btn.disabled = true;\n errorEl.style.display = \"none\";\n successWrapper.style.display = \"none\";\n \n statusEl.innerHTML = '",
|
|
"Hapus Account?",
|
|
"Tekan untuk menyimpan perubahan",
|
|
"');\n $('#wifiNetworkList').fadeIn(300);\n });\n });\n\n // Click on network to fill SSID\n $(document).on('click', '.wifi-network-item', function() {\n var ssid = $(this).data('ssid');\n $('#new-ssid').val(ssid);\n $('#wifiScanModal').modal('hide');\n $('#new-wifi-pass').val('').focus();\n });\n\n // Hover effect on network items\n $(document).on('mouseenter', '.wifi-network-item', function() {\n $(this).css('background', '#f0eeff');\n }).on('mouseleave', '.wifi-network-item', function() {\n $(this).css('background', '');\n });\n\n // ===== WIFI SAVE WITH FULLSCREEN ANIMATION =====\n $('#saveWifiBtn, #wifiSaveModal .btn:last-child').on('click', function() {\n var ssid = $('#new-ssid').first().val().trim();\n var pass = $('#new-wifi-pass').first().val().trim();\n if (!ssid) { alert('SSID cannot be empty'); return; }\n\n // Send change API command\n $.post('api_wifi.php', { action: 'change_wifi', ssid: ssid, password: pass }, function(res) {\n if(res.success) {\n // Reset all steps\n $('.save-step').hide();\n $('.save-bar-fill').css('width', '0%');\n // Reset modal to small for non-failure steps\n $('#wifiSaveModalDialog').removeClass('modal-lg').addClass('modal-sm');\n $('#saveStep1').show();\n $('#wifiSaveModal').modal('show');\n\n // Step 1: Connecting (0-2.5s)\n setTimeout(function() { $('#saveStep1 .save-bar-fill').css('width', '50%'); }, 500);\n setTimeout(function() { $('#saveStep1 .save-bar-fill').css('width', '100%'); }, 1800);\n\n // Step 2 & 3: Waiting logic\n setTimeout(function() {\n $('.save-step').hide();\n $('#saveStep2').fadeIn(300);\n \n // Animasi progress bar step 2 melambat (pura-pura proses hingga batas waktu)\n $('#saveStep2 .save-bar-fill').css({'width': '85%', 'transition': 'width 30s cubic-bezier(0.1, 0.8, 0.3, 1)'});\n }, 1800);\n\n // Simpan sesi ganti WiFi & Mulai Timer Timeout (Misal 60 Detik maksimal)\n var isCheckingChangeStatus = true;\n var maxTimeout = 90; // detik\n var elapsedWait = 0;\n \n // Polling status penggantian WiFi\n var changeCheckInterval = setInterval(function() {\n elapsedWait += 2.5; \n \n $.get('api_wifi.php?action=check_change_status', function(statusRes) {\n if (statusRes.status === 'failed' || elapsedWait >= maxTimeout) {\n clearInterval(changeCheckInterval);\n var reason = statusRes.reason || '';\n var errMsg = statusRes.status === 'failed' ? (statusRes.message || 'Gagal terhubung.') : 'Waktu Tunggu Habis (Timeout).';\n \n // Build detailed failure explanation\n var reasonText = '';\n var suggestions = [];\n var fallbackHtml = '';\n var targetSsid = $('#new-ssid').first().val().trim();\n \n if (reason.indexOf('wrong_password') !== -1) {\n errMsg = 'Autentikasi WiFi Ditolak (Wrong Password)';\n reasonText = '",
|
|
"Menghubungkan ke server.",
|
|
"';\n }\n };\n xhr.onerror = function() {\n showToast('err', 'Koneksi gagal');\n btnSave.disabled = false;\n btnSave.querySelector('.ring').innerHTML = '",
|
|
"Hubungi occupant Anda melalui Integrasi Telegram Bot.",
|
|
"Verify your identity to reset password.",
|
|
"`;\n });\n }\n\n // Occupant\n if (showOccupant && data.occupant && data.occupant.length > 0) {\n html += '",
|
|
".pin-input-group {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 5px;\n }\n .pin-box {\n width: 45px;\n height: 55px;\n text-align: center;\n font-size: 24px;\n border: none;\n border-bottom: 2px solid #ccc;\n background: transparent;\n outline: none;\n transition: border-color 0.3s;\n }\n .pin-box:focus {\n border-bottom-color: #4B49AC;\n }\n .text-separator {\n text-align: center;\n color: #888;\n font-size: 14px;\n margin: 15px 0;\n }\n \n .custom-alert {\n position: fixed;\n top: 20px;\n right: 20px;\n background: #fff;\n border-left: 4px solid #fcd34d;\n box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n padding: 15px 20px;\n border-radius: 4px;\n z-index: 9999;\n display: none;\n align-items: center;\n gap: 12px;\n min-width: 300px;\n max-width: 400px;\n animation: slideIn 0.3s ease-out;\n }\n .custom-alert.error { border-left-color: #ef4444; }\n .custom-alert.success { border-left-color: #10b981; }\n @keyframes slideIn {\n from { transform: translateX(100%); opacity: 0; }\n to { transform: translateX(0); opacity: 1; }\n }\n @keyframes fadeOut {\n from { opacity: 1; }\n to { opacity: 0; }\n }\n .custom-alert-icon { font-size: 24px; }\n .custom-alert.error .custom-alert-icon { color: #ef4444; }\n .custom-alert.success .custom-alert-icon { color: #10b981; }\n .custom-alert.warning .custom-alert-icon { color: #f59e0b; }\n .custom-alert-content { flex: 1; text-align: left; }\n .custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }\n .custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }\n .custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }\n .custom-alert-close:hover { color: #333; }\n \n body, .container-scroller, .page-body-wrapper, .content-wrapper {\n background-color: #f8f9fa !important;\n }",
|
|
"Setup PIN Gagal",
|
|
"showCustomAlert(\"Pendaftaran Gagal\", \"",
|
|
"Gunakan menu ini untuk Memindahkan ESP32 ke koneksi WiFi lain (Misal dari WiFi A ke WiFi B). Tekan tombol",
|
|
"';\n data.overview.forEach(item => {\n html += `",
|
|
"Klik foto di atas untuk mengubah via QR Code",
|
|
"Daftarkan akun baru,",
|
|
"7) {\n let jam = actDate.getHours().toString().padStart(2, '0');\n let menit = actDate.getMinutes().toString().padStart(2, '0');\n let tgl = actDate.getDate().toString().padStart(2, '0');\n let bln = (actDate.getMonth() + 1).toString().padStart(2, '0');\n let thn = actDate.getFullYear();\n occStatStr = `Terakhir Online ${jam}.${menit} ${tgl}-${bln}-${thn}`;\n } else {\n const hariArr = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];\n let namaHari = hariArr[actDate.getDay()];\n let jam = actDate.getHours().toString().padStart(2, '0');\n let menit = actDate.getMinutes().toString().padStart(2, '0');\n occStatStr = `Terakhir Online Jam ${jam}:${menit} Hari ${namaHari}`;\n }\n }\n document.getElementById('activeStatus').innerText = occStatStr;\n document.getElementById('activeAvatar').src = res.occupant.foto;\n activeAvatarUrl = res.occupant.foto;\n \n renderMessages(res.data);\n loadContacts();\n }\n }, 'json');\n}\n\nfunction backToContacts() {\n document.querySelector('.chat-wrapper').classList.remove('mobile-chat-open');\n activeOccupantId = null;\n document.getElementById('chatBlank').style.display = 'flex';\n document.getElementById('chatActive').style.display = 'none';\n renderContacts(contacts);\n}\n\nfunction buildMessagesHtml(messages) {\n let html = '';\n const adminAvatar = 'images/admin.png';\n \n messages.forEach(msg => {\n let isRight = (msg.sender === 'account');\n let cls = isRight ? 'msg-right' : 'msg-left';\n \n let statusHtml = '';\n if (isRight) {\n if (msg.is_read == 1) {\n statusHtml = '",
|
|
"Autentikasi biometrik diperlukan untuk membuka akses pintu IoT.",
|
|
"0.6) { editSetFaceNotOk(); editSetStatus('err', 'Wajah terlalu dekat'); }\n else if (dx > 0.2 || dy > 0.2) { editSetFaceNotOk(); editSetStatus('err', 'Pusatkan wajah'); }\n else {\n editFaceOk = true;\n editOval.classList.add('ok');\n if (landmarks) {\n const pts = landmarks.positions;\n if (editPrevLandmarks && pts.length === editPrevLandmarks.length) {\n let totalDiff = 0;\n for (let i = 0; i",
|
|
"/edit_data",
|
|
"Pindah ke Sampah",
|
|
"Kesalahan koneksi ke server.",
|
|
"Lengkapi profil sekarang!",
|
|
"Instagram belum diisi",
|
|
"const pwForm = document.getElementById('passwordForm');\n if(pwForm) {\n pwForm.addEventListener('submit', function(e) {\n var pass = document.getElementById(\"newPassword\").value;\n const pwValid = (pass.length >= 8 && /[a-z]/.test(pass) && /[A-Z]/.test(pass) && /\\d/.test(pass) && /[^A-Za-z0-9]/.test(pass));\n if (!pwValid) {\n e.preventDefault();\n showCustomAlert(\"Warning\", \"Password does not meet criteria (min. 8 chars, uppercase, lowercase, numbers, and symbols)!\", \"warning\");\n document.getElementById(\"newPassword\").focus();\n }\n });\n\n // Eye Toggle Logic\n var toggles = document.querySelectorAll('.eye-toggle');\n toggles.forEach(function(toggle) {\n toggle.addEventListener('click', function() {\n var targetId = this.getAttribute('data-target');\n var input = document.getElementById(targetId);\n var icon = this.querySelector('i');\n if (input.type === 'password') {\n input.type = 'text';\n icon.classList.remove('mdi-eye-off');\n icon.classList.add('mdi-eye');\n } else {\n input.type = 'password';\n icon.classList.remove('mdi-eye');\n icon.classList.add('mdi-eye-off');\n }\n });\n });\n\n // Password Strength Logic\n document.getElementById('newPassword').addEventListener('input', function() {\n var pass = this.value;\n var meter = document.getElementById('pwStrengthMeterReg');\n var bar = document.getElementById('pwStrengthBarReg');\n var text = document.getElementById('pwStrengthTextReg');\n\n if(pass.length > 0) {\n meter.style.display = 'flex';\n var hasLower = /[a-z]/.test(pass);\n var hasUpper = /[A-Z]/.test(pass);\n var hasNumber = /\\d/.test(pass);\n var hasSymbol = /[^A-Za-z0-9]/.test(pass);\n var typesCount = (hasLower?1:0) + (hasUpper?1:0) + (hasNumber?1:0) + (hasSymbol?1:0);\n \n if (pass.length >= 8 && typesCount === 4) {\n bar.style.width = '100%'; bar.style.backgroundColor = '#10b981'; // Kuat\n text.innerText = 'Strong Password'; text.style.color = '#10b981';\n } else if (pass.length >= 6 && typesCount >= 2) {\n bar.style.width = '66%'; bar.style.backgroundColor = '#f59e0b'; // Sedang\n text.innerText = 'Medium Password'; text.style.color = '#f59e0b';\n } else {\n bar.style.width = '33%'; bar.style.backgroundColor = '#ef4444'; // Lemah\n text.innerText = 'Weak Password'; text.style.color = '#ef4444';\n }\n } else {\n meter.style.display = 'none';\n text.innerText = '';\n }\n });\n }",
|
|
"';\n btnSave.disabled = true;\n\n var fd = new FormData();\n fd.append('token', TOKEN);\n fd.append('foto', dataURL);\n fd.append('liveness_score', Math.round(livenessScore));\n fd.append('capture_time', new Date().toISOString());\n fd.append('device_info', navigator.userAgent);\n fd.append('screen_res', screen.width + 'x' + screen.height);\n\n var xhr = new XMLHttpRequest();\n xhr.open('POST', 'save_foto.php', true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.status === 'success') {\n sessionSaved = true;\n clearInterval(tokenCheckInterval);\n showPage(pgSuccess);\n if (d.foto) {\n document.getElementById('sucFoto').src = d.foto;\n document.getElementById('sucFoto').style.display = 'block';\n }\n // Auto-close countdown\n var count = 30;\n var cdEl = document.getElementById('sucCountdown');\n var cdInterval = setInterval(function() {\n count--;\n if (cdEl) cdEl.textContent = 'Halaman otomatis tertutup dalam ' + count + ' detik';\n if (count",
|
|
"// Tambah\n$(document).ready(function() {\n window.isSelectMode = false;\n $('#btnSelectMode').on('click', function() {\n window.isSelectMode = !window.isSelectMode;\n if(window.isSelectMode) {\n $(this).html('",
|
|
"') {\n document.getElementById('filterDateRow').value = '';\n document.getElementById('filterMonthRow').value = '';\n document.getElementById('filterYearRow').value = '';\n }\n checkFilterForm();\n}\n\nfunction checkFilterForm() {\n let type = document.getElementById('filterType').value;\n let isValid = false;\n \n if (type === 'date') {\n isValid = document.getElementById('filterDateRow').value.trim() !== '';\n } else if (type === 'month') {\n isValid = document.getElementById('filterMonthRow').value !== '' && document.getElementById('filterYearRow').value.trim() !== '';\n } else if (type === 'year') {\n isValid = document.getElementById('filterYearRow').value.trim() !== '';\n }\n \n document.getElementById('btnSubmitFilter').disabled = !isValid;\n}\n\n// Run validation on load based on current inputs\n$(document).ready(function() {\n checkFilterForm();\n});\n\n// Live AJAX Search\nlet searchTimer;\n$('#searchInput').on('keyup', function() {\n clearTimeout(searchTimer);\n let form = $(this).closest('form');\n // Set page to 1 on new search\n let formData = form.serializeArray();\n let queryParams = [];\n $.each(formData, function(i, field) {\n queryParams.push(field.name + '=' + encodeURIComponent(field.value));\n });\n queryParams.push('page=1');\n \n let url = form.attr('action') + '?' + queryParams.join('&');\n \n searchTimer = setTimeout(function() {\n // Update browser URL without reload\n window.history.pushState({}, '', url);\n \n // Show lightweight loading indication\n $('#emailListWrapper').css('opacity', '0.5');\n \n // Fetch new data\n $.get(url, function(data) {\n let $parsed = $(data);\n $('#emailListWrapper').html($parsed.find('#emailListWrapper').html()).css('opacity', '1');\n $('#paginationControls').html($parsed.find('#paginationControls').html());\n });\n }, 150); // 150ms debounce for faster feel\n});\n\n// Prevent traditional form submission on enter\n$('#searchForm').on('submit', function(e) {\n e.preventDefault();\n});",
|
|
"\"\n data-fingerid=\"",
|
|
". Berikut adalah PIN rahasia Anda:",
|
|
"Periksa firewall dan isolasi klien",
|
|
"Belum ada kegiatan",
|
|
"';\n data.account.forEach(item => {\n html += `",
|
|
"70?'er':sc(sd.status)}\" id=\"bdg-sd\">${tmp>70?'OVERHEAT':(e.state==='active'?'ACTIVE':'INACTIVE')}",
|
|
"oleh firmware mikrokontroler.' ;\n suggestions = [\n '",
|
|
"Foto Profil",
|
|
"';\n\n fetch('search_api.php?q=' + encodeURIComponent(query))\n .then(r => r.json())\n .then(res => {\n if (res.status === 'success') {\n renderResults(res.data, query, activeTab);\n }\n })\n .catch(err => {\n searchResults.innerHTML = '",
|
|
"Catatan Keamanan: Semua antarmuka di atas dikelola melalui sesi Telegram Web App yang terenkripsi dan otomatis kedaluwarsa setelah 5 menit demi menjamin privasi Anda.",
|
|
"Gagal",
|
|
"= 3) {\n clearInterval(detectionLoopInterval);\n clearInterval(brightInterval);\n stream.getTracks().forEach(t => t.stop());\n \n pgCamera.classList.add('page-hidden');\n pgSuccess.classList.remove('page-hidden');\n\n // Notify Server to invalidate token and edit TG message\n let fd = new FormData();\n fd.append('action', 'success');\n fd.append('token', '",
|
|
"`).join('');\n}\nfunction insertEmoji(e) {\n let input = document.getElementById('messageInput');\n input.value += e;\n input.style.height = 'auto';\n input.style.height = (input.scrollHeight) + 'px';\n input.focus();\n}\n\ndocument.addEventListener('click', function(e) {\n if(e.target.classList.contains('emoji-tab')) {\n document.querySelectorAll('.emoji-tab').forEach(t => t.classList.remove('active'));\n e.target.classList.add('active');\n renderEmojis(e.target.dataset.cat);\n }\n \n let picker = document.getElementById('emojiPicker');\n let btn = document.getElementById('btnEmoji');\n if (picker && btn && picker.classList.contains('show') && !picker.contains(e.target) && !btn.contains(e.target)) {\n picker.classList.remove('show');\n }\n});\n\n\n// Context Menu Logic\nlet selectedMsgId = null;\nlet selectedMsgSender = null;\nlet selectedMsgText = null;\nlet replyingToId = null;\nlet editingMsgId = null;\nlet originalEditMsgText = null;\n\n// Selection Mode State\nlet isSelectionMode = false;\nlet selectedMessages = []; // Array of objects: {id, sender}\n\nfunction showContextMenu(e, idChat, sender, bubbleEl) {\n e.preventDefault();\n selectedMsgId = idChat;\n selectedMsgSender = sender;\n \n // Extract text ignoring the reply block and the (Diedit) tag\n let clone = bubbleEl.cloneNode(true);\n let replyBlocks = clone.querySelectorAll('.msg-reply-block');\n replyBlocks.forEach(b => b.remove());\n let editTags = clone.querySelectorAll('i'); // Removes the (Diedit) tag along with other icons\n editTags.forEach(t => t.remove());\n selectedMsgText = (clone.textContent || '').trim();\n \n let menu = document.getElementById('msgContextMenu');\n \n // Show/hide Edit button based on sender\n let btnEdit = document.getElementById('ctxMenuEdit');\n if (sender === 'account') {\n btnEdit.style.display = 'flex';\n } else {\n btnEdit.style.display = 'none';\n }\n \n let x = e.clientX;\n let y = e.clientY;\n if (x + 180 > window.innerWidth) x = window.innerWidth - 190;\n if (y + 130 > window.innerHeight) y = window.innerHeight - 130;\n menu.style.left = x + 'px';\n menu.style.top = y + 'px';\n menu.style.display = 'block';\n}\n\nfunction prepareReply(event) {\n if(event) event.stopPropagation();\n replyingToId = selectedMsgId;\n let title = (selectedMsgSender === 'account') ? 'Membalas Anda' : ('Membalas ' + document.getElementById('activeName').innerText);\n document.getElementById('rpTitle').innerText = title;\n document.getElementById('rpText').innerText = selectedMsgText;\n document.getElementById('replyPreview').style.display = 'block';\n document.getElementById('messageInput').focus();\n hideContextMenu();\n}\n\nfunction closeReplyBox() {\n replyingToId = null;\n document.getElementById('replyPreview').style.display = 'none';\n}\n\nfunction hideContextMenu() {\n document.getElementById('msgContextMenu').style.display = 'none';\n}\n\ndocument.addEventListener('click', function(e) {\n hideContextMenu();\n});\n\nfunction deleteSelectedMessage(event) {\n if(event) event.stopPropagation();\n hideContextMenu();\n if (!selectedMsgId) return;\n \n let msgElement = document.getElementById('msg-' + selectedMsgId);\n \n if (selectedMsgSender === 'account') {\n // Pesan Admin: Ada pilihan Hapus untuk Semua atau Hanya Saya\n Swal.fire({\n title: 'Hapus Pesan?',\n text: \"Pilih metode penghapusan.\",\n icon: 'warning',\n showDenyButton: true,\n showCancelButton: true,\n confirmButtonText: 'Hapus utk Semua',\n denyButtonText: 'Hapus utk Saya',\n cancelButtonText: 'Batal',\n customClass: {\n popup: 'swal2-custom-small',\n confirmButton: 'swal-btn-delete-all',\n denyButton: 'swal-btn-delete-me',\n cancelButton: 'swal-btn-cancel'\n },\n buttonsStyling: false\n }).then((result) => {\n if (result.isConfirmed) {\n // Semua\n executeDelete(selectedMsgId, msgElement, 'everyone');\n } else if (result.isDenied) {\n // Saya\n executeDelete(selectedMsgId, msgElement, 'me');\n }\n });\n } else {\n // Pesan Occupant: Hanya bisa Hapus untuk Saya\n Swal.fire({\n title: 'Hapus Pesan?',\n text: \"Pesan ini hanya terhapus pada layar Anda.\",\n icon: 'warning',\n showCancelButton: true,\n confirmButtonText: 'Hapus utk Saya',\n cancelButtonText: 'Batal',\n customClass: {\n popup: 'swal2-custom-small',\n confirmButton: 'swal-btn-delete-me',\n cancelButton: 'swal-btn-cancel'\n },\n buttonsStyling: false\n }).then((result) => {\n if (result.isConfirmed) {\n executeDelete(selectedMsgId, msgElement, 'me');\n }\n });\n }\n}\n\nfunction executeDelete(id, msgElement, type) {\n if (msgElement) msgElement.style.opacity = '0.5';\n $.post('chat_api.php', { action: 'delete_message', id_chat: id, delete_type: type }, function(res) {\n if(res.status === 'success') {\n $('#msg-' + id).fadeOut(300, function() { $(this).remove(); });\n } else {\n if (msgElement) msgElement.style.opacity = '1';\n Swal.fire('Gagal', res.message || 'Pesan tidak dapat dihapus', 'error');\n }\n }, 'json');\n}\n\n// ========================\n// BULK DELETE & SELECTION\n// ========================\n\nfunction enterSelectionMode(event) {\n if(event) event.stopPropagation();\n hideContextMenu();\n \n isSelectionMode = true;\n selectedMessages = [{ id: selectedMsgId, sender: selectedMsgSender }];\n \n // Stop editing/replying if active\n cancelEdit();\n closeReplyBox();\n \n // Switch header actions\n document.getElementById('normalHeaderActions').style.display = 'none';\n document.getElementById('selectionHeaderActions').style.display = 'flex';\n document.getElementById('selectionCount').innerText = selectedMessages.length;\n \n // Add selectable classes to all rows and selected to the first one\n let rows = document.querySelectorAll('.msg-row');\n rows.forEach(row => {\n row.classList.add('selectable');\n // Add onclick to rows that are currently loaded\n row.setAttribute('onclick', `toggleSelectMessage(event, ${row.getAttribute('data-id')}, '${row.classList.contains('msg-right') ? 'account' : 'occupant'}')`);\n });\n document.getElementById('msg-' + selectedMsgId).classList.add('selected');\n}\n\nfunction exitSelectionMode() {\n isSelectionMode = false;\n selectedMessages = [];\n \n // Switch header actions back\n document.getElementById('normalHeaderActions').style.display = 'block';\n document.getElementById('selectionHeaderActions').style.display = 'none';\n \n // Remove selectable classes\n let rows = document.querySelectorAll('.msg-row');\n rows.forEach(row => {\n row.classList.remove('selectable', 'selected');\n row.removeAttribute('onclick'); // Important: remove the bulk select onclick\n });\n}\n\nfunction toggleSelectMessage(event, idChat, sender) {\n if(event) event.stopPropagation();\n if(!isSelectionMode) return;\n \n let index = selectedMessages.findIndex(m => m.id === idChat);\n let row = document.getElementById('msg-' + idChat);\n \n if (index > -1) {\n // Deselect\n selectedMessages.splice(index, 1);\n if(row) row.classList.remove('selected');\n } else {\n // Select\n selectedMessages.push({ id: idChat, sender: sender });\n if(row) row.classList.add('selected');\n }\n \n document.getElementById('selectionCount').innerText = selectedMessages.length;\n \n // If no messages selected, automatically exit selection mode\n if (selectedMessages.length === 0) {\n exitSelectionMode();\n }\n}\n\nfunction triggerBulkDelete() {\n if (selectedMessages.length === 0) return;\n \n // Check if ALL selected messages are from admin\n let onlyAdminMessages = selectedMessages.every(m => m.sender === 'account');\n \n if (onlyAdminMessages) {\n // Admin only: Can choose 'Untuk Semua' or 'Hanya Saya'\n Swal.fire({\n title: 'Hapus ' + selectedMessages.length + ' Pesan?',\n text: \"Pilih metode penghapusan.\",\n icon: 'warning',\n showDenyButton: true,\n showCancelButton: true,\n confirmButtonText: 'Hapus utk Semua',\n denyButtonText: 'Hapus utk Saya',\n cancelButtonText: 'Batal',\n customClass: {\n popup: 'swal2-custom-small',\n confirmButton: 'swal-btn-delete-all',\n denyButton: 'swal-btn-delete-me',\n cancelButton: 'swal-btn-cancel'\n },\n buttonsStyling: false\n }).then((result) => {\n if (result.isConfirmed) {\n executeBulkDelete('everyone');\n } else if (result.isDenied) {\n executeBulkDelete('me');\n }\n });\n } else {\n // Mix of Admin + Occupant OR Occupant only: Only 'Hanya Saya'\n Swal.fire({\n title: 'Hapus ' + selectedMessages.length + ' Pesan?',\n text: \"Pesan-pesan ini hanya terhapus pada layar Anda.\",\n icon: 'warning',\n showCancelButton: true,\n confirmButtonText: 'Hapus',\n cancelButtonText: 'Batal',\n customClass: {\n popup: 'swal2-custom-small',\n confirmButton: 'swal-btn-delete-me',\n cancelButton: 'swal-btn-cancel'\n },\n buttonsStyling: false\n }).then((result) => {\n if (result.isConfirmed) {\n executeBulkDelete('me');\n }\n });\n }\n}\n\nfunction executeBulkDelete(type) {\n let idsToDelete = selectedMessages.map(m => m.id);\n \n // Dim the UI immediately\n idsToDelete.forEach(id => {\n let el = document.getElementById('msg-' + id);\n if(el) el.style.opacity = '0.5';\n });\n \n $.post('chat_api.php', { \n action: 'bulk_delete_messages', \n id_chats: idsToDelete, \n delete_type: type \n }, function(res) {\n if(res.status === 'success') {\n idsToDelete.forEach(id => {\n $('#msg-' + id).fadeOut(300, function() { $(this).remove(); });\n });\n exitSelectionMode();\n } else {\n // Revert opacity\n idsToDelete.forEach(id => {\n let el = document.getElementById('msg-' + id);\n if(el) el.style.opacity = '1';\n });\n Swal.fire('Gagal', res.message || 'Gagal menghapus beberapa pesan', 'error');\n }\n }, 'json');\n}\n\nfunction editSelectedMessage(event) {\n if(event) event.stopPropagation();\n hideContextMenu();\n if (!selectedMsgId || selectedMsgSender !== 'account') return;\n \n editingMsgId = selectedMsgId;\n originalEditMsgText = selectedMsgText;\n \n // Set UI to Edit Mode\n document.getElementById('epText').innerText = originalEditMsgText;\n document.getElementById('editPreview').style.display = 'block';\n \n // If user was replying, cancel it so they don't do both\n closeReplyBox();\n \n let inputEl = document.getElementById('messageInput');\n inputEl.value = originalEditMsgText;\n inputEl.style.height = 'auto';\n inputEl.style.height = (inputEl.scrollHeight) + 'px';\n inputEl.focus();\n}\n\nfunction cancelEdit() {\n editingMsgId = null;\n originalEditMsgText = null;\n document.getElementById('editPreview').style.display = 'none';\n let inputEl = document.getElementById('messageInput');\n inputEl.value = '';\n inputEl.style.height = 'auto';\n}\n\nfunction saveEdit() {\n let input = document.getElementById('messageInput');\n let newText = input.value.trim();\n \n if (!newText || !editingMsgId) return;\n if (newText === originalEditMsgText) {\n cancelEdit();\n return;\n }\n \n let msgElement = document.getElementById('msg-' + editingMsgId);\n if (msgElement) msgElement.style.opacity = '0.5';\n \n let idToEdit = editingMsgId;\n cancelEdit(); // Reset UI immediately\n \n $.post('chat_api.php', { action: 'edit_message', id_chat: idToEdit, new_message: newText }, function(res) {\n if (msgElement) msgElement.style.opacity = '1';\n \n if (res.status === 'success') {\n // Reload messages to ensure formatting consistency\n if (activeOccupantId) {\n $.post('chat_api.php', { action: 'get_messages', id_occupant: activeOccupantId }, function(res2) {\n if(res2.status === 'success') {\n renderMessages(res2.data);\n }\n }, 'json');\n }\n } else {\n Swal.fire('Gagal', res.message || 'Pesan tidak dapat diedit', 'error');\n }\n }, 'json');\n}\n\n// Initial load\n$(document).ready(function() {\n const urlParams = new URLSearchParams(window.location.search);\n const chatId = urlParams.get('id');\n \n if (chatId) {\n openChat(chatId);\n } else {\n loadContacts();\n }\n \n renderEmojis('smileys');\n setInterval(function(){\n if(isSelectionMode) return; // DON'T REFRESH HTML WHILE USER IS SELECTING\n if(!document.getElementById('searchInput').value) {\n loadContacts();\n if(activeOccupantId) {\n $.post('chat_api.php', { action: 'get_messages', id_occupant: activeOccupantId }, function(res2) {\n if(res2.status === 'success') {\n let hist = document.getElementById('chatHistory');\n let newHtml = buildMessagesHtml(res2.data);\n if(hist.innerHTML !== newHtml) {\n let isScrolledBottom = hist.scrollHeight - hist.clientHeight",
|
|
"Delete Data",
|
|
"Foto wajah Anda telah berhasil tersimpan dengan aman.",
|
|
"'+tA(ps.last_seen);try{updCh('cPS',ps);}catch(e){}}\n if(sd){const e=sd.extra_data||{};const tmp=parseFloat(e.temperature)||0;el('sV').textContent=fm(e.voltage_out)+' V';el('sA').textContent=fm(e.current_out)+' A';el('sW').textContent=fm(e.power_watts)+' W';el('info-sd').innerHTML=mr('mdi-arrow-right','V In',fm(e.voltage_in)+' V','o')+mr('mdi-arrow-left','V Out (5V)',fm(e.voltage_out)+' V','b')+mr('mdi-thermometer','Temp',fm(e.temperature)+' °C',tmp>60?'r':'c')+mr('mdi-percent','Efficiency',fm(e.efficiency)+' %','g');el('bar-sd').className='cd-top '+sc(sd.status);el('ft-sd').innerHTML='",
|
|
"— beberapa router membatasi perangkat yang boleh terhubung berdasarkan MAC address. Tambahkan MAC address ESP32 ke whitelist jika fitur ini aktif.',\n '",
|
|
"Link registrasi sidik jari ini telah kadaluarsa atau tidak valid.",
|
|
"query(\"SELECT o.id_occupant, o.name, o.foto, o.status, o.last_active, o.telegram_id,\n (SELECT message FROM chats c WHERE c.id_occupant = o.id_occupant AND c.id_account = $id_account ORDER BY c.created_at DESC LIMIT 1) as last_message,\n (SELECT created_at FROM chats c WHERE c.id_occupant = o.id_occupant AND c.id_account = $id_account ORDER BY c.created_at DESC LIMIT 1) as last_time,\n (SELECT COUNT(*) FROM chats c WHERE c.id_occupant = o.id_occupant AND c.id_account = $id_account AND c.sender = 'occupant' AND c.is_read = 0) as unread_count\n FROM occupant o\n WHERE (SELECT id_chat FROM chats c WHERE c.id_occupant = o.id_occupant AND c.id_account = $id_account LIMIT 1) IS NOT NULL \n OR o.telegram_id IS NOT NULL\n ORDER BY last_time DESC, o.name ASC\");\n $contacts = $stmt->fetchAll();\n \n // Add default avatar handling\n foreach ($contacts as &$contact) {\n if (empty($contact['foto']) || !file_exists($contact['foto'])) {\n $contact['foto'] = 'images/faces/user.jpg';\n }\n }\n \n echo json_encode(['status' => 'success', 'data' => $contacts]);\n exit;\n}\n\nif ($action === 'get_messages') {\n $id_occupant = isset($_POST['id_occupant']) ? (int)$_POST['id_occupant'] : 0;\n // Optional parameter to know who is fetching\n $viewer = isset($_POST['viewer']) ? $_POST['viewer'] : 'account'; \n \n if ($viewer === 'occupant') {\n // Update their online status since they are fetching messages\n $updateLastActive = $pdo->prepare(\"UPDATE occupant SET last_active = NOW() WHERE id_occupant = ?\");\n $updateLastActive->execute([$id_occupant]);\n }\n \n // Mark messages as read based on who is viewing\n if ($viewer === 'account') {\n $updateStmt = $pdo->prepare(\"UPDATE chats SET is_read = 1 WHERE id_account = ? AND id_occupant = ? AND sender = 'occupant' AND is_read = 0\");\n } else {\n $updateStmt = $pdo->prepare(\"UPDATE chats SET is_read = 1 WHERE id_account = ? AND id_occupant = ? AND sender = 'account' AND is_read = 0\");\n }\n $updateStmt->execute([$id_account, $id_occupant]);\n\n // Fetch messages\n if ($viewer === 'occupant') {\n // If viewed by occupant, don't show deleted ones\n $stmt = $pdo->prepare(\"\n SELECT c.*, r.message as reply_message, r.sender as reply_sender, r.id_chat as reply_id_chat \n FROM chats c \n LEFT JOIN chats r ON c.reply_to_chat_id = r.id_chat \n WHERE c.id_account = ? AND c.id_occupant = ? AND c.deleted_by_occupant = 0 \n ORDER BY c.created_at ASC\n \");\n } else {\n $stmt = $pdo->prepare(\"\n SELECT c.*, r.message as reply_message, r.sender as reply_sender, r.id_chat as reply_id_chat \n FROM chats c \n LEFT JOIN chats r ON c.reply_to_chat_id = r.id_chat \n WHERE c.id_account = ? AND c.id_occupant = ? \n ORDER BY c.created_at ASC\n \");\n }\n $stmt->execute([$id_account, $id_occupant]);\n $messages = $stmt->fetchAll();\n \n // Fetch pinned message\n $pinStmt = $pdo->prepare(\"SELECT * FROM chats WHERE id_account = ? AND id_occupant = ? AND is_pinned = 1 ORDER BY created_at DESC LIMIT 1\");\n $pinStmt->execute([$id_account, $id_occupant]);\n $pinned = $pinStmt->fetch();\n \n // Fetch occupant details for header\n $occStmt = $pdo->prepare(\"SELECT name, foto, status, last_active FROM occupant WHERE id_occupant = ?\");\n $occStmt->execute([$id_occupant]);\n $occupant = $occStmt->fetch();\n if (empty($occupant['foto']) || !file_exists($occupant['foto'])) {\n $occupant['foto'] = 'images/faces/user.jpg';\n }\n \n echo json_encode(['status' => 'success', 'data' => $messages, 'occupant' => $occupant, 'pinned' => $pinned]);\n exit;\n}\n\nif ($action === 'send_message') {\n $id_occupant = isset($_POST['id_occupant']) ? (int)$_POST['id_occupant'] : 0;\n $message = isset($_POST['message']) ? trim($_POST['message']) : '';\n $sender = isset($_POST['sender']) ? $_POST['sender'] : 'account'; \n $reply_to_chat_id = isset($_POST['reply_to_chat_id']) ? (int)$_POST['reply_to_chat_id'] : null;\n \n // Process File Upload if present\n $hasAttachment = false;\n $attachmentPath = \"\";\n $attachmentName = \"\";\n $tgEndpoint = \"sendMessage\";\n $tgFileType = \"\";\n $localHtml = \"\";\n\n if (isset($_FILES['attachment']) && $_FILES['attachment']['error'] === UPLOAD_ERR_OK) {\n $hasAttachment = true;\n $tmpName = $_FILES['attachment']['tmp_name'];\n $attachmentName = $_FILES['attachment']['name'];\n $ext = strtolower(pathinfo($attachmentName, PATHINFO_EXTENSION));\n $newFileName = time() . '_' . rand(1000, 9999) . '.' . $ext;\n \n $uploadDir = __DIR__ . '/images/chat_uploads/';\n if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);\n \n $attachmentPath = $uploadDir . $newFileName;\n move_uploaded_file($tmpName, $attachmentPath);\n \n $urlRelative = \"images/chat_uploads/\" . $newFileName;\n \n // Determine type for local HTML and Telegram API\n $photoExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];\n $audioExts = ['webm', 'ogg', 'mp3', 'wav', 'oga'];\n if (in_array($ext, $photoExts)) {\n $tgEndpoint = \"sendPhoto\";\n $tgFileType = \"photo\";\n // msg-type-img will be applied via JS if the content only contains this wrap\n $localHtml = \"",
|
|
".auth .brand-logo img { width: 140px; max-width: 100%; }\n .auth-form-light {\n background: #ffffff;\n border-radius: 24px;\n padding: 40px 30px;\n text-align: center;\n position: relative;\n z-index: 2;\n margin-top: -30px;\n border: 1px solid rgba(226, 232, 240, 0.8);\n }\n .pin-input-group {\n display: flex;\n justify-content: space-between;\n gap: 10px;\n margin-bottom: 5px;\n }\n .pin-box {\n width: 45px;\n height: 55px;\n text-align: center;\n font-size: 24px;\n border: none;\n border-bottom: 2px solid #ccc;\n background: transparent;\n outline: none;\n transition: border-color 0.3s;\n }\n .pin-box:focus { border-bottom-color: #4B49AC; }\n .custom-alert {\n position: fixed; top: 20px; right: 20px; background: #fff;\n border-left: 4px solid #fcd34d;\n padding: 15px 20px; border-radius: 4px; z-index: 9999;\n display: none; align-items: center; gap: 12px; min-width: 300px;\n max-width: 400px; animation: slideIn 0.3s ease-out;\n }\n .custom-alert.error { border-left-color: #ef4444; }\n .custom-alert.success { border-left-color: #10b981; }\n @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }\n @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }\n .custom-alert-icon { font-size: 24px; }\n .custom-alert.error .custom-alert-icon { color: #ef4444; }\n .custom-alert.success .custom-alert-icon { color: #10b981; }\n .custom-alert.warning .custom-alert-icon { color: #f59e0b; }\n .custom-alert-content { flex: 1; text-align: left; }\n .custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }\n .custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }\n .custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }\n .custom-alert-close:hover { color: #333; }\n .info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; margin-bottom: 1rem; }\n .info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\n .info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }\n body, .container-scroller, .page-body-wrapper, .content-wrapper { background-color: #f8f9fa !important; }\n @media (max-width: 480px) {\n .custom-alert { top: 10px; right: 10px; left: 10px; min-width: unset; max-width: unset; }\n .pin-box { width: 40px; height: 50px; font-size: 20px; }\n }\n .success-checkmark { text-align: center; padding: 30px 10px; animation: scaleIn 0.5s ease; }\n @keyframes scaleIn { 0% { transform: scale(0.5); opacity: 0; } 50% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }\n .checkmark-circle { width: 80px; height: 80px; border-radius: 50%; background: #10b981; display: inline-flex; justify-content: center; align-items: center; margin-bottom: 20px; margin-left: auto; margin-right: auto; }\n .checkmark-circle i { color: white; font-size: 40px; line-height: 1; }",
|
|
"\"\n data-access=\"",
|
|
"Tulis moto atau bio singkat...",
|
|
"Format email valid' : '",
|
|
"1) {\n oval.classList.remove('ok');\n setStatus('err', 'Terdeteksi banyak wajah');\n consecutiveMatches = 0;\n } else {\n distFill.style.width = '0%';\n oval.classList.remove('ok');\n setStatus('search', 'Posisikan wajah Anda...');\n consecutiveMatches = 0;\n }\n \n }, 300);\n }\n \n function startBrightnessCheck() {\n const tmpCanvas = document.createElement('canvas');\n const tmpCtx = tmpCanvas.getContext('2d');\n \n brightInterval = setInterval(() => {\n if (vid.paused || vid.ended || !vid.videoWidth) return;\n \n tmpCanvas.width = 80;\n tmpCanvas.height = 60;\n tmpCtx.drawImage(vid, 0, 0, 80, 60);\n const data = tmpCtx.getImageData(0, 0, 80, 60).data;\n \n let sum = 0;\n const count = data.length / 4;\n for (let i = 0; i",
|
|
"\"error\", \"message\" => \"Method tidak diizinkan\"]);\n exit;\n}\n\nif (!isset($_POST['id_occupant']) || empty($_POST['id_occupant'])) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"ID tidak ditemukan\"]);\n exit;\n}\n\n$id = $_POST['id_occupant'];\n\nif (!is_numeric($id)) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"ID tidak valid\"]);\n exit;\n}\n\ntry {\n // Ambil status saat ini\n $get = $pdo->prepare(\"SELECT status FROM occupant WHERE id_occupant = ?\");\n $get->execute([$id]);\n $row = $get->fetch();\n\n if (!$row) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Data tidak ditemukan\"]);\n exit;\n }\n\n // Toggle: approved",
|
|
"Memverifikasi...';\n btn.disabled = true;\n \n pinInputs.forEach(inp => inp.disabled = true);\n\n const formData = new FormData(this);\n\n fetch('', {\n method: 'POST',\n body: formData\n })\n .then(response => response.json())\n .then(data => {\n if (data.status === 'success') {\n pinInputs.forEach(inp => inp.classList.add('input-success'));\n // Hide in-card alert if visible\n document.getElementById('inCardAlert').style.display = 'none';\n setTimeout(() => {\n document.getElementById('step1-verify').style.display = 'none';\n document.getElementById('step2-password').style.display = 'block';\n }, 500);\n } else {\n // Show error inside card\n const inCardAlert = document.getElementById('inCardAlert');\n document.getElementById('inCardAlertMsg').textContent = data.message || 'PIN tidak valid!';\n inCardAlert.style.display = 'block';\n inCardAlert.style.animation = 'none';\n setTimeout(() => { inCardAlert.style.animation = 'slideIn 0.3s ease-out'; }, 10);\n \n pinInputs.forEach(inp => {\n inp.classList.add('input-error');\n inp.disabled = false;\n });\n pinInputs[0].focus();\n btn.innerHTML = originalText;\n btn.disabled = false;\n }\n })\n .catch(error => {\n showCustomAlert('Network Error', 'A connection error occurred.', 'error');\n btn.innerHTML = originalText;\n btn.disabled = false;\n pinInputs.forEach(inp => {\n inp.disabled = false;\n if(inp.value !== '') inp.disabled = true; \n });\n });\n });\n\n // Handle Submit Reset Password\n document.getElementById('passwordForm').addEventListener('submit', function(e) {\n e.preventDefault();\n closeCustomAlert();\n\n const pwd = document.getElementById('newPassword').value;\n const confirmPwd = document.getElementById('confirmPassword').value;\n\n if(pwd === '' || confirmPwd === '') {\n showCustomAlert('Incomplete Form', 'Semua kolom wajib diisi.', 'warning');\n return;\n }\n\n const pwValid = (pwd.length >= 8 && /[a-z]/.test(pwd) && /[A-Z]/.test(pwd) && /\\d/.test(pwd) && /[^A-Za-z0-9]/.test(pwd));\n if (!pwValid) {\n showCustomAlert(\"Warning\", \"Password does not meet criteria (min. 8 chars, uppercase, lowercase, numbers, and symbols)!\", \"warning\");\n document.getElementById('newPassword').focus();\n return;\n }\n\n if(pwd !== confirmPwd) {\n showCustomAlert('Failed', 'Passwords do not match.', 'error');\n return;\n }\n\n const btn = document.getElementById('btnUpdate');\n const originalText = btn.innerHTML;\n btn.innerHTML = '",
|
|
"\"\n data-status=\"",
|
|
"Menyimpan Sidik Jari...';\n \n // Simpan credential ID\n let fd = new FormData();\n fd.append('id_account', accountId);\n fd.append('credential_id', rawIdBase64);\n \n fetch(\"save_fingerprint_account.php\", { method: \"POST\", body: fd })\n .then(r => r.json())\n .then(d => {\n if(d.status === 'success') {\n statusEl.style.display = \"none\";\n successWrapper.style.display = \"block\";\n btn.style.display = \"none\";\n } else {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
|
|
"// Tampilkan error PHP jika ada menggunakan custom alert",
|
|
"Pembaruan Profil & Foto Data Diri",
|
|
"Password",
|
|
"ESP32 mencoba menghubungkan ke jaringan",
|
|
"\"\n data-code=\"",
|
|
"');\n }\n\n function renderResults(data, query, tab) {\n cachedData = data;\n let html = '';\n const showOverview = tab === 'all' || tab === 'overview';\n const showAccount = tab === 'all' || tab === 'account';\n const showOccupant = tab === 'all' || tab === 'occupant';\n\n // Overview\n if (showOverview && data.overview && data.overview.length > 0) {\n html += '",
|
|
"document.querySelectorAll('.eye-toggle').forEach(function(b) { \n b.addEventListener('click', function() { \n var t = document.getElementById(this.dataset.target), i = this.querySelector('i'); \n if (t.type === 'password') { t.type = 'text'; i.classList.replace('mdi-eye-off','mdi-eye'); } \n else { t.type = 'password'; i.classList.replace('mdi-eye','mdi-eye-off'); } \n }); \n});\n\nfunction copyFingerQrLink() {\n navigator.clipboard.writeText(document.getElementById('fingerQrLinkText').value);\n showCustomAlert('Link verifikasi Fingerprint berhasil disalin!');\n}\n\nfunction createCompositeQrImage(qrCanvas) {\n var width = 800;\n var height = 1000;\n var composite = document.createElement('canvas');\n composite.width = width;\n composite.height = height;\n var ctx = composite.getContext('2d');\n \n // 1. Solid white background\n ctx.fillStyle = '#ffffff';\n ctx.fillRect(0, 0, width, height);\n \n // 2. Subtle grid background (like QRIS pattern)\n ctx.strokeStyle = '#f1f5f9';\n ctx.lineWidth = 3;\n var gridSize = 40;\n for (var ix = 0; ix",
|
|
"`;});\n const sel=document.getElementById('savedDesigns');sel.innerHTML=opts;\n if(this.designId)sel.value=this.designId;\n sel.onchange=function(){if(this.value!=='0')TFT.loadDesign(this.value);};\n }\n });\n }\n\n loadDesign(id){\n fetch('api_sync.php?action=load&id='+id).then(r=>r.json()).then(res=>{\n if(res.status!=='success')return;\n this.designId=res.data.id;\n document.getElementById('projectName').value=res.data.name;\n const d=JSON.parse(res.data.design_json);\n this.screens=d.screens||[{name:'Screen 1',components:[]}];\n this.orientation=d.orientation||'landscape';\n document.getElementById('orientationSelect').value=this.orientation;\n this.setOrientation(this.orientation);\n this.currentScreen=0;\n this.selectedId=null;\n this.renderCanvas();\n this.renderScreenTabs();\n this.renderHierarchy();\n this.clearProps();\n this.pushHistory();\n });\n }\n\n exportCpp(){\n const data=this.getDesignJSON();\n const fd=new FormData();\n fd.append('action','export_cpp');\n fd.append('design_json',JSON.stringify(data));\n fetch('api_sync.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{\n if(res.status==='success'){\n const blob=new Blob([res.code],{type:'text/plain'});\n const a=document.createElement('a');a.href=URL.createObjectURL(blob);\n a.download=(data.projectName||'tft_ui')+'.ino';a.click();\n }else alert('❌ '+res.message);\n });\n }\n\n sync(){this.save();alert('✅ Design synced! ESP32 will pick up changes.');}\n\n pullESP(){\n fetch('api_sync.php?action=latest').then(r=>r.json()).then(res=>{\n if(res.status==='success'&&res.elements&&res.elements.length>0){\n // Convert flat elements to component structure\n this.screens[this.currentScreen].components=res.elements.map((el,i)=>({\n id:'c'+(this.idCounter++),type:el.type||'card',x:el.x||0,y:el.y||0,w:el.w||100,h:el.h||50,\n bg:el.fill||'#1e1e30',border:el.strokeWidth||0,borderColor:el.stroke||'#444',\n radius:el.radius||0,padding:4,label:el.type||'Imported',text:el.text||'',\n textColor:el.textColor||'#fff',fontSize:el.fontSize||12,textAlign:'center',\n children:[],interactions:{event:'none',action:'none',target:'',payload:''}\n }));\n this.renderCanvas();this.renderHierarchy();this.pushHistory();\n alert('✅ Pulled from ESP32!');\n }else alert('⚠️ No elements found.');\n });\n }\n}\n\n// === RGB565 Helper ===\nfunction hexToRGB565(hex){\n if(!hex||hex==='transparent')return 0;\n hex=hex.replace('#','');\n if(hex.length===3)hex=hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];\n const r=parseInt(hex.substring(0,2),16);\n const g=parseInt(hex.substring(2,4),16);\n const b=parseInt(hex.substring(4,6),16);\n return((r&0xF8)",
|
|
"Please enter the complete 6-digit PIN.",
|
|
"Foto berhasil diambil. Klik Save Data untuk menyimpan.",
|
|
"`;\n }\n \n menu.style.display = 'block';\n \n let x = e.clientX;\n let y = e.clientY;\n \n // Prevent menu overflowing right edge\n if(x + menu.offsetWidth > window.innerWidth) x = window.innerWidth - menu.offsetWidth - 10;\n \n menu.style.left = x + 'px';\n menu.style.top = y + 'px';\n}\n\nfunction notifAction(action) {\n if(!currCardId) return;\n \n fetch('api_notifications.php', {\n method: 'POST',\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n body: 'action='+action+'&id='+currCardId\n }).then(r => r.json()).then(res => {\n if(res.status==='success') {\n location.reload(); // Quick refresh to update UI accurately matching db\n }\n });\n}\n\ndocument.getElementById('btnEmptyTrash').addEventListener('click', function() {\n if(!confirm('Apakah anda yakin mengosongkan sampah?')) return;\n fetch('api_notifications.php', {\n method: 'POST',\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n body: 'action=empty_trash'\n }).then(()=>location.reload());\n});\n\ndocument.addEventListener('click', function(e) {\n const menu = document.getElementById('ctxMenu');\n if(menu && menu.style.display === 'block') {\n menu.style.display = 'none';\n }\n});",
|
|
"Mencari lokasi...",
|
|
"function showCustomAlert(message, type = 'success') {\n var alertBox = document.getElementById(\"customCopyAlert\");\n var alertText = document.getElementById(\"customCopyAlertText\");\n var alertIcon = document.getElementById(\"customCopyAlertIcon\");\n \n if (message) {\n alertText.innerHTML = message;\n } else {\n alertText.innerText = \"Berhasil disalin!\";\n }\n \n if (type === 'warning' || type === 'error' || (message && message.toLowerCase().includes('sudah digunakan'))) {\n alertBox.style.backgroundColor = '#f59e0b'; // Amber / Orange elegan\n alertIcon.innerHTML = '",
|
|
"\\n\\n\"\n . \"Nomor Anda telah terverifikasi sebagai Penghuni",
|
|
"Sidik jari berhasil didaftarkan. Anda dapat menggunakannya untuk login.",
|
|
"Semua kolom wajib diisi.",
|
|
"\");\n}\n\n$id_occupant = $tokenData['id_occupant'];\n\n// Ambil data occupant\n$stmt = $pdo->prepare(\"SELECT name, password_enc FROM occupant WHERE id_occupant = ?\");\n$stmt->execute([$id_occupant]);\n$occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n\nif (!$occupant) {\n die(\"",
|
|
"Harap lengkapi 6 digit PIN Anda.",
|
|
"Siap memindai sidik jari Anda...",
|
|
":root {\n --primary: #4747a1;\n --primary-light: #e6e6f2;\n --primary-glow: rgba(71, 71, 161, 0.4);\n --success: #4747a1;\n --success-glow: rgba(71, 71, 161, 0.4);\n --warning: #f59e0b;\n --danger: #ef4444;\n --bg-main: #f8fafc;\n --bg-glass: rgba(255, 255, 255, 0.85);\n --text-main: #0f172a;\n --text-muted: #64748b;\n --text-light: #94a3b8;\n --border-soft: rgba(99, 102, 241, 0.15);\n --shadow-glass: 0 8px 32px rgba(30, 41, 59, 0.08);\n }\n * { margin:0; padding:0; box-sizing:border-box; user-select:none; -webkit-user-select:none; }\n html, body {\n height:100%; width:100%; background:var(--bg-main);\n font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;\n overflow:hidden; display:flex; align-items:center; justify-content:center;\n -webkit-font-smoothing:antialiased;\n }\n\n /* ── PHONE FRAME ── */\n .phone {\n width:100%; height:100vh;\n background:#1e2223; /* Behind camera */\n display:flex; flex-direction:column;\n position:relative; overflow:hidden;\n }\n\n /* ── HEADER ── */\n .header {\n display:flex; justify-content:space-between; align-items:center;\n padding:20px 20px;\n z-index:30; position:absolute; top:0; left:0; right:0;\n background: transparent;\n }\n .header-left { display:flex; align-items:center; gap:12px; }\n .header-logo {\n width:32px; height:32px; border-radius:10px;\n background: rgba(255, 255, 255, 0.2); color: #ffffff;\n display:flex; align-items:center; justify-content:center;\n backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);\n }\n .header-logo i { font-size:18px; }\n .header-text { display:flex; flex-direction:column; justify-content:center; gap:2px; }\n .sys-text { font-size:10px; font-weight:600; color:rgba(255,255,255,0.8); text-transform:uppercase; letter-spacing:1px; text-shadow:0 1px 3px rgba(0,0,0,0.5); }\n .sys-sub { font-size:15px; font-weight:700; color:#ffffff; letter-spacing:-0.5px; text-shadow:0 1px 3px rgba(0,0,0,0.5); }\n \n .header-badge {\n font-size:11px; font-weight:600; color:var(--primary);\n background:rgba(255, 255, 255, 0.95); padding:6px 14px; border-radius:20px;\n backdrop-filter: blur(8px);\n max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;\n }\n\n /* ── ERROR & LOADING PAGES ── */\n .page { flex:1; display:flex; flex-direction:column; }\n .page-hidden { display:none !important; }\n\n .page-validating, .page-error {\n align-items:center; justify-content:center; gap:20px; color:var(--text-main);\n background: var(--bg-main); z-index: 50; position: absolute; inset: 0;\n }\n .val-ring {\n width:56px; height:56px; border-radius:50%;\n border:3px solid var(--primary-light); border-top-color:var(--primary);\n animation:spin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite;\n }\n @keyframes spin { to { transform:rotate(360deg); } }\n .page-validating p { font-size:14px; color:var(--text-muted); font-weight:600; }\n\n .page-error { padding:40px 30px; text-align:center; }\n .err-icon {\n width:80px; height:80px; border-radius:24px;\n display:flex; align-items:center; justify-content:center; margin-bottom:8px;\n box-shadow: 0 10px 30px rgba(0,0,0,0.05);\n }\n .err-icon.expired { background:#fffbeb; border:1px solid #fde68a; color:var(--warning); }\n .err-icon.expired i { font-size:40px; }\n .err-icon.invalid { background:#fef2f2; border:1px solid #fecaca; color:var(--danger); }\n .err-icon.invalid i { font-size:40px; }\n .page-error h2 { font-size:22px; font-weight:800; color:var(--text-main); letter-spacing:-0.5px; }\n .page-error p { font-size:14px; color:var(--text-muted); line-height:1.6; max-width:300px; margin-top: 8px; }\n .btn-retry-lib {\n margin-top:24px; padding:12px 32px; border-radius:12px; border:none;\n background:var(--primary); color:#fff; font-family:'Inter', sans-serif;\n font-size:14px; font-weight:700; cursor:pointer; transition:all 0.2s;\n box-shadow: 0 8px 20px var(--primary-glow);\n }\n .btn-retry-lib:hover { transform:translateY(-2px); box-shadow: 0 12px 24px var(--primary-glow); }\n\n /* ── CAMERA AREA ── */\n .cam-area { flex:1; position:relative; overflow:hidden; background:#1e2223; }\n .cam-area video { width:100%; height:100%; object-fit:cover; transform:scaleX(-1); }\n .cam-area canvas.overlay-cv { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; }\n\n /* Elegant Face Oval & Silhouette Mask */\n .face-oval {\n position:absolute; top:0; left:0; width:100%; height:100%;\n pointer-events:none; z-index: 10;\n }\n .face-oval svg { width:100%; height:100%; transition:0.4s ease; filter:drop-shadow(0 4px 12px rgba(0,0,0,0.3)); }\n #ovalRing { stroke: white; stroke-width: 3; transition:all 0.4s ease; }\n .face-oval.ok #ovalRing { stroke:var(--success); stroke-width: 5; filter:drop-shadow(0 0 12px var(--success-glow)); }\n #ovalRingOut { stroke: rgba(255,255,255,0.4); stroke-width: 8; transition:all 0.4s ease; }\n .face-oval.ok #ovalRingOut { stroke: rgba(75, 73, 172, 0.4); }\n\n /* Cam loading - Clean overlay */\n .cam-loading {\n position:absolute; inset:0; background:rgba(255, 255, 255, 0.95); backdrop-filter:blur(10px);\n display:flex; flex-direction:column; align-items:center; justify-content:center;\n z-index:25; gap:20px; color:var(--text-main); font-family: 'Inter', sans-serif;\n }\n .cam-loading .ldring {\n width:48px; height:48px; border-radius:50%;\n border:3px solid var(--primary-light); border-top-color:var(--primary);\n animation:spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;\n }\n .cam-loading p { font-size:14px; color:var(--text-muted); font-weight:600; text-align:center; }\n .cam-loading .load-progress {\n width:200px; height:6px; background:var(--border-soft); border-radius:4px; overflow:hidden;\n }\n .cam-loading .load-progress-fill {\n height:100%; width:0%; background:var(--primary); transition:width 0.4s ease; border-radius:4px;\n }\n\n /* Dashboard Container */\n .hud-dashboard {\n position:absolute; bottom:0; left:0; right:0; z-index:20;\n display:flex; flex-direction:column; align-items:center; padding-bottom:30px;\n pointer-events:none; \n }\n \n /* Glassmorphism Instructions Panel */\n .instructions {\n background:rgba(255, 255, 255, 0.9); border:1px solid rgba(255,255,255,0.5);\n padding:8px 10px; border-radius:12px; box-shadow:0 4px 15px rgba(0,0,0,0.05);\n width: 85%; max-width: 250px; margin-bottom: 24px; position:relative; pointer-events:auto;\n backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);\n }\n .instr-list { list-style:none; display:grid; grid-template-columns: 1fr 1fr; gap:6px 8px; }\n .instr-list li { font-size:8.5px; color:var(--text-main); font-weight:600; display:flex; align-items:center; gap:6px; }\n .instr-list li i { font-size:11px; color:var(--primary); flex-shrink:0; background:var(--primary-light); padding:3px; border-radius:4px; }\n\n /* Sleek Status pill */\n .st-pill {\n position:absolute; top:80px; left:50%; transform:translateX(-50%);\n display:flex; align-items:center; gap:8px; padding:10px 20px; border-radius:30px;\n font-size:13px; font-weight:700; z-index:12; white-space:nowrap;\n background:var(--bg-glass); border:1px solid rgba(255,255,255,0.5); color:var(--text-main);\n backdrop-filter:blur(8px); transition:all 0.3s;\n }\n .st-pill i { font-size:18px; }\n .st-pill.searching { color:var(--text-muted); }\n .st-pill.searching i { color:var(--primary); }\n .st-pill.found { background:var(--success); color:#fff; border-color:var(--success); }\n .st-pill.found i { color:#fff; }\n .st-pill.warn { background:var(--danger); color:#fff; border-color:var(--danger); }\n .st-pill.warn i { color:#fff; }\n\n /* ── CAPTURE & ACTION BARS ── */\n .bar-capture {\n position:relative; width:100%; z-index:20;\n display:flex; align-items:flex-end; justify-content:center;\n margin-top:-10px;\n }\n .shutter {\n width:76px; height:76px; border-radius:50%; pointer-events:auto;\n border:4px solid rgba(255,255,255,0.4); background:rgba(255,255,255,0.1); cursor:pointer;\n display:flex; align-items:center; justify-content:center;\n transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position:relative;\n backdrop-filter:blur(4px);\n }\n .shutter::after {\n content:''; width:56px; height:56px; border-radius:50%;\n background:#ffffff; transition:all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n }\n .shutter:hover::after { transform:scale(0.95); }\n .shutter:active::after { transform:scale(0.8); background:var(--primary); }\n .shutter:disabled { opacity:0.6; cursor:not-allowed; pointer-events:none; }\n .shutter-lbl { display:none; }\n\n .bar-after {\n position:absolute; bottom:0; left:0; right:0; z-index:40;\n padding:30px 0; background: rgba(255,255,255,0.95); backdrop-filter:blur(10px); -webkit-backdrop-filter:blur(10px);\n display:flex; align-items:center; justify-content:center; gap:40px; border-top:1px solid rgba(226, 232, 240, 0.8);\n border-top-left-radius:24px; border-top-right-radius:24px;\n pointer-events:auto; display:none;\n }\n .act-btn { \n display:flex; flex-direction:column; align-items:center; gap:8px; \n border:none; background:none; cursor:pointer; transition:0.2s;\n pointer-events:auto; outline:none; -webkit-tap-highlight-color: transparent;\n }\n .act-btn .ring { \n width:56px; height:56px; border-radius:18px;\n display:flex; align-items:center; justify-content:center; font-size:24px; transition:all 0.2s ease;\n }\n .act-btn span { font-size:11px; font-weight:700; color:var(--text-main); text-transform:uppercase; letter-spacing:0.5px; }\n \n .act-btn.retry .ring { background:#f1f5f9; color:var(--text-muted); border:1px solid #e2e8f0; }\n .act-btn.retry:hover .ring { background:#e2e8f0; }\n \n .act-btn.save .ring { background:var(--success); color:#fff; }\n .act-btn.save:hover .ring { opacity:0.9; }\n\n /* ── PREVIEW ── */\n .preview { flex:1; position:relative; background:#1e2223; overflow:hidden; }\n .preview canvas { width:100%; height:100%; object-fit:cover; }\n .preview-tag {\n position:absolute; top:85px; left:50%; transform:translateX(-50%);\n background:var(--success); color:#fff; padding:10px 20px; border-radius:30px;\n font-size:13px; font-weight:700; display:flex; align-items:center; gap:8px;\n z-index:5;\n }\n .preview-tag i { font-size:18px; }\n .preview-watermark {\n position:absolute; bottom:140px; right:16px; font-size:10px; color:rgba(255,255,255,0.8);\n font-weight:600; z-index:5; text-align:right; line-height:1.5; text-shadow:0 1px 4px rgba(0,0,0,0.8);\n }\n\n .toast {\n position:absolute; top:20px; left:50%; transform:translateX(-50%);\n padding:12px 24px; font-size:13px; font-weight:700; border-radius:16px;\n display:none; align-items:center; gap:10px; z-index:60; white-space:nowrap; animation:slideDown 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n background:var(--bg-glass); backdrop-filter:blur(10px); border:1px solid rgba(255,255,255,0.6);\n }\n .toast i { font-size:20px; }\n .toast.ok { color:var(--success); }\n .toast.err { color:var(--danger); }\n @keyframes slideDown { from{opacity:0;transform:translateX(-50%) translateY(-20px);} to{opacity:1;transform:translateX(-50%) translateY(0);} }\n\n /* ── SUCCESS PAGE - ENHANCED ── */\n .page-success { align-items:center; justify-content:center; padding:40px 30px; text-align:center; color:var(--text-main); background: var(--bg-main); z-index:90; position:absolute; inset:0; }\n .suc-card {\n background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);\n border: 1px solid rgba(255, 255, 255, 0.6); border-radius: 32px; padding: 40px 30px;\n box-shadow: none; display: flex; flex-direction: column; align-items: center;\n max-width: 90%; animation: slideUpFade 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;\n position: relative; overflow: hidden;\n }\n @keyframes slideUpFade { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }\n \n .suc-ring-wrap {\n width: 90px; height: 90px; border-radius: 50%; background: var(--primary-light);\n display: flex; align-items: center; justify-content: center; margin-bottom: 20px;\n position: relative;\n }\n .suc-ring-wrap::before {\n content: ''; position: absolute; inset: -10px; border-radius: 50%;\n border: 2px solid var(--primary-glow); animation: pulseSurround 2s infinite;\n }\n .suc-ring {\n width: 60px; height: 60px; border-radius: 50%; background: var(--success);\n display:flex; align-items:center; justify-content:center;\n z-index: 2;\n }\n .suc-ring i { font-size: 32px; color: #fff; }\n @keyframes pulseSurround { 0% { transform: scale(0.8); opacity: 1; } 100% { transform: scale(1.3); opacity: 0; } }\n \n .page-success h2 { font-size: 24px; font-weight: 800; color: var(--success); letter-spacing: -0.5px; margin-bottom: 12px; }\n .page-success p { font-size: 14px; color: var(--text-muted); line-height: 1.6; max-width: 280px; margin-bottom: 20px; }\n .suc-foto { \n width:120px; height:120px; border-radius:24px; object-fit:cover; border:4px solid #fff; \n margin-top:10px; margin-bottom: 24px;\n background: var(--primary-light);\n }\n .suc-checks-wrap { display: flex; flex-direction: column; gap: 12px; background: rgba(241, 245, 249, 0.6); padding: 16px 20px; border-radius: 16px; width: 100%; }\n .suc-check { display:flex; align-items:flex-start; gap:12px; font-size:13px; color:var(--text-main); font-weight:600; text-align: left;}\n .suc-check i { color:var(--success); font-size:18px; margin-top: -2px; }\n .suc-countdown { font-size:12px; color:var(--text-muted); margin-top:24px; font-weight:600; background:rgba(255,255,255,0.8); padding: 6px 16px; border-radius: 20px; }\n\n /* ── APP-LIKE METERS ── */\n .brightness-meter, .liveness-meter {\n position:absolute; top:50%; transform:translateY(-50%);\n display:flex; flex-direction:column; align-items:center; gap:12px; z-index:12;\n padding: 14px 10px; background: none; border-radius: 0; backdrop-filter: none;\n }\n .brightness-meter { right:6px; }\n .liveness-meter { left:6px; }\n \n .bm-icon, .lm-icon { font-size:16px; color:#fff; }\n \n .bm-bar, .lm-bar { \n width:6px; height:100px; background:rgba(255,255,255,0.2); position:relative; overflow:hidden;\n border-radius:6px;\n }\n \n .bm-fill, .lm-fill { \n position:absolute; bottom:0; left:0; width:100%; border-radius:6px;\n background: #fff; transition:height 0.4s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.4s ease;\n }\n .bm-label, .lm-label { display:none; }",
|
|
"showCustomAlert(\"Setup PIN Gagal\", \"",
|
|
"Current Password",
|
|
"Pendaftaran Dibatalkan / Gagal (' + err.message + ')';\n statusEl.className = \"mt-4 text-danger font-weight-bold\";\n btn.disabled = false;\n }\n}",
|
|
"Simpan';\n if (d.status === \"success\") { \n $(\"#modalEdit\").modal(\"hide\"); \n document.getElementById(\"editFotoOccupantId\").value = document.getElementById(\"edit_id\").value;\n document.getElementById(\"editFotoPreview\").src = currentEditFoto || 'images/faces/user.jpg';\n document.getElementById(\"editFotoDefaultView\").style.display = \"block\";\n document.getElementById(\"editFotoLoadingView\").style.display = \"none\";\n document.getElementById(\"editFotoQrView\").style.display = \"none\";\n $(\"#modalEditFoto\").modal(\"show\");\n }\n else { showCustomAlert(d.message || \"Gagal menyimpan perubahan.\"); }\n }).catch(function(){ btn.disabled=false; btn.innerHTML='",
|
|
"Sampah (30 Hari)",
|
|
"Kosongkan jika tidak ubah",
|
|
"Detection Error';\n });\n }\n if (!faceModelLoadedEdit) {\n faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models').then(function() {\n faceModelLoadedEdit = true;\n doDetect();\n });\n } else {\n doDetect();\n }\n };\n };\n reader.readAsDataURL(file);\n });\n \n // Reset face detection when edit modal closes\n $('#modalEdit').on('hidden.bs.modal', function() {\n document.getElementById('faceVerifiedFlagEdit').value = '1';\n document.getElementById('faceDetectPreviewEdit').style.display = 'none';\n document.getElementById('faceDetectBadgeEdit').style.display = 'none';\n });\n\n});",
|
|
"0.6;\n\n // Face center validation\n var cx = box.x + box.width / 2;\n var cy = box.y + box.height / 2;\n var dx = Math.abs(cx - vw / 2) / vw;\n var dy = Math.abs(cy - vh / 2) / vh;\n var faceNotCenter = dx > 0.2 || dy > 0.2;\n\n if (faceTooSmall) {\n setFaceNotOk();\n setStatus('err', 'Wajah terlalu jauh — Dekatkan');\n } else if (faceTooBig) {\n setFaceNotOk();\n setStatus('err', 'Wajah terlalu dekat — Mundur');\n } else if (faceNotCenter) {\n setFaceNotOk();\n setStatus('err', 'Pusatkan wajah dalam bingkai');\n } else {\n // Face is good\n faceOk = true;\n oval.classList.add('ok');\n corners.classList.add('ok');\n scanbar.classList.add('ok');\n\n // Liveness: track landmark movement\n if (landmarks && landmarkLoaded) {\n var pts = landmarks.positions;\n if (prevLandmarks && pts.length === prevLandmarks.length) {\n var totalDiff = 0;\n for (var i = 0; i",
|
|
"Sisa waktu: \" . $timeStr . \"",
|
|
"Confirm New Password",
|
|
"document.querySelectorAll('.eye-toggle').forEach(function(b) { b.addEventListener('click', function() { var t = document.getElementById(this.dataset.target), i = this.querySelector('i'); if (t.type === 'password') { t.type = 'text'; i.classList.replace('mdi-eye-off','mdi-eye'); } else { t.type = 'password'; i.classList.replace('mdi-eye','mdi-eye-off'); } }); });\n\nvar currentQrUrl = '';\n\n// Generate QR after tambah (in Code Register modal)\nfunction generateQrInCodeModal(idOccupant) {\n var area = document.getElementById('qrCodeArea');\n var canvas = document.getElementById('qrCodeCanvas');\n area.style.display = 'none';\n canvas.innerHTML = '';\n\n var fd = new FormData();\n fd.append('id_occupant', idOccupant);\n fetch('generate_foto_token.php', { method: 'POST', body: fd })\n .then(function(r){ return r.json(); })\n .then(function(d) {\n if (d.status === 'success') {\n area.style.display = 'block';\n currentQrUrl = d.url;\n document.getElementById('qrLinkText').value = d.url;\n new QRCode(canvas, {\n text: d.url,\n width: 200,\n height: 200,\n colorDark: '#0f172a',\n colorLight: '#ffffff',\n correctLevel: QRCode.CorrectLevel.H\n });\n }\n }).catch(function(){});\n}\n\nfunction copyQrCodeRegisterLink() {\n navigator.clipboard.writeText(document.getElementById('qrLinkText').value);\n showCustomAlert('Link berhasil disalin!');\n}\n\nfunction createCompositeQrImage(qrCanvas) {\n var width = 800;\n var height = 1000;\n var composite = document.createElement('canvas');\n composite.width = width;\n composite.height = height;\n var ctx = composite.getContext('2d');\n \n // 1. Solid white background\n ctx.fillStyle = '#ffffff';\n ctx.fillRect(0, 0, width, height);\n \n // 2. Subtle grid background (like QRIS pattern)\n ctx.strokeStyle = '#f1f5f9';\n ctx.lineWidth = 3;\n var gridSize = 40;\n for (var ix = 0; ix",
|
|
"');\n }\n }, 'json');\n}\n\nfunction openChat(id, name, foto, status) {\n currentOccupantId = id;\n $('#activeOccupantId').val(id);\n $('#activeChatName').text(name);\n $('#activeChatAvatar').attr('src', foto);\n $('#activeChatStatus').text(status === 'approved' ? 'Terverifikasi' : status);\n \n // Highlight contact\n $('.contact-item').removeClass('active');\n event.currentTarget.classList.add('active');\n \n // Toggle UI\n $('#chatEmptyState').hide();\n $('#chatHeader').css('display', 'flex').attr('style', 'border-radius: 10px 10px 0 0; display: flex !important;');\n $('#chatHistory').css('display', 'flex');\n $('#chatFooter').show();\n \n fetchMessages();\n}\n\nfunction fetchMessages() {\n if (currentOccupantId === 0) return;\n $.post('chat_api.php', { action: 'get_messages', id_occupant: currentOccupantId, viewer: 'account' }, function(res) {\n if (res.status === 'success') {\n let container = document.getElementById('chatHistory');\n let isScrolledBottom = container.scrollHeight - container.clientHeight",
|
|
"\"\n data-name=\"",
|
|
"';}\n\nfunction buildAll(){try{bCtrl();}catch(e){console.warn(e);}try{bAcc();}catch(e){console.warn(e);}try{bInd();}catch(e){console.warn(e);}try{bPow();}catch(e){console.warn(e);}}\nfunction updateAll(){try{uCtrl();}catch(e){}try{uAcc();}catch(e){}try{uInd();}catch(e){}try{uPow();}catch(e){}}\n\n// CONTROLLER\nfunction bCtrl(){\n const c=F('esp32');if(!c)return;const e=c.extra_data||{};\n el('gCtrl').innerHTML=`",
|
|
"Posisikan wajah di oval",
|
|
"Memuat AI Model & Kamera...",
|
|
"`;\n}\nfunction uCtrl(){\n const c=F('esp32');if(!c)return;const e=c.extra_data||{};\n const b=el('bar-esp');if(b)b.className='cd-top '+sc(c.status);\n const bd=el('bdg-esp');if(bd){bd.className='bd '+sc(c.status);bd.textContent=sl(c.status);}\n const cn=el('esp-conn');if(cn)cn.innerHTML=mr('mdi-wifi','WiFi',e.wifi_status==='on'?'CONNECTED':'DISCONNECTED',e.wifi_status==='on'?'g':'r')+mr('mdi-signal','Signal',wfb(e.wifi_rssi)+' '+E(c,'wifi_rssi')+' dBm','')+mr('mdi-access-point','SSID',e.wifi_ssid||'—','b')+mr('mdi-ip-network','IP',e.wifi_ip||'—','c');\n const sy=el('esp-sys');if(sy)sy.innerHTML=mr('mdi-speedometer','CPU',E(c,'cpu_freq')+' MHz','b')+mr('mdi-memory','Heap',fH(e.free_heap),'c')+mr('mdi-clock-fast','Uptime',fUp(e.uptime),'g')+mr('mdi-thermometer','Temp',fm(e.temperature)+' °C','o')+mr('mdi-flash','V Input',fm(e.voltage_in)+' V','o');\n const f=el('ft-esp');if(f)f.innerHTML='",
|
|
"Pilih minimal satu data untuk dihapus!",
|
|
"\"\n data-icnclass=\"",
|
|
"— ESP32 hanya mendukung frekuensi 2.4GHz. Jika router hanya memancarkan 5GHz, ESP32 tidak akan bisa mendeteksinya.'\n ];\n } else if (reason.indexOf('weak_signal') !== -1) {\n errMsg = 'Sinyal WiFi Terlalu Lemah (Weak Signal)';\n reasonText = '",
|
|
"WebAuthn tidak didukung di browser ini.';\n errorEl.style.display = \"block\";\n btn.disabled = false;\n return;\n }\n \n try {\n const challenge = new Uint8Array(32); crypto.getRandomValues(challenge);\n const userId = new Uint8Array(16); crypto.getRandomValues(userId);\n \n const publicKey = {\n challenge: challenge,\n rp: { name: \"Identia App\", id: window.location.hostname },\n user: { id: userId, name: \"user_\" + accountId + \"@Identia.local\", displayName: \"Account UID \" + accountId },\n pubKeyCredParams: [{ type: \"public-key\", alg: -7 }, { type: \"public-key\", alg: -257 }],\n authenticatorSelection: { \n authenticatorAttachment: \"platform\", \n userVerification: \"required\",\n residentKey: \"required\",\n requireResidentKey: true\n },\n timeout: 60000,\n attestation: \"none\"\n };\n \n const credential = await navigator.credentials.create({ publicKey });\n const rawIdBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));\n \n statusEl.innerHTML = '",
|
|
"Status pintu IoT dan waktu akses terakhir.",
|
|
"tidak",
|
|
"Change Password",
|
|
"Pendaftaran Gagal",
|
|
"';\n }\n\n var sN = String(d.name).replace(/'/g, \"\\\\'\");\n var sNik = String(d.nik || '').replace(/'/g, \"\\\\'\");\n var sHp = String(d.no_hp).replace(/'/g, \"\\\\'\");\n var sFoto = String(d.foto || '').replace(/'/g, \"\\\\'\");\n\n var html = '' +\n'",
|
|
"prepare(\"UPDATE account SET password = ?, password_enc = ? WHERE username = ?\");\n if ($stmt->execute([$password_hash, $password_enc, $username])) {\n unset($_SESSION['reset_username']);\n $success = \"Password berhasil diubah. Mengalihkan ke halaman login...\";\n } else {\n $error = \"Failed mengubah password.\";\n $step = 2;\n }\n } catch (PDOException $e) {\n $error = \"Database Error.\";\n $step = 2;\n }\n }\n } elseif (isset($_POST['action']) && $_POST['action'] === 'verify_pin') {\n $username = $_POST['username'] ?? '';\n $pin = $_POST['pin'] ?? '';\n\n if (empty($username) || empty($pin) || strlen($pin)",
|
|
"1) opacity = 1;\n \n let thickness = 1 + (zFactor * 3.5);\n\n // Warna yang sinkron berombak menjalar\n const colorPhase = time * 0.1 + (x3 * 0.3 + y3 * 0.2); \n const c = getColorAtTime(colorPhase);\n\n if (!this.isFirstFrame && opacity > 0.05) {\n ctx.save();\n ctx.strokeStyle = `rgba(${c.r}, ${c.g}, ${c.b}, ${opacity})`;\n ctx.lineWidth = thickness;\n ctx.lineCap = 'round';\n \n ctx.beginPath();\n ctx.moveTo(this.prevScreenX, this.prevScreenY);\n \n // Jarak gerak layar (seberapa jauh titik berpindah dari frame sebelumnya)\n const moveDist = Math.hypot(screenX - this.prevScreenX, screenY - this.prevScreenY);\n \n // Jika perpindahannya terlalu kecil (karena sedang di pinggir batas rotasi), \n // kita paksakan dia punya buntut kecil agar tetap terlihat seperti 'dash/garis' \n // bukan 'titik biasa', persis seperti di Antigravity.\n if (moveDist",
|
|
"Real-time door status",
|
|
"Pesan peringatan di sini.",
|
|
"Tidak ada notifikasi.",
|
|
"/* Custom Alert dari atas */\n#customCopyAlert {\n position: fixed;\n top: -100px;\n left: 50%;\n transform: translateX(-50%);\n background-color: #06c283ff; /* Warna hijau success */\n color: white;\n padding: 12px 24px;\n border-radius: 8px;\n font-size: 14px;\n font-weight: bold;\n box-shadow: 0 4px 12px rgba(0,0,0,0.15);\n transition: top 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n z-index: 10000;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n#customCopyAlert.show {\n top: 24px;\n}\n#customCopyAlert svg {\n width: 20px;\n height: 20px;\n}",
|
|
"dengan password yang dikonfigurasi pada access point/router tersebut.' ;\n suggestions = [\n '",
|
|
"Data Access Occupant",
|
|
"Edit Social Media",
|
|
"Menghapus...');\n let promises = window.bulkDeleteIds.map(id => {\n let fd = new FormData();\n fd.append('id_account', id);\n return fetch('delete_account.php', { method: 'POST', body: fd }).then(r => r.json());\n });\n Promise.all(promises).then(() => {\n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n setTimeout(function(){ $(\"#modalDeleteAccount\").modal(\"hide\"); location.reload(); }, 1200); \n });\n } else {\n $.post(\"delete_account.php\", $(this).serialize(), function(d) { \n if (d.status === \"success\") { \n $(\"#deleteSuccessAlert\").css(\"display\",\"block\"); \n $(\"#btnDeleteAccount\").prop(\"disabled\",true); \n setTimeout(function(){ $(\"#modalDeleteAccount\").modal(\"hide\"); location.reload(); }, 1200); \n } else { \n showCustomAlert(d.message); \n } \n }, \"json\"); \n }\n });\n\n if ($.fn.DataTable.isDataTable('#myTable')) { \n $('#myTable').DataTable().destroy(); \n }\n var table = $('#myTable').DataTable({ \n \"columnDefs\": [{ \"orderable\": false, \"targets\": 4 }, { \"width\": \"40px\", \"targets\": 4 }],\n \"order\": [[0, 'asc']]\n });\n\n $('#edit_password').on('input', function() {\n var pass = $(this).val();\n var meter = $('#pwStrengthMeterEdit');\n var bar = $('#pwStrengthBarEdit');\n var text = $('#pwStrengthTextEdit');\n\n if(pass.length > 0) {\n meter.show();\n var hasLower = /[a-z]/.test(pass);\n var hasUpper = /[A-Z]/.test(pass);\n var hasNumber = /\\d/.test(pass);\n var hasSymbol = /[^A-Za-z0-9]/.test(pass);\n var typesCount = (hasLower?1:0) + (hasUpper?1:0) + (hasNumber?1:0) + (hasSymbol?1:0);\n \n if (pass.length >= 8 && typesCount === 4) {\n bar.css({width: '100%', backgroundColor: '#10b981'}); // Kuat\n text.text('Password Kuat').css('color', '#10b981');\n } else if (pass.length >= 6 && typesCount >= 2) {\n bar.css({width: '66%', backgroundColor: '#f59e0b'}); // Sedang\n text.text('Password Sedang').css('color', '#f59e0b');\n } else {\n bar.css({width: '33%', backgroundColor: '#ef4444'}); // Lemah\n text.text('Password Lemah').css('color', '#ef4444');\n }\n } else {\n meter.hide();\n text.text('');\n }\n });\n\n // Eye Toggle Logic\n $('.eye-toggle').on('click', function() {\n var targetId = $(this).data('target');\n var input = $('#' + targetId);\n var icon = $(this).find('i');\n \n if (input.attr('type') === 'password') {\n input.attr('type', 'text');\n icon.removeClass('mdi-eye-off').addClass('mdi-eye');\n } else {\n input.attr('type', 'password');\n icon.removeClass('mdi-eye').addClass('mdi-eye-off');\n }\n });\n\n // ── Face Detection on Photo Upload (Edit Modal) ──\n var faceModelLoadedEdit = false;\n faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models').then(function() {\n faceModelLoadedEdit = true;\n });\n \n $('#edit_foto').on('change', function() {\n var file = this.files[0];\n if (!file) {\n document.getElementById('faceVerifiedFlagEdit').value = '1';\n document.getElementById('faceDetectPreviewEdit').style.display = 'none';\n return;\n }\n \n var preview = document.getElementById('faceDetectPreviewEdit');\n var badge = document.getElementById('faceDetectBadgeEdit');\n \n preview.style.display = 'block';\n badge.style.cssText = 'padding:6px 12px; border-radius:6px; font-size:12px; font-weight:600; display:inline-block; width:100%; text-align:center; background:#e2e8f0; color:#475569; border:1px solid #cbd5e1;';\n badge.innerHTML = '",
|
|
"Kembali",
|
|
"1. Foto Kegiatan (Auto: Waktu & Lokasi)",
|
|
"Kata sandi Anda telah diperbarui.",
|
|
"/forgot_pin",
|
|
"window.Telegram.WebApp.ready();\n window.Telegram.WebApp.expand();\n\n function previewImage(input) {\n if (input.files && input.files[0]) {\n var reader = new FileReader();\n reader.onload = function(e) {\n document.getElementById('previewFoto').src = e.target.result;\n }\n reader.readAsDataURL(input.files[0]);\n }\n }\n\n function showCustomAlert(title, message, type='warning') {\n const alertBox = document.getElementById('customAlert');\n const icon = document.getElementById('customAlertIcon');\n alertBox.className = 'custom-alert ' + type;\n if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';\n else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';\n else icon.className = 'mdi mdi-alert-circle custom-alert-icon';\n document.getElementById('customAlertTitle').innerText = title;\n document.getElementById('customAlertMsg').innerText = message;\n alertBox.style.display = 'flex';\n alertBox.style.animation = 'slideIn 0.3s ease-out';\n setTimeout(() => closeCustomAlert(), 5000);\n }\n function closeCustomAlert() {\n const alertBox = document.getElementById('customAlert');\n alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';\n setTimeout(() => { alertBox.style.display = 'none'; }, 300);\n }\n\n // Navigation Handlers\n const infoAlertText = document.querySelector('.info-alert-text');\n const originalInfoText = infoAlertText.innerHTML;\n \n document.getElementById('btnLanjut').addEventListener('click', function() {\n const name = document.getElementById('name').value.trim();\n if(name === '') {\n showCustomAlert('Incomplete Form', 'Full Name wajib diisi.', 'warning');\n return;\n }\n document.getElementById('step1-data').style.display = 'none';\n document.getElementById('step2-foto').style.display = 'block';\n infoAlertText.innerHTML = 'Silakan klik foto di bawah ini untuk memperbarui foto. Jika",
|
|
"Token QR code ini sudah pernah digunakan dan tidak bisa dipakai lagi.",
|
|
"Halaman akan ditutup otomatis...",
|
|
"'+tA(fp.last_seen);}\n if(so){const e=so.extra_data||{};const lk=e.lock_state==='locked';el('info-so').innerHTML=`",
|
|
"Status Pintu",
|
|
"');\n \n $.post('chat_api.php', { action: 'send_message', id_occupant: currentOccupantId, message: text, sender: 'account' }, function(res) {\n if (res.status === 'success') {\n $('#chatInput').val('');\n fetchMessages();\n fetchContacts();\n } else {\n showCustomAlert(res.message);\n }\n $('#chatInput').prop('disabled', false).focus();\n submitBtn.prop('disabled', false).html('",
|
|
"*/\n .sidebar .nav .sub-menu .nav-item .nav-link::before {\n content: \"\" !important;\n display: inline-block;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background-color: #a3a4a5;\n margin-right: 10px;\n flex-shrink: 0;\n }\n \n /* Menggeser (menarik) letak kumpulan submenu agak lebih ke kiri jika dibutuhkan */\n .sidebar .nav .sub-menu {\n padding-left: 1.5rem !important;\n padding-top: 2px !important;\n padding-bottom: 2px !important;\n margin-top: -1px !important;\n margin-bottom: -1px !important;\n }\n .sidebar .nav .sub-menu .nav-item .nav-link {\n padding-top: 10px !important;\n padding-bottom: 10px !important;\n }\n .sidebar .nav .nav-item .collapse,\n .sidebar .nav .nav-item .collapsing {\n margin-top: 0 !important;\n margin-bottom: 0 !important;\n }\n \n /* 7. SAAT SUBMENU AKTIF ATAU DIKLIK (warna teks + titik membulat menjadi biru pudar) */\n .sidebar .nav .sub-menu .nav-item .nav-link.active,\n .sidebar .nav .sub-menu .nav-item .nav-link:hover,\n .sidebar .nav .nav-item.active .sub-menu .nav-item .nav-link.active,\n .sidebar .nav .nav-item.active .sub-menu .nav-item .nav-link:hover {\n color: #4B49AC !important;\n font-weight: 500 !important;\n }\n \n /* Titik bulat berubah biru jika sub-menu tersebut aktif */\n .sidebar .nav .sub-menu .nav-item .nav-link.active::before,\n .sidebar .nav .sub-menu .nav-item .nav-link:hover::before,\n .sidebar .nav .nav-item.active .sub-menu .nav-item .nav-link.active::before,\n .sidebar .nav .nav-item.active .sub-menu .nav-item .nav-link:hover::before {\n background-color: #4B49AC !important;\n }\n\n /* ========================================================= */\n /* CHAT SIDEBAR MENU STYLING */\n /* ========================================================= */\n .contact-item { display: flex; padding: 12px 20px; gap: 12px; cursor: pointer; transition: 0.2s; align-items: center; text-decoration: none; border-bottom: 1px solid #f0f0f0; }\n .contact-item:hover { background: #f8faff; text-decoration: none; }\n .contact-item .ci-avatar { width: 42px; height: 42px; border-radius: 50%; position: relative; flex-shrink: 0; }\n .contact-item .ci-avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }\n .contact-item .ci-status { position: absolute; bottom: 2px; right: 2px; width: 10px; height: 10px; border-radius: 50%; background: #10b981; border: 2px solid #fff; }\n .contact-item .ci-info { flex: 1; min-width: 0; display:flex; flex-direction:column; justify-content:center; }\n .contact-item .ci-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }\n .contact-item .ci-name { font-size: 14px; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .contact-item .ci-time { font-size: 11px; color: #999; flex-shrink: 0; }\n .contact-item .ci-msg { font-size: 12px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }\n .contact-item .ci-unread-badge { background: #5856d6; color: #fff; font-size: 10px; font-weight: bold; padding: 2px 6px; border-radius: 10px; margin-left: 6px; display: inline-block; vertical-align: middle; }\n\n /* ========================================================= */\n /* HEADER SEARCH DROPDOWN */\n /* ========================================================= */\n .search-dropdown {\n position: absolute;\n top: calc(100% + 6px);\n left: 0;\n width: 420px;\n background: #ffffff;\n border: 1px solid #e8e8e8;\n border-radius: 12px;\n box-shadow: 0 12px 40px rgba(0,0,0,0.12), 0 4px 12px rgba(0,0,0,0.06);\n z-index: 9999;\n overflow: hidden;\n animation: searchDropIn 0.2s ease;\n }\n @keyframes searchDropIn {\n from { opacity: 0; transform: translateY(-8px); }\n to { opacity: 1; transform: translateY(0); }\n }\n .search-tabs {\n display: flex;\n gap: 4px;\n padding: 12px 16px 0;\n border-bottom: 1px solid #f0f0f0;\n }\n .search-tab-item {\n background: none;\n border: none;\n padding: 8px 16px;\n font-size: 13px;\n font-weight: 500;\n color: #888;\n cursor: pointer;\n border-bottom: 2px solid transparent;\n transition: all 0.2s ease;\n border-radius: 6px 6px 0 0;\n outline: none;\n }\n .search-tab-item:hover {\n color: #4B49AC;\n background: #f6f5ff;\n }\n .search-tab-item.active {\n color: #4B49AC;\n border-bottom-color: #4B49AC;\n font-weight: 600;\n }\n .search-results {\n max-height: 360px;\n overflow-y: auto;\n padding: 8px 0;\n }\n .search-results::-webkit-scrollbar { width: 4px; }\n .search-results::-webkit-scrollbar-track { background: transparent; }\n .search-results::-webkit-scrollbar-thumb { background: #ddd; border-radius: 4px; }\n .search-section-title {\n padding: 8px 16px 4px;\n font-size: 10px;\n font-weight: 700;\n color: #aaa;\n letter-spacing: 1px;\n text-transform: uppercase;\n }\n .search-result-item {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 16px;\n cursor: pointer;\n transition: background 0.15s ease;\n text-decoration: none;\n color: inherit;\n }\n .search-result-item:hover {\n background: #f8f7ff;\n text-decoration: none;\n color: inherit;\n }\n .search-result-icon {\n width: 38px;\n height: 38px;\n border-radius: 10px;\n background: #f1f5f9;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n }\n .search-result-icon i {\n font-size: 16px;\n color: #101112ff;\n }\n .search-result-avatar {\n width: 38px;\n height: 38px;\n border-radius: 50%;\n object-fit: cover;\n flex-shrink: 0;\n border: 2px solid #f0f0f0;\n }\n .search-result-info {\n flex: 1;\n min-width: 0;\n }\n .search-result-name {\n font-size: 13.5px;\n font-weight: 600;\n color: #2c2e33;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n }\n .search-result-sub {\n font-size: 11.5px;\n color: #999;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n }\n .search-result-arrow {\n color: #ccc;\n font-size: 14px;\n flex-shrink: 0;\n transition: color 0.15s;\n }\n .search-result-item:hover .search-result-arrow {\n color: #4B49AC;\n }\n .search-empty {\n text-align: center;\n padding: 30px 16px;\n color: #bbb;\n font-size: 13px;\n }\n .search-highlight {\n background: #e8e5ff;\n border-radius: 2px;\n padding: 0 1px;\n }",
|
|
"Ambil Foto",
|
|
"WebAuthn tidak didukung di browser ini.';\n errorEl.style.display = \"block\";\n btn.disabled = false;\n return;\n }\n \n try {\n const challenge = new Uint8Array(32); crypto.getRandomValues(challenge);\n const userId = new Uint8Array(16); crypto.getRandomValues(userId);\n \n const publicKey = {\n challenge: challenge,\n rp: { name: \"Identia App\" },\n user: { id: userId, name: \"user_\" + accountId + \"@Identia.local\", displayName: \"Account UID \" + accountId },\n pubKeyCredParams: [{ type: \"public-key\", alg: -7 }, { type: \"public-key\", alg: -257 }],\n authenticatorSelection: { authenticatorAttachment: \"platform\", userVerification: \"required\" },\n timeout: 60000,\n attestation: \"none\"\n };\n \n const credential = await navigator.credentials.create({ publicKey });\n const rawIdBase64 = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));\n \n statusEl.innerHTML = '",
|
|
"Peringatan",
|
|
"'+tA(rl.last_seen);}\n}\n\n// INDICATORS\nfunction bInd(){\n const tf=F('tft_screen'),bz=F('buzzer'),lr=F('led_red'),lg=F('led_green');let h='';\n if(tf){const e=tf.extra_data||{};const act=(e.display_state||'active')==='active';\n h+=`",
|
|
"— beberapa router memiliki fitur AP Isolation atau Client Isolation yang mencegah perangkat baru terhubung. Nonaktifkan fitur ini di pengaturan router.'\n ];\n } else {\n errMsg = 'Koneksi Gagal (Alasan Tidak Diketahui)';\n reasonText = '",
|
|
"ID: ' + TOKEN.substring(0, 8) + '...';\n }\n\n dataURL = capCanvas.toDataURL('image/png');\n if (stream) stream.getTracks().forEach(function(t) { t.stop(); });\n camArea.style.display = 'none';\n previewWrap.classList.remove('page-hidden');\n previewWrap.style.display = 'block';\n barCapture.style.display = 'none';\n barAfter.style.display = 'flex';\n\n var st = document.getElementById('simpleText');\n if (st) st.style.display = 'none';\n var hr = document.querySelector('.header-right');\n if (hr) hr.style.display = 'none';\n }\n\n // ── Retake ──\n function retake() {\n dataURL = null;\n livenessScore = 0; livenessFrames = 0; prevLandmarks = null;\n lmFill.style.height = '0%';\n previewWrap.classList.add('page-hidden');\n previewWrap.style.display = '';\n camArea.style.display = 'block';\n barAfter.style.display = 'none';\n barCapture.style.display = 'flex';\n \n var st = document.getElementById('simpleText');\n if (st) st.style.display = 'block';\n var hr = document.querySelector('.header-right');\n if (hr) hr.style.display = 'block';\n toast.style.display = 'none';\n camLoading.style.display = 'flex';\n loadText.textContent = 'Memuat ulang kamera...';\n setProgress(50);\n startCamera().then(function(ok) {\n if (ok) {\n camLoading.style.display = 'none';\n setStatus('search', 'Posisikan wajah Anda...');\n startDetection();\n startBrightnessCheck();\n }\n });\n }\n\n // ── Save ──\n function save() {\n if (!dataURL || sessionSaved) return;\n btnSave.querySelector('.ring').innerHTML = '",
|
|
"Simpan'; \n// showCustomAlert(\"Koneksi ke server gagal.\"); \n// });\n// });\n\nfunction openDeleteModal(id, name) { \n document.getElementById(\"delete_id_account\").value = id; \n document.getElementById(\"delete_account_name\").innerText = name; \n $(\"#modalDeleteAccount\").modal(\"show\"); \n}\n\nfunction toggleStatus(id) { \n $.post(\"toggle_status_account.php\", { id_account: id }, function(d) { \n if (d.status === \"success\") location.reload(); \n else showCustomAlert(d.message || \"Gagal memproses.\"); \n }, \"json\"); \n}\n\n$(document).ready(function() {\n window.isSelectMode = false;\n $('#btnSelectMode').on('click', function() {\n window.isSelectMode = !window.isSelectMode;\n if(window.isSelectMode) {\n $(this).html('",
|
|
"to edit properties",
|
|
"Edit Account",
|
|
"Download Foto",
|
|
"');\n selectElement.prop('disabled', true);\n\n $.ajax({\n url: 'update_access_occupant.php',\n type: 'POST',\n dataType: 'json',\n data: {\n id_occupant: idOccupant,\n access: accessValue\n },\n success: function(response) {\n selectElement.prop('disabled', false);\n btn.html('",
|
|
"Menyimpan...';\n \n fetch(\"tambah_account.php\", { method: \"POST\", body: new FormData(this) })\n .then(function(r){ return r.json(); })\n .then(function(d) {\n btn.disabled = false; btn.innerHTML = '",
|
|
"Waktu akses (5 menit) telah habis. Silakan gunakan perintah /edit_data di Telegram lagi.",
|
|
"untuk terhubung secara personal dengan Admin Identia.\\n\\n\"\n . \"Namun, untuk memulai obrolan langsung dengan Admin, kami perlu memverifikasi data Anda terlebih dahulu.\\n\\n\"\n . \"👇",
|
|
"Yakin menghapus item yang dipilih secara permanen?",
|
|
"Sudah Terdaftar",
|
|
"r.json())\n .then(d => {\n document.getElementById(\"fingerprintQrLoadingView\").style.display = 'none';\n if(d.status === 'success') {\n document.getElementById(\"fingerprintQrView\").style.display = 'block';\n document.getElementById(\"fingerQrLinkText\").value = d.url;\n var canvas = document.getElementById(\"fingerprintQrCanvas\");\n canvas.innerHTML = '';\n new QRCode(canvas, {\n text: d.url, width: 150, height: 150,\n colorDark: '#0f172a', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.H\n });\n } else {\n document.getElementById(\"fingerprintQrDefaultView\").style.display = 'block';\n }\n }).catch(e => {\n document.getElementById(\"fingerprintQrLoadingView\").style.display = 'none';\n document.getElementById(\"fingerprintQrDefaultView\").style.display = 'block';\n });\n}\n\nasync function startWebAuthnRegistration() {\n var btn = document.getElementById(\"btnScanFingerHere\");\n var statusEl = document.getElementById(\"webAuthnStatus\");\n var errorEl = document.getElementById(\"webAuthnError\");\n var successWrapper = document.getElementById(\"webAuthnStatusWrapper\");\n var accountId = document.getElementById(\"fingerprintAccountId\").value;\n \n btn.disabled = true;\n errorEl.style.display = \"none\";\n successWrapper.style.display = \"none\";\n \n statusEl.innerHTML = '",
|
|
"1) {\n faceOk = false;\n oval.classList.remove('ok');\n corners.classList.remove('ok');\n scanbar.classList.remove('ok');\n setStatus('err', 'Terdeteksi lebih dari 1 wajah');\n btnShutter.disabled = true;\n } else {\n faceOk = false;\n oval.classList.remove('ok');\n corners.classList.remove('ok');\n scanbar.classList.remove('ok');\n setStatus('search', 'Posisikan wajah Anda...');\n btnShutter.disabled = true;\n }\n });\n }, 280);\n }\n\n /* ── Brightness Check ────────── */\n function startBrightnessCheck() {\n var tmpCanvas = document.createElement('canvas');\n var tmpCtx = tmpCanvas.getContext('2d');\n brightInterval = setInterval(function() {\n if (vid.paused || vid.ended || !vid.videoWidth) return;\n tmpCanvas.width = 80; tmpCanvas.height = 60;\n tmpCtx.drawImage(vid, 0, 0, 80, 60);\n var data = tmpCtx.getImageData(0, 0, 80, 60).data;\n var sum = 0, count = data.length / 4;\n for (var i = 0; i",
|
|
"';\n btnSave.disabled = true;\n\n var fd = new FormData();\n fd.append('token', TOKEN);\n fd.append('foto', dataURL);\n fd.append('liveness_score', Math.round(livenessScore));\n fd.append('capture_time', new Date().toISOString());\n fd.append('device_info', navigator.userAgent);\n fd.append('screen_res', screen.width + 'x' + screen.height);\n\n var xhr = new XMLHttpRequest();\n xhr.open('POST', 'save_foto.php', true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.status === 'success') {\n sessionSaved = true;\n clearInterval(tokenCheckInterval);\n showPage(pgSuccess);\n if (d.foto) {\n document.getElementById('sucFoto').src = d.foto;\n document.getElementById('sucFoto').style.display = 'block';\n }\n // Auto-close countdown\n var count = 5;\n var cdEl = document.getElementById('sucCountdown');\n if (cdEl) cdEl.innerText = \"Sesi ini akan otomatis ditutup dalam \" + count + \" detik...\";\n var cdInterval = setInterval(function() {\n count--;\n if (cdEl) cdEl.innerText = \"Sesi ini akan otomatis ditutup dalam \" + count + \" detik...\";\n if (count",
|
|
"'; });\n $('#saveFailSuggestions').html(sugHtml);\n \n if (fallbackHtml) {\n $('#saveFailFallbackInfo').html(fallbackHtml).show();\n } else {\n $('#saveFailFallbackInfo').hide();\n }\n \n // Widen modal for detailed failure view\n $('#wifiSaveModalDialog').removeClass('modal-sm').addClass('modal-md');\n $('#saveStep5').fadeIn(300);\n \n } else if (statusRes.status === 'success') {\n clearInterval(changeCheckInterval);\n \n $('#saveStep2 .save-bar-fill').css({'width': '100%', 'transition': 'width 0.5s'}); // Cepat penuhi\n \n setTimeout(function() {\n // Lanjutkan ke Success View\n $('.save-step').hide();\n $('#saveStep2 .save-bar-fill').css({'width': '0%', 'transition': 'none'}); // Reset for next\n $('#saveNewSsidDisplay').html('",
|
|
"Belum ada riwayat percakapan. Mulai kirim chat!",
|
|
"— dinding beton, lemari logam, dan cermin dapat menyerap atau memantulkan sinyal WiFi 2.4GHz secara signifikan. Posisikan ESP32 di tempat yang memiliki jalur pandang (line of sight) ke router.',\n '",
|
|
"Pastikan mengunci pintu kembali jika Anda sudah selesai.",
|
|
"'+jam+' · ",
|
|
"Kelola data penghuni yang sudah disetujui,",
|
|
"';\n }\n } catch(e) {\n showToast('err', 'Respons tidak valid');\n btnSave.disabled = false;\n btnSave.querySelector('.ring').innerHTML = '",
|
|
"Edit Profile",
|
|
"Sesi Anda akan segera diakhiri. Pastikan pekerjaan Anda telah tersimpan dengan aman.",
|
|
"Mempersiapkan Kamera...",
|
|
"');\n }, 'json').fail(function() {\n showCustomAlert(\"Gagal terhubung ke API\");\n $('#chatInput').prop('disabled', false);\n submitBtn.prop('disabled', false).html('",
|
|
"Simpan'; showCustomAlert(\"Koneksi ke server gagal.\"); });\n});\n\nfunction copyCode() { var v = document.getElementById(\"hasilCodeRegister\").value, b = document.getElementById(\"btnCopy\").querySelector('i'); navigator.clipboard.writeText(v).then(function() { b.classList.replace(\"fa-copy\",\"fa-check\"); showCustomAlert('Kode Registrasi berhasil disalin!'); setTimeout(function(){ b.classList.replace(\"fa-check\",\"fa-copy\"); }, 2000); }); }\nfunction copyRevealed() { var v = document.getElementById(\"revealedPassword\").value, b = document.getElementById(\"btnCopyRevealed\"); navigator.clipboard.writeText(v).then(function() { b.classList.replace(\"fa-copy\",\"fa-check\"); showCustomAlert('Password berhasil disalin!'); setTimeout(function(){ b.classList.replace(\"fa-check\",\"fa-copy\"); }, 2000); }); }\n\nvar currentEditFoto = '';\n\nfunction openDeleteModal(id, name) { document.getElementById(\"delete_id_occupant\").value = id; document.getElementById(\"delete_occupant_name\").innerText = name; $(\"#modalDeleteOccupant\").modal(\"show\"); }\nfunction openEditModal(id, name, nik, no_hp, foto) {\n document.getElementById(\"edit_id\").value = id;\n document.getElementById(\"edit_name\").value = name;\n document.getElementById(\"edit_nik\").value = nik || '';\n document.getElementById(\"edit_no_hp\").value = no_hp;\n currentEditFoto = foto;\n $(\"#modalEdit\").modal(\"show\");\n}\n\nfunction generateEditFotoQr() {\n var id = document.getElementById(\"editFotoOccupantId\").value;\n document.getElementById(\"editFotoDefaultView\").style.display = 'none';\n document.getElementById(\"editFotoLoadingView\").style.display = 'block';\n var fd = new FormData();\n fd.append('id_occupant', id);\n fetch('generate_foto_token.php', { method: 'POST', body: fd })\n .then(function(r) { return r.json(); })\n .then(function(d) {\n document.getElementById(\"editFotoLoadingView\").style.display = 'none';\n if (d.status === 'success') {\n document.getElementById(\"editFotoQrView\").style.display = 'block';\n var canvas = document.getElementById(\"editFotoQrCanvas\");\n canvas.innerHTML = '';\n document.getElementById(\"editFotoLinkText\").value = d.url;\n new QRCode(canvas, {\n text: d.url,\n width: 180,\n height: 180,\n colorDark: '#1e1b4b',\n colorLight: '#ffffff',\n correctLevel: QRCode.CorrectLevel.H\n });\n } else {\n showCustomAlert(\"Gagal generate QR\");\n document.getElementById(\"editFotoDefaultView\").style.display = 'block';\n }\n }).catch(function(e) {\n document.getElementById(\"editFotoLoadingView\").style.display = 'none';\n showCustomAlert(\"Koneksi gagal saat mengambil QR.\");\n document.getElementById(\"editFotoDefaultView\").style.display = 'block';\n });\n}\nfunction copyEditFotoLink() {\n navigator.clipboard.writeText(document.getElementById('editFotoLinkText').value);\n showCustomAlert('Link Edit Foto berhasil disalin!');\n}\nfunction downloadEditFotoQr() {\n var qrCanvas = document.querySelector('#editFotoQrCanvas canvas');\n if (!qrCanvas) { showCustomAlert(\"QR Code belum ter-generate.\"); return; }\n var imgUrl = createCompositeQrImage(qrCanvas);\n var dlLink = document.createElement('a');\n dlLink.download = 'QR_Perbarui_Foto.jpg';\n dlLink.href = imgUrl.replace(\"image/jpeg\", \"image/octet-stream\");\n dlLink.click();\n}\nfunction closeEditFotoModal() { $(\"#modalEditFoto\").modal(\"hide\"); location.reload(); }\n\nfunction toggleStatus(id) { $.post(\"toggle_status_occupant.php\", { id_occupant: id }, function(d) { if (d.status === \"success\") location.reload(); else showCustomAlert(d.message || \"Gagal memproses.\"); }, \"json\"); }\n\n// PIN verify QR\nfunction closePinModal() {\n $(\"#modalVerifikasiPin\").modal(\"hide\");\n}\n\nfunction openPinVerifikasiModal(id) {\n document.getElementById(\"verifikasiPinOccupantId\").value = id;\n document.getElementById(\"verifikasiPinDefaultView\").style.display = \"block\";\n document.getElementById(\"verifikasiPinLoadingView\").style.display = \"none\";\n document.getElementById(\"verifikasiPinQrView\").style.display = \"none\";\n \n // Auto generate QR on open\n $(\"#modalVerifikasiPin\").modal(\"show\");\n generatePinQr();\n}\n\nfunction generatePinQr() {\n var id = document.getElementById(\"verifikasiPinOccupantId\").value;\n document.getElementById(\"verifikasiPinDefaultView\").style.display = 'none';\n document.getElementById(\"verifikasiPinLoadingView\").style.display = 'block';\n \n var fd = new FormData();\n fd.append('id_occupant', id);\n \n fetch('generate_pin_token.php', { method: 'POST', body: fd })\n .then(function(r) { return r.json(); })\n .then(function(d) {\n document.getElementById(\"verifikasiPinLoadingView\").style.display = 'none';\n if (d.status === 'success') {\n document.getElementById(\"verifikasiPinQrView\").style.display = 'block';\n document.getElementById(\"verifikasiPinLinkText\").value = d.url;\n var canvas = document.getElementById(\"verifikasiPinQrCanvas\");\n canvas.innerHTML = '';\n new QRCode(canvas, {\n text: d.url,\n width: 200,\n height: 200,\n colorDark: '#0f172a',\n colorLight: '#ffffff',\n correctLevel: QRCode.CorrectLevel.H\n });\n } else {\n alert(\"Gagal generate QR: \" + (d.message || \"Unknown error\"));\n document.getElementById(\"verifikasiPinDefaultView\").style.display = 'block';\n }\n })\n .catch(function(e) {\n document.getElementById(\"verifikasiPinLoadingView\").style.display = 'none';\n alert(\"Terjadi kesalahan jaringan.\");\n document.getElementById(\"verifikasiPinDefaultView\").style.display = 'block';\n });\n}\n\nfunction copyPinQrLink() {\n navigator.clipboard.writeText(document.getElementById('verifikasiPinLinkText').value);\n showCustomAlert('Link verifikasi PIN berhasil disalin!');\n}\n\nfunction downloadPinQr() {\n var qrCanvas = document.querySelector('#verifikasiPinQrCanvas canvas');\n if (!qrCanvas) {\n alert(\"QR Code belum ter-generate.\");\n return;\n }\n \n var imgUrl = createCompositeQrImage(qrCanvas);\n var dlLink = document.createElement('a');\n dlLink.download = 'QR_Verifikasi_PIN.jpg';\n dlLink.href = imgUrl.replace(\"image/jpeg\", \"image/octet-stream\");\n dlLink.click();\n}",
|
|
"*Minimal 8 karakter, huruf besar, huruf kecil, angka & simbol jika ingin mengubah.",
|
|
"'+tA(bt.last_seen);try{updCh('cBT',bt);}catch(e){}}\n}\n\n// CHARTS\nfunction initCh(id,comp){const cv=document.getElementById(id);if(!cv)return;HD[id]={labels:[],ds:{}};pushD(id,comp);CH[id]=new Chart(cv.getContext('2d'),{type:'line',data:{labels:[...HD[id].labels],datasets:getDS(id,comp)},options:chO()});}\nfunction updCh(id,comp){if(!CH[id])return;pushD(id,comp);const h=HD[id];CH[id].data.labels=[...h.labels];const ds=getDS(id,comp);ds.forEach((d,i)=>{if(CH[id].data.datasets[i])CH[id].data.datasets[i].data=d.data;});CH[id].update('none');}\nfunction pushD(id,comp){const h=HD[id],e=comp.extra_data||{};const now=new Date().toLocaleTimeString('id-ID',{hour:'2-digit',minute:'2-digit',second:'2-digit'});h.labels.push(now);if(h.labels.length>MP)h.labels.shift();if(comp.component_key==='power_supply'){pu(h,'v',parseFloat(e.voltage_out||0));pu(h,'a',parseFloat(e.current_out||0));}else if(comp.component_key==='step_down'){pu(h,'vi',parseFloat(e.voltage_in||0));pu(h,'vo',parseFloat(e.voltage_out||0));pu(h,'a',parseFloat(e.current_out||0));}else if(comp.component_key==='battery'){pu(h,'v',parseFloat(e.voltage||0));pu(h,'p',parseFloat(e.charge_percent||0));}}\nfunction getDS(id,comp){const h=HD[id];if(comp.component_key==='power_supply')return[ld('Voltage',h.ds.v,'#d97706','rgba(217,119,6,.06)',true),ld('Current',h.ds.a,'#0891b2','rgba(8,145,178,.06)',true)];if(comp.component_key==='step_down')return[ld('V In',h.ds.vi,'#dc2626',null,false),ld('V Out',h.ds.vo,'#059669','rgba(5,150,105,.04)',true),ld('Current',h.ds.a,'#2563eb',null,false)];if(comp.component_key==='battery')return[ld('Voltage',h.ds.v,'#059669','rgba(5,150,105,.04)',true),ld('Capacity %',h.ds.p,'#d97706','rgba(217,119,6,.06)',true)];return[];}\nfunction ld(l,d,c,bg,f){return{label:l,data:[...d],borderColor:c,backgroundColor:bg||'transparent',borderWidth:1.5,fill:!!f,tension:.4,pointRadius:0,pointBackgroundColor:c};}\nfunction pu(h,k,v){if(!h.ds[k])h.ds[k]=[];h.ds[k].push(v);if(h.ds[k].length>MP)h.ds[k].shift();}\nfunction chO(){return{responsive:true,maintainAspectRatio:false,animation:false,plugins:{legend:{display:true,position:'bottom',labels:{color:'#475569',boxWidth:6,font:{size:7},padding:4}},tooltip:{mode:'index',intersect:false,backgroundColor:'#fff',titleColor:'#1e293b',bodyColor:'#475569',borderColor:'#e5e9f0',borderWidth:1,titleFont:{size:9},bodyFont:{size:8}}},scales:{x:{display:true,grid:{color:'rgba(0,0,0,.03)',drawBorder:false},ticks:{color:'#475569',font:{size:6},maxRotation:45,maxTicksLimit:6}},y:{display:true,grid:{color:'rgba(0,0,0,.04)',drawBorder:false},ticks:{color:'#475569',font:{size:7,family:'monospace'},padding:3,maxTicksLimit:5}}},interaction:{mode:'nearest',axis:'x',intersect:false}};}\n\ndocument.getElementById('bR').addEventListener('click',pull);\npull();setInterval(pull,POLL);\n})();",
|
|
"\"error\", \"message\" => \"Method tidak diizinkan\"]);\n exit;\n}\n\n// Validasi input foto\nif (!isset($_POST['foto'])) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Data foto tidak ada\"]);\n exit;\n}\n\n$fotoData = $_POST['foto'];\n$id_occupant = null;\n$token = null;\n\n// Method 1: Token-based (from QR code scan)\nif (isset($_POST['token']) && trim($_POST['token']) !== '') {\n $token = trim($_POST['token']);\n\n // Validate token\n $stmt = $pdo->prepare(\"SELECT ft.id, ft.id_occupant, ft.status FROM foto_tokens ft WHERE ft.token = ?\");\n $stmt->execute([$token]);\n $tokenRow = $stmt->fetch();\n\n if (!$tokenRow) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Token tidak valid\"]);\n exit;\n }\n if ($tokenRow['status'] === 'used') {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Token sudah digunakan (kadaluwarsa)\"]);\n exit;\n }\n\n $id_occupant = $tokenRow['id_occupant'];\n}\n// Method 2: Direct id_occupant (backward compat)\nelseif (isset($_POST['id_occupant']) && trim($_POST['id_occupant']) !== '') {\n $id_occupant = trim($_POST['id_occupant']);\n}\nelse {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Token atau ID Occupant diperlukan\"]);\n exit;\n}\n\n// Validasi id_occupant ada di database\n$cek = $pdo->prepare(\"SELECT id_occupant, foto FROM occupant WHERE id_occupant = ?\");\n$cek->execute([$id_occupant]);\n$occupant = $cek->fetch();\n\nif (!$occupant) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Occupant tidak ditemukan\"]);\n exit;\n}\n\n// Decode base64 image\nif (preg_match('/^data:image\\/(png|jpeg|jpg|webp);base64,/', $fotoData, $matches)) {\n $ext = $matches[1] === 'jpeg' ? 'jpg' : $matches[1];\n $fotoData = preg_replace('/^data:image\\/\\w+;base64,/', '', $fotoData);\n $fotoData = base64_decode($fotoData);\n\n if ($fotoData === false) {\n echo json_encode([\"status\" => \"error\", \"message\" => \"Gagal decode gambar\"]);\n exit;\n }\n\n // Validate image size (min 50KB, max 10MB)\n $imgSize = strlen($fotoData);\n if ($imgSize",
|
|
"Daftarkan pengguna baru untuk ruangan yang tersedia,",
|
|
". Ini adalah",
|
|
"Data Berhasil Disimpan!",
|
|
"Menyimpan Sidik Jari...';\n \n let fd = new FormData();\n fd.append('id_account', accountId);\n fd.append('credential_id', rawIdBase64);\n \n fetch(\"save_fingerprint_account.php\", { method: \"POST\", body: fd })\n .then(r => r.json())\n .then(d => {\n if(d.status === 'success') {\n statusEl.innerHTML = '",
|
|
"— interferensi dari WiFi tetangga pada channel yang sama dapat mengganggu koneksi. Ubah ke channel 1, 6, atau 11 yang paling sedikit interferensinya.',\n '",
|
|
"\";\n }\n } else {\n $localHtml = htmlspecialchars($message); // no attachment, just text\n }\n\n if ($sender === 'occupant') {\n // Keeps occupant online\n $updateLastActive = $pdo->prepare(\"UPDATE occupant SET last_active = NOW() WHERE id_occupant = ?\");\n $updateLastActive->execute([$id_occupant]);\n }\n\n // Only proceed if there is text or an attachment\n if ($id_occupant > 0 && (!empty($message) || $hasAttachment)) {\n $stmt = $pdo->prepare(\"INSERT INTO chats (id_account, id_occupant, sender, message, reply_to_chat_id) VALUES (?, ?, ?, ?, ?)\");\n if ($stmt->execute([$id_account, $id_occupant, $sender, $localHtml, $reply_to_chat_id])) {\n $id_chat = $pdo->lastInsertId();\n \n // Fetch Telegram Reply ID if replying\n $tgReplyId = null;\n if ($reply_to_chat_id) {\n $qRep = $pdo->prepare(\"SELECT telegram_msg_id FROM chats WHERE id_chat = ? LIMIT 1\");\n $qRep->execute([$reply_to_chat_id]);\n $fRep = $qRep->fetch();\n if ($fRep && $fRep['telegram_msg_id']) {\n $tgReplyId = $fRep['telegram_msg_id'];\n }\n }\n \n // +++++ SEND TO TELEGRAM IF SENDER IS ACCOUNT +++++\n if ($sender === 'account') {\n $telStmt = $pdo->prepare(\"SELECT telegram_id FROM occupant WHERE id_occupant = ?\");\n $telStmt->execute([$id_occupant]);\n $telegramUser = $telStmt->fetch();\n \n if ($telegramUser && !empty($telegramUser['telegram_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $website = \"https://api.telegram.org/bot\".$botToken;\n \n $postData = [\n 'chat_id' => $telegramUser['telegram_id']\n ];\n \n // IF Replying\n if ($tgReplyId) {\n $postData['reply_to_message_id'] = $tgReplyId;\n }\n \n // IF Attachment\n if ($hasAttachment) {\n $postData[$tgFileType] = new CURLFile($attachmentPath);\n if (!empty($message)) {\n $postData['caption'] = $message;\n }\n } else {\n $postData['text'] = $message;\n }\n \n $ch = curl_init($website.\"/\".$tgEndpoint);\n curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n curl_setopt($ch, CURLOPT_POST, 1);\n curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);\n curl_setopt($ch, CURLOPT_TIMEOUT, 30);\n $result = curl_exec($ch);\n curl_close($ch);\n \n if ($result) {\n $resJson = json_decode($result, true);\n if (isset($resJson['ok']) && $resJson['ok'] && isset($resJson['result']['message_id'])) {\n $tgMsgId = $resJson['result']['message_id'];\n $updateMsg = $pdo->prepare(\"UPDATE chats SET telegram_msg_id = ? WHERE id_chat = ?\");\n $updateMsg->execute([$tgMsgId, $id_chat]);\n }\n }\n }\n }\n // +++++++++++++++++++++++++++++++++++++++++++++++++\n \n $msgStmt = $pdo->prepare(\"SELECT * FROM chats WHERE id_chat = ?\");\n $msgStmt->execute([$id_chat]);\n $newMsg = $msgStmt->fetch();\n echo json_encode(['status' => 'success', 'data' => $newMsg]);\n exit;\n }\n }\n echo json_encode(['status' => 'error', 'message' => 'Invalid data']);\n exit;\n}\n\nif ($action === 'login_occupant') {\n $no_hp = isset($_POST['no_hp']) ? trim($_POST['no_hp']) : '';\n $pin = isset($_POST['pin']) ? trim($_POST['pin']) : '';\n \n if (empty($no_hp) || empty($pin)) {\n echo json_encode(['status' => 'error', 'message' => 'Lengkapi form.']);\n exit;\n }\n \n $stmt = $pdo->prepare(\"SELECT id_occupant, name, status, access FROM occupant WHERE no_hp = ?\");\n $stmt->execute([$no_hp]);\n $occ = $stmt->fetch();\n \n if ($occ) {\n if ($occ['status'] !== 'approved' || $occ['access'] !== 'active') {\n echo json_encode(['status' => 'error', 'message' => 'Akun belum disetujui atau sedang tidak aktif.']);\n exit;\n }\n \n $verifyStmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE no_hp = ? AND pin = ?\");\n $verifyStmt->execute([$no_hp, $pin]);\n if ($verifyStmt->fetch()) {\n $_SESSION['chat_occupant_id'] = $occ['id_occupant'];\n echo json_encode(['status' => 'success', 'data' => $occ]);\n exit;\n }\n }\n echo json_encode(['status' => 'error', 'message' => 'No HP atau PIN salah.']);\n exit;\n}\n\nif ($action === 'delete_history') {\n $id_occupant = isset($_POST['id_occupant']) ? (int)$_POST['id_occupant'] : 0;\n if ($id_occupant > 0) {\n $stmt = $pdo->prepare(\"UPDATE chats SET deleted_by_occupant = 1 WHERE id_account = ? AND id_occupant = ?\");\n $stmt->execute([$id_account, $id_occupant]);\n echo json_encode(['status' => 'success', 'message' => 'Chat history deleted from your view.']);\n exit;\n }\n echo json_encode(['status' => 'error', 'message' => 'Invalid occupant ID.']);\n exit;\n}\n\nif ($action === 'delete_message') {\n $id_chat = isset($_POST['id_chat']) ? (int)$_POST['id_chat'] : 0;\n $delete_type = isset($_POST['delete_type']) ? $_POST['delete_type'] : 'me'; // 'me' or 'everyone'\n \n // Validate message belongs to account before deleting\n $stmt = $pdo->prepare(\"SELECT c.sender, c.telegram_msg_id, o.telegram_id \n FROM chats c \n LEFT JOIN occupant o ON c.id_occupant = o.id_occupant \n WHERE c.id_chat = ? AND c.id_account = ?\");\n $stmt->execute([$id_chat, $id_account]);\n $msg = $stmt->fetch();\n \n if ($msg) {\n if ($msg['sender'] === 'account') {\n // It's the admin's message\n // Delete from Database\n $delStmt = $pdo->prepare(\"DELETE FROM chats WHERE id_chat = ?\");\n $delStmt->execute([$id_chat]);\n \n // Delete from Telegram if exists AND delete_type is 'everyone'\n if ($delete_type === 'everyone' && !empty($msg['telegram_msg_id']) && !empty($msg['telegram_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $website = \"https://api.telegram.org/bot\".$botToken;\n \n $postData = [\n 'chat_id' => $msg['telegram_id'],\n 'message_id' => $msg['telegram_msg_id']\n ];\n \n $ch = curl_init($website.\"/deleteMessage\");\n curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n curl_setopt($ch, CURLOPT_POST, 1);\n curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));\n curl_setopt($ch, CURLOPT_TIMEOUT, 2);\n curl_exec($ch);\n curl_close($ch);\n }\n \n echo json_encode(['status' => 'success']);\n exit;\n } else {\n // It's the occupant's message\n // Based on user request, admin can now delete occupant messages from their own view (Delete for Me)\n // Occupant messages cannot be deleted from Telegram via bot API anyway\n \n $delStmt = $pdo->prepare(\"DELETE FROM chats WHERE id_chat = ?\");\n $delStmt->execute([$id_chat]);\n \n echo json_encode(['status' => 'success']);\n exit;\n }\n }\n \n echo json_encode(['status' => 'error', 'message' => 'Message not found']);\n exit;\n}\n\n// ── FORWARD MESSAGE ──\nif ($action === 'forward_message') {\n $id_chat = isset($_POST['id_chat']) ? (int)$_POST['id_chat'] : 0;\n $target_occupant = isset($_POST['target_occupant']) ? (int)$_POST['target_occupant'] : 0;\n \n if ($id_chat > 0 && $target_occupant > 0) {\n $srcStmt = $pdo->prepare(\"SELECT message FROM chats WHERE id_chat = ?\");\n $srcStmt->execute([$id_chat]);\n $srcMsg = $srcStmt->fetch();\n \n if ($srcMsg) {\n $fwdHtml = \"",
|
|
"\"\n data-fingerok=\"",
|
|
"({x: p.x, y: p.y}));\n editLivenessScore = Math.min(100, (editLivenessFrames / 12) * 100);\n editLmFill.style.height = editLivenessScore + '%';\n if (editLivenessScore > 70) { editLmFill.style.background = '#22c55e'; editLmIcon.style.color = '#22c55e'; }\n else if (editLivenessScore > 30) { editLmFill.style.background = '#f59e0b'; editLmIcon.style.color = '#f59e0b'; }\n else { editLmFill.style.background = '#ef4444'; editLmIcon.style.color = '#ef4444'; }\n }\n if (editLivenessScore >= 50) {\n editSetStatus('ok', 'Wajah terdeteksi — Siap!');\n editBtnShutter.disabled = false;\n } else {\n editSetStatus('search', 'Mendeteksi liveness...');\n editBtnShutter.disabled = true;\n }\n }\n } else if (results.length > 1) {\n editSetFaceNotOk(); editSetStatus('err', 'Terdeteksi lebih dari 1 wajah');\n } else {\n editSetFaceNotOk(); editSetStatus('search', 'Posisikan wajah Anda...');\n }\n }, 300);\n }\n\n function editSetFaceNotOk() {\n editFaceOk = false;\n editOval.classList.remove('ok');\n editBtnShutter.disabled = true;\n }\n\n function startEditBrightness() {\n const tmpCanvas = document.createElement('canvas');\n const tmpCtx = tmpCanvas.getContext('2d');\n editBrightInterval = setInterval(() => {\n if (editVid.paused || editVid.ended || !editVid.videoWidth) return;\n tmpCanvas.width = 80; tmpCanvas.height = 60;\n tmpCtx.drawImage(editVid, 0, 0, 80, 60);\n const data = tmpCtx.getImageData(0, 0, 80, 60).data;\n let sum = 0; const count = data.length / 4;\n for (let i = 0; i",
|
|
"Simpan kode dibawah ini untuk melakukan registrasi di Finger Print.",
|
|
"\"\n data-unread=\"",
|
|
"— tekan tombol reset atau cabut dan pasang kembali power ESP32, lalu coba hubungkan ulang.',\n '",
|
|
"Wajib Membagikan Kontak",
|
|
"(di bawah -75 dBm) untuk membuat koneksi yang stabil. Paket data handshake gagal terkirim/diterima dengan sempurna karena jarak yang terlalu jauh atau penghalang fisik antara ESP32 dan router.';\n suggestions = [\n '",
|
|
"Tambah Account",
|
|
"`;\n });\n }\n\n // Account\n if (showAccount && data.account && data.account.length > 0) {\n html += '",
|
|
"Link ini invalid atau sudah pernah digunakan. Silakan minta link terbarunya dari Admin.",
|
|
"exec(\"CREATE TABLE IF NOT EXISTS chats (\n id_chat INT AUTO_INCREMENT PRIMARY KEY,\n id_account INT NOT NULL,\n id_occupant INT NOT NULL,\n sender ENUM('account', 'occupant') NOT NULL,\n message TEXT NOT NULL,\n is_read TINYINT DEFAULT 0,\n deleted_by_occupant TINYINT DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4\");\n\n// Automatically add columns to existing tables if updating\ntry {\n $pdo->exec(\"ALTER TABLE chats ADD COLUMN deleted_by_occupant TINYINT DEFAULT 0 AFTER is_read\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\ntry {\n $pdo->exec(\"ALTER TABLE occupant ADD COLUMN last_active DATETIME NULL\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\ntry {\n $pdo->exec(\"ALTER TABLE occupant ADD COLUMN telegram_id VARCHAR(100) NULL AFTER no_hp\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\ntry {\n $pdo->exec(\"ALTER TABLE chats ADD COLUMN telegram_msg_id INT NULL AFTER id_chat\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\ntry {\n $pdo->exec(\"ALTER TABLE chats ADD COLUMN reply_to_chat_id INT NULL AFTER telegram_msg_id\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\ntry {\n $pdo->exec(\"ALTER TABLE chats ADD COLUMN is_pinned TINYINT DEFAULT 0 AFTER reply_to_chat_id\");\n} catch(PDOException $e) { /* Column likely exists */ }\n\n// AUTO CLEANUP: Hapus chat yang usianya lebih dari 3 hari (72 Jam)\ntry {\n $pdo->exec(\"DELETE FROM chats WHERE created_at",
|
|
"Cahaya",
|
|
"function showCustomAlert(title, message, type='warning') {\n const alertBox = document.getElementById('customAlert');\n const icon = document.getElementById('customAlertIcon');\n \n alertBox.className = 'custom-alert ' + type;\n if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';\n else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';\n else icon.className = 'mdi mdi-alert-circle custom-alert-icon';\n\n document.getElementById('customAlertTitle').innerText = title;\n document.getElementById('customAlertMsg').innerText = message;\n \n alertBox.style.display = 'flex';\n alertBox.style.animation = 'slideIn 0.3s ease-out';\n\n setTimeout(() => closeCustomAlert(), 5000); // auto close after 5s\n }\n\n function closeCustomAlert() {\n const alertBox = document.getElementById('customAlert');\n alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';\n setTimeout(() => {\n alertBox.style.display = 'none';\n }, 300);\n }\n\n document.addEventListener('DOMContentLoaded', function() {\n const usernameInput = document.getElementById('usernameInput');\n const usernameTooltip = document.getElementById('usernameTooltip');\n let checkTimeout;\n\n function checkUsername() {\n const username = usernameInput.value.trim();\n if (username === '') {\n usernameTooltip.style.display = 'none';\n usernameInput.setCustomValidity(''); \n return;\n }\n\n const formData = new FormData();\n formData.append('username', username);\n\n fetch('check_username.php', {\n method: 'POST',\n body: formData\n })\n .then(res => res.json())\n .then(data => {\n if (data.status === 'taken') {\n usernameTooltip.style.display = 'flex';\n // Prevent form parsing if invalid\n usernameInput.setCustomValidity('Username sudah digunakan');\n } else {\n usernameTooltip.style.display = 'none';\n usernameInput.setCustomValidity('');\n }\n })\n .catch(err => {\n console.error(err);\n showCustomAlert(\"Error\", \"Gagal memeriksa username.\", \"error\");\n });\n }\n\n usernameInput.addEventListener('input', function() {\n usernameTooltip.style.display = 'none'; // hide while typing\n clearTimeout(checkTimeout);\n checkTimeout = setTimeout(checkUsername, 500); // debounce check\n });\n\n usernameInput.addEventListener('blur', checkUsername);\n \n // Hide tooltip immediately when focusing to re-type\n usernameInput.addEventListener('focus', function() {\n usernameTooltip.style.display = 'none';\n });\n\n // Tampilkan error PHP jika ada sebagai alert juga optional (kita tampilkan sebagai error merah)",
|
|
"Edit Foto",
|
|
"Hapus",
|
|
"r.json())\n .then(res => {\n if (res.status === 'success') {\n // Update unread danger count\n let unreadCount = parseInt(res.danger_unread) || 0;\n let indicator = document.getElementById('headerWarningCountIndicator');\n let badge = document.getElementById('headerWarningBadge');\n \n if (unreadCount > 0) {\n if(indicator) indicator.style.display = 'block';\n if(badge) badge.innerText = unreadCount + ' Baru';\n } else {\n if(indicator) indicator.style.display = 'none';\n if(badge) badge.innerText = '0';\n }\n\n // Build list\n let html = '';\n let warnings = res.warnings || [];\n if (warnings.length > 0) {\n warnings.forEach(w => {\n let timeDiff = formatRelativeHeader(w.time);\n html += `",
|
|
"/forgot_password",
|
|
"email-star\" data-id=\"",
|
|
"New Password",
|
|
"`;\n hist.appendChild(tempRow);\n scrollToBottom();\n \n input.value = '';\n input.style.height = 'auto'; // Reset textarea height\n closeReplyBox();\n \n $.ajax({\n url: 'chat_api.php',\n type: 'POST',\n data: formData,\n contentType: false,\n processData: false,\n dataType: 'json',\n success: function(res) {\n if(res.status === 'success') {\n loadContacts();\n $.post('chat_api.php', { action: 'get_messages', id_occupant: activeOccupantId }, function(res2) {\n if(res2.status === 'success') {\n renderMessages(res2.data);\n }\n }, 'json');\n } else {\n let el = document.getElementById(tempId);\n if (el) {\n el.querySelector('.msg-status').innerHTML = '",
|
|
"Jelaskan kegiatan yang Anda lakukan (misal: Maintenance AC)...",
|
|
"Scan ulang jaringan",
|
|
"PIN diperlukan untuk melihat password di halaman detail.",
|
|
"\\n\"\n . \"Gunakan perintah ini jika Anda melupakan PIN 6-Digit Anda. Sistem akan membuatkan sesi verifikasi wajah biometrik (FR) satu kali pakai. Setelah wajah Anda terverifikasi sesuai dengan foto pendaftaran, Anda diizinkan untuk mengatur ulang PIN Anda kembali.\\n\\n\"\n . \"🔑",
|
|
"Setup PIN - Identia Admin",
|
|
"Terjadi Kesalahan Jaringan.';\n statusEl.className = \"mt-4 text-danger font-weight-bold\";\n btn.disabled = false;\n });\n \n } catch (err) {\n statusEl.innerHTML = '",
|
|
"UNLOCK DOOR');\n \n // Animate Door Icon Back\n $('#doorIcon').removeClass('mdi-door-open').addClass('mdi-door-closed');\n }\n }\n\n function pollDoorStatus() {\n $.get('api_door_status.php', function(data) {\n if (data && data.status === 'success') {\n updateUIFromState(data.door_state);\n }\n }, 'json');\n }\n\n // Poll every 2.5 seconds\n setInterval(pollDoorStatus, 2500);\n \n // Run once immediately on load\n pollDoorStatus();\n });",
|
|
"\"\n data-danger=\"",
|
|
"Silahkan isi semua slot PIN (6 angka).",
|
|
"Password baru belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!",
|
|
"\" . $srcMsg['message'];\n \n $insStmt = $pdo->prepare(\"INSERT INTO chats (id_account, id_occupant, sender, message) VALUES (?, ?, 'account', ?)\");\n $insStmt->execute([$id_account, $target_occupant, $fwdHtml]);\n $newId = $pdo->lastInsertId();\n \n // Send to Telegram\n $telStmt = $pdo->prepare(\"SELECT telegram_id FROM occupant WHERE id_occupant = ?\");\n $telStmt->execute([$target_occupant]);\n $tgUser = $telStmt->fetch();\n if ($tgUser && !empty($tgUser['telegram_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $plainText = strip_tags($srcMsg['message']);\n $postData = ['chat_id' => $tgUser['telegram_id'], 'text' => \"[Diteruskan]\\n\" . $plainText];\n $ch = curl_init(\"https://api.telegram.org/bot{$botToken}/sendMessage\");\n curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n curl_setopt($ch, CURLOPT_POST, 1);\n curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);\n curl_setopt($ch, CURLOPT_TIMEOUT, 5);\n $res = curl_exec($ch);\n curl_close($ch);\n if ($res) {\n $rj = json_decode($res, true);\n if (isset($rj['ok']) && $rj['ok'] && isset($rj['result']['message_id'])) {\n $pdo->prepare(\"UPDATE chats SET telegram_msg_id = ? WHERE id_chat = ?\")->execute([$rj['result']['message_id'], $newId]);\n }\n }\n }\n echo json_encode(['status' => 'success']);\n exit;\n }\n }\n echo json_encode(['status' => 'error', 'message' => 'Invalid forward data']);\n exit;\n}\n\n// ── EDIT MESSAGE ──\nif ($action === 'edit_message') {\n $id_chat = isset($_POST['id_chat']) ? (int)$_POST['id_chat'] : 0;\n $new_message = isset($_POST['new_message']) ? trim($_POST['new_message']) : '';\n \n if ($id_chat > 0 && !empty($new_message)) {\n // Only allow editing account messages\n $stmt = $pdo->prepare(\"SELECT sender, telegram_msg_id, (SELECT telegram_id FROM occupant WHERE id_occupant = chats.id_occupant) as telegram_id FROM chats WHERE id_chat = ? AND id_account = ?\");\n $stmt->execute([$id_chat, $id_account]);\n $msg = $stmt->fetch();\n \n if ($msg && $msg['sender'] === 'account') {\n $formattedHtml = htmlspecialchars($new_message) . \"",
|
|
"Menunggu Sensor...';\n statusEl.className = \"mt-4 text-info font-weight-bold\";\n statusEl.style.display = \"block\";\n \n if (!window.PublicKeyCredential) {\n statusEl.innerHTML = '",
|
|
"\"\n data-detail=\"",
|
|
"prepare(\"SELECT telegram_id FROM occupant WHERE id_occupant = ?\");\n $tstmt->execute([$id_occupant]);\n $tdata = $tstmt->fetch(PDO::FETCH_ASSOC);\n if ($tdata && $tdata['telegram_id']) {\n $chatId = $tdata['telegram_id'];\n file_get_contents(\"https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['msg_id']}\");\n }\n }\n unset($tokens[$postToken]);\n file_put_contents($manifestPath, json_encode($tokens, JSON_PRETTY_PRINT));\n }\n }\n echo json_encode(['status'=>'ok']);\n exit;\n }\n }\n\n // Validasi token via local request atau include logic langsung\n // Karena ini di file yang sama (local), kita bisa request HTTP ke server sendiri\n // Tapi untuk mudahnya karena sudah ada db:\n $manifestPath = __DIR__ . '/manifest_pin_tokens.json';\n if (file_exists($manifestPath)) {\n $json = file_get_contents($manifestPath);\n $tokens = json_decode($json, true) ?: [];\n \n if (isset($tokens[$tokenParam])) {\n $tokenData = $tokens[$tokenParam];\n if (time()",
|
|
"— nama WiFi harus diketik persis sama termasuk huruf besar/kecil. Contoh: \"WiFi_Rumah\" ≠ \"wifi_rumah\" ≠ \"WiFi Rumah\".',\n '",
|
|
"\";\n \n // Update local DB\n $updStmt = $pdo->prepare(\"UPDATE chats SET message = ? WHERE id_chat = ?\");\n $updStmt->execute([$formattedHtml, $id_chat]);\n \n // Update Telegram if telegram_msg_id exists\n if (!empty($msg['telegram_msg_id']) && !empty($msg['telegram_id'])) {\n $botToken = \"8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho\";\n $postData = [\n 'chat_id' => $msg['telegram_id'],\n 'message_id' => $msg['telegram_msg_id'],\n 'text' => $new_message\n ];\n $ch = curl_init(\"https://api.telegram.org/bot{$botToken}/editMessageText\");\n curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);\n curl_setopt($ch, CURLOPT_POST, 1);\n curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);\n curl_setopt($ch, CURLOPT_TIMEOUT, 5);\n curl_exec($ch);\n curl_close($ch);\n }\n echo json_encode(['status' => 'success', 'new_message' => $formattedHtml]);\n exit;\n }\n }\n echo json_encode(['status' => 'error', 'message' => 'Cannot edit this message']);\n exit;\n}\n\n// ── BULK DELETE MESSAGES ──\nif ($action === 'bulk_delete_messages') {\n $ids = isset($_POST['id_chats']) ? $_POST['id_chats'] : [];\n $deleteType = isset($_POST['delete_type']) ? $_POST['delete_type'] : 'me'; // 'everyone' or 'me'\n \n if (is_array($ids) && count($ids) > 0) {\n $deletedCount = 0;\n foreach ($ids as $mid) {\n $mid = (int)$mid;\n if ($mid",
|
|
"Foto Profil (Opsional)",
|
|
"Simpan'; \n showCustomAlert(\"Koneksi ke server gagal.\"); \n });\n });\n\n $(\"#formDeleteAccount\").submit(function(e) { \n e.preventDefault(); \n var val = $(\"#delete_id_account\").val();\n if(val === \"BULK\") {\n $(\"#btnDeleteAccount\").prop(\"disabled\",true);\n $(\"#btnDeleteAccount\").html('",
|
|
"Ingat password?",
|
|
"Identity data not found.",
|
|
"Link (tautan) tidak valid atau sesi kadaluwarsa. Silakan minta link terbarunya dari Admin.",
|
|
"Simpan';\n if (d.status === 'success') {\n $('#' + modalId).modal('hide');\n if (typeof showCustomAlert === 'function') showCustomAlert('Berhasil', d.message, 'success');\n setTimeout(() => location.reload(), 1000);\n } else {\n if (typeof showCustomAlert === 'function') showCustomAlert('Gagal', d.message, 'error');\n else alert(d.message);\n }\n }).catch(() => {\n btn.disabled = false;\n btn.innerHTML = '",
|
|
"\";\n $postData = ['chat_id' => $chatId, 'text' => $text, 'parse_mode' => 'HTML'];\n $res = file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n \n // Pin pesan Panduan Penggunaan\n $resData = json_decode($res, true);\n if (isset($resData['result']['message_id'])) {\n $helpMsgId = $resData['result']['message_id'];\n file_get_contents($website.\"/pinChatMessage?chat_id=$chatId&message_id=$helpMsgId\");\n }\n exit;\n }\n \n if ($textMessage === '/forgot_pin') {\n $token = bin2hex(random_bytes(16));\n $manPath = __DIR__ . '/manifest_pin_tokens.json';\n $tkns = file_exists($manPath) ? (json_decode(file_get_contents($manPath), true) ?: []) : [];\n // Delete previous message for same occupant\n foreach ($tkns as $oldTk => $oldData) {\n if (isset($oldData['id_occupant']) && $oldData['id_occupant'] == $id_occupant && isset($oldData['msg_id'])) {\n @file_get_contents($website.\"/deleteMessage?chat_id=$chatId&message_id={$oldData['msg_id']}\");\n unset($tkns[$oldTk]);\n }\n }\n $tkns[$token] = ['id_occupant'=>$id_occupant, 'created_at'=>time(), 'expires_at'=>time() + 300];\n \n $expireTime = date('H:i:s', time() + 300);\n $webAppUrl = $baseUrl . \"/verifikasi_wajah.php?token=\" . $token;\n $btn = ['inline_keyboard' => [[['text' => 'Biometric Verification', 'web_app' => ['url' => $webAppUrl]]]]];\n $origText = \"Silakan klik tombol di bawah ini untuk memulai verifikasi wajah.\";\n $postData = ['chat_id' => $chatId, 'text' => $origText . \"\\n⏳",
|
|
"Silakan klik foto di bawah ini untuk memperbarui foto. Jika",
|
|
"Daftar Notifikasi Terbaru",
|
|
"';\n });\n }\n\n // Input event with debounce\n searchInput.addEventListener('input', function() {\n clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n doSearch(this.value.trim());\n }, 300);\n });\n\n // Focus shows dropdown if there's content\n searchInput.addEventListener('focus', function() {\n if (this.value.trim().length >= 1) {\n doSearch(this.value.trim());\n }\n });\n\n // Tab clicks\n searchTabs.addEventListener('click', function(e) {\n const btn = e.target.closest('.search-tab-item');\n if (!btn) return;\n searchTabs.querySelectorAll('.search-tab-item').forEach(b => b.classList.remove('active'));\n btn.classList.add('active');\n activeTab = btn.getAttribute('data-tab');\n const query = searchInput.value.trim();\n if (query) {\n renderResults(cachedData, query, activeTab);\n }\n });\n\n // Close on Escape\n document.addEventListener('keydown', function(e) {\n if (e.key === 'Escape') {\n searchDropdown.style.display = 'none';\n }\n });\n\n // Close on click outside\n document.addEventListener('click', function(e) {\n if (!searchDropdown.contains(e.target) && e.target !== searchInput && !e.target.closest('#navbar-search-icon')) {\n searchDropdown.style.display = 'none';\n }\n });\n})();",
|
|
"\");\n}\n\n$id_occupant = $tokenData['id_occupant'];\n\n// Ambil data occupant\n$stmt = $pdo->prepare(\"SELECT name, nik_nip, no_hp, foto FROM occupant WHERE id_occupant = ?\");\n$stmt->execute([$id_occupant]);\n$occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n\nif (!$occupant) {\n die(\"",
|
|
"Hapus Permanen",
|
|
"Lihat",
|
|
"Hari Ini",
|
|
"\\n\\n\"\n . \"Berikut adalah rincian fitur mandiri yang dapat Anda akses menggunakan Telegram Web App secara instan dan aman:\\n\\n\"\n . \"🔐",
|
|
"`;}\n if(so){const e=so.extra_data||{};const lk=e.lock_state==='locked';\n h+=`",
|
|
"document.addEventListener('DOMContentLoaded', () => {\n const group = document.getElementById('pinGroup');\n const inputs = group.querySelectorAll('.pin-box');\n const hiddenInput = document.getElementById('realPin');\n\n inputs.forEach((input, index) => {\n input.addEventListener('input', (e) => {\n input.value = input.value.replace(/\\D/g, '');\n if (input.value !== '' && index",
|
|
"Token QR code ini sudah pernah digunakan.",
|
|
".content-wrapper, .footer { background: #f8f9fb !important; }\n/* Perkecil wrapper DataTables */\n.dataTables_wrapper .dataTables_length,\n.dataTables_wrapper .dataTables_filter,\n.dataTables_wrapper .dataTables_info,\n.dataTables_wrapper .dataTables_paginate {\n font-size: 0.875rem;\n}\n\n/* Dropdown Show entries lebih kecil */\n.dataTables_wrapper .dataTables_length select {\n padding: 0.15rem 0.1rem 0.15rem 0.4rem; /* padding kanan dikurangi */\n font-size: 0.8rem;\n height: 28px;\n line-height: 1.2;\n width: auto; /* menyesuaikan konten */\n min-width: 40px; /* batas minimal */\n max-width: 60px; /* batas maksimal agar tidak melebar */\n background: transparent !important;\n border: 0px solid #ddd !important;\n color: #333 !important;\n margin: 0 4px;\n border-radius: 4px;\n}\n\n/* Input pencarian lebih kecil */\n.dataTables_wrapper .dataTables_filter input {\n padding: 0.15rem 0.5rem;\n font-size: 0.8rem;\n height: 28px;\n}\n/* Pagination: hilangkan background, hanya border, dan beri jarak */\n/* Jarak antara tabel dan info/pagination */\n.dataTables_wrapper .dataTables_info,\n.dataTables_wrapper .dataTables_paginate {\n margin-top: 1rem !important;\n}\n.dataTables_wrapper .dataTables_paginate .paginate_button.current {\n background: transparent !important;\n border: 1px solid #ddd !important;\n color: #7b7979 !important;\n margin: 0 4px;\n border-radius: 4px;\n}\n\n/* Tombol pagination */\n.dataTables_wrapper .dataTables_paginate .paginate_button {\n padding: 0.25rem 0.6rem;\n font-size: 0.875rem;\n}\n.dataTables_wrapper .dataTables_paginate .paginate_button.previous,\n.dataTables_wrapper .dataTables_paginate .paginate_button.next {\n padding: 0.25rem 0.75rem;\n}\n\ntable#myTable td,\ntable#myTable th {\n padding: 1rem 1.5rem !important;\n}\ntable#myTable,\ntable#myTable th,\ntable#myTable td {\n border: none !important;\n}\n\n/* Pastikan tidak ada border bawah pada tabel */\ntable#myTable {\n border-bottom: none !important;\n margin-bottom: 0.5rem;\n}\n\n/* Hapus border dari pembungkus tabel */\n.table-responsive {\n border-bottom: none;\n}\n\n/* Antisipasi garis dari wrapper DataTables */\n.dataTables_wrapper {\n border-top: none;\n}\n.dataTables_wrapper .dataTables_paginate .paginate_button:hover {\n background: #f5f6f8 !important; /* abu putih soft */\n border: 1px solid #ddd !important;\n color: #333 !important;\n border-radius: 4px;\n}\n\n.dataTables_wrapper .dataTables_paginate .paginate_button:focus {\n box-shadow: none !important;\n}\n\n/* INPUT SEARCH DATATABLES */\n.dataTables_wrapper .dataTables_filter input {\n background: transparent !important; /* transparan */\n border: 1px solid #d1d5db !important; /* abu soft */\n border-radius: 3px;\n color: #333 !important;\n padding: 0.3rem 0.6rem;\n transition: 0.2s ease;\n}\n\n/* Saat focus */\n.dataTables_wrapper .dataTables_filter input:focus {\n outline: none !important;\n border: 1px solid #60a5fa !important; /* biru soft */\n box-shadow: 0 0 0 2px rgba(96,165,250,0.15);\n}\n/* Untuk Chrome, Edge, Safari */\n.dataTables_wrapper .dataTables_filter input[type=\"search\"]::-webkit-search-cancel-button {\n -webkit-appearance: none;\n height: 14px;\n width: 14px;\n cursor: pointer;\n background: none;\n position: relative;\n}\n\n/* Buat custom X warna hitam */\n.dataTables_wrapper .dataTables_filter input[type=\"search\"]::-webkit-search-cancel-button:after {\n content: \"✕\";\n color: #1e2223; /* HITAM */\n font-size: 14px;\n font-weight: bold;\n}\ntable.dataTable tbody tr td {\n vertical-align: middle;\n}\n\n/* Warna Striped Tabel Selang-Seling Konsisten */\ntable.dataTable tbody tr.odd > td,\ntable#myTable tbody tr.row-odd > td {\n background-color: #fafafa !important;\n}\ntable.dataTable tbody tr.even > td,\ntable#myTable tbody tr.row-even > td {\n background-color: #ffffff !important;\n}\n\n/* KETIKA DROPDOWN DIBUKA: Induk (Menu) WAJIB Putih, Anak (Dropdown) WAJIB Abu-abu */\ntable.dataTable tbody tr.odd.shown > td,\ntable.dataTable tbody tr.even.shown > td,\ntable#myTable tbody tr.shown > td {\n background-color: #ffffff !important;\n}\n\ntable.dataTable tbody tr.shown + tr > td,\ntable#myTable tbody tr.shown + tr > td {\n background-color: #fafafa !important;\n}\n\n/* MENGGESER SELURUH BARIS KE BAWAH/ATAS AGAR SELANG-SELING TIDAK TERPUTUS. \n Kasus 1: Menu Genap dibuka -> Seluruh menu di BAWAHNYA harus bertukar warna. */\ntable.dataTable tbody tr.even.shown ~ tr.odd > td,\ntable#myTable tbody tr.row-even.shown ~ tr.row-odd > td {\n background-color: #ffffff !important;\n}\ntable.dataTable tbody tr.even.shown ~ tr.even > td,\ntable#myTable tbody tr.row-even.shown ~ tr.row-even > td {\n background-color: #fafafa !important;\n}\n\n/* Kasus 2: Menu Ganjil dibuka -> Karena ia berubah jadi Putih, maka seluruh menu di ATASNYA harus bertukar warna! \n (Memerlukan CSS :has() selector untuk menarget elemen sebelum tr.shown) */\ntable.dataTable tbody:has(tr.odd.shown) tr.even:not(.shown ~ tr) > td,\ntable#myTable tbody:has(tr.row-odd.shown) tr.row-even:not(.shown ~ tr) > td {\n background-color: #fafafa !important;\n}\ntable.dataTable tbody:has(tr.odd.shown) tr.odd:not(.shown):not(.shown ~ tr) > td,\ntable#myTable tbody:has(tr.row-odd.shown) tr.row-odd:not(.shown):not(.shown ~ tr) > td {\n background-color: #ffffff !important;\n}\n\ntable.dataTable tbody tr.shown + tr td {\n padding: 0 !important;\n border: none !important;\n}",
|
|
"Silakan mendaftarkan sidik jari untuk akun ini.",
|
|
"Token Tidak Valid",
|
|
"// ===== TFT Component-Based UI Builder =====\nconst COMP_DEFAULTS = {\n sidebar: {w:80,h:320,bg:'#1a1a2e',border:0,borderColor:'#333',radius:0,label:'Sidebar'},\n header: {w:480,h:40,bg:'#16213e',border:0,borderColor:'#333',radius:0,label:'Header'},\n card: {w:140,h:100,bg:'#1e1e30',border:1,borderColor:'#444',radius:8,label:'Card'},\n button: {w:90,h:32,bg:'#4B49AC',border:0,borderColor:'#666',radius:6,label:'Button',text:'Button',textColor:'#FFFFFF',fontSize:12,textAlign:'center'},\n toggle: {w:44,h:22,bg:'#34C759',border:0,borderColor:'transparent',radius:11,label:'Toggle',toggleState:true},\n progressbar: {w:120,h:10,bg:'#333',border:0,borderColor:'transparent',radius:5,label:'Progress',value:65,barColor:'#4B49AC'},\n datavalue: {w:80,h:36,bg:'transparent',border:0,borderColor:'transparent',radius:0,label:'DataValue',text:'24.5°C',textColor:'#00FF88',fontSize:18,textAlign:'center'},\n label: {w:100,h:20,bg:'transparent',border:0,borderColor:'transparent',radius:0,label:'Label',text:'Label Text',textColor:'#CCCCCC',fontSize:12,textAlign:'left'},\n icon: {w:28,h:28,bg:'transparent',border:0,borderColor:'transparent',radius:0,label:'Icon',icon:'★',textColor:'#FFD700',fontSize:18,textAlign:'center'}\n};\n\nconst CONTAINERS = ['sidebar','header','card'];\n\nclass TFTBuilder {\n constructor(){\n this.screens=[{name:'Screen 1',components:[]}];\n this.currentScreen=0;\n this.selectedId=null;\n this.idCounter=1;\n this.history=[];\n this.historyIdx=-1;\n this.designId=0;\n this.orientation='landscape';\n this.init();\n }\n\n init(){\n this.bindDragDrop();\n this.renderScreenTabs();\n this.renderHierarchy();\n this.pushHistory();\n this.loadList();\n document.addEventListener('keydown',e=>{\n if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='SELECT')return;\n if(e.key==='Delete'||e.key==='Backspace')this.deleteSelected();\n if(e.ctrlKey&&e.key==='z'){e.preventDefault();this.undo();}\n if(e.ctrlKey&&e.key==='y'){e.preventDefault();this.redo();}\n });\n }\n\n // ===== ORIENTATION =====\n setOrientation(o){\n this.orientation=o;\n const f=document.getElementById('canvasFrame');\n if(o==='landscape'){f.style.width='480px';f.style.height='320px';}\n else{f.style.width='320px';f.style.height='480px';}\n this.renderCanvas();\n }\n getW(){return this.orientation==='landscape'?480:320;}\n getH(){return this.orientation==='landscape'?320:480;}\n\n // ===== DRAG & DROP FROM LIBRARY =====\n bindDragDrop(){\n document.querySelectorAll('.clib-item').forEach(el=>{\n el.addEventListener('dragstart',e=>{\n e.dataTransfer.setData('newComp',el.dataset.comp);\n e.dataTransfer.effectAllowed='copy';\n });\n });\n const canvas=document.getElementById('tftCanvas');\n canvas.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='copy';});\n canvas.addEventListener('drop',e=>{\n e.preventDefault();\n const compType=e.dataTransfer.getData('newComp');\n if(!compType)return;\n const rect=canvas.getBoundingClientRect();\n const x=Math.max(0,Math.round((e.clientX-rect.left)/5)*5);\n const y=Math.max(0,Math.round((e.clientY-rect.top)/5)*5);\n // Check if dropped on a container\n let parentId=null;\n const target=document.elementFromPoint(e.clientX,e.clientY);\n if(target){\n const parentEl=target.closest('.tft-comp[data-container=\"true\"]');\n if(parentEl)parentId=parentEl.dataset.id;\n }\n this.addComponent(compType,x,y,parentId);\n });\n canvas.addEventListener('click',e=>{\n if(e.target===canvas||e.target.id==='tftCanvas'){this.select(null);}\n });\n }\n\n // ===== COMPONENT MANAGEMENT =====\n addComponent(type,x=10,y=10,parentId=null){\n const d=JSON.parse(JSON.stringify(COMP_DEFAULTS[type]));\n const id='c'+this.idCounter++;\n const comp={id,type,x,y,...d,padding:4,children:[],interactions:{event:'none',action:'none',target:'',payload:''}};\n if(parentId){\n const parent=this.findComp(this.currentComps(),parentId);\n if(parent){\n // Position relative to parent\n const pr=this.getAbsPos(parentId);\n comp.x=Math.max(0,x-(pr?.x||0));\n comp.y=Math.max(0,y-(pr?.y||0));\n parent.children.push(comp);\n }else{this.currentComps().push(comp);}\n }else{this.currentComps().push(comp);}\n this.pushHistory();\n this.renderCanvas();\n this.select(id);\n return id;\n }\n\n currentComps(){return this.screens[this.currentScreen].components;}\n\n findComp(arr,id){\n for(const c of arr){\n if(c.id===id)return c;\n if(c.children){const f=this.findComp(c.children,id);if(f)return f;}\n }\n return null;\n }\n\n findParent(arr,id,parent=null){\n for(const c of arr){\n if(c.id===id)return{parent,list:arr};\n if(c.children){const f=this.findParent(c.children,id,c);if(f)return f;}\n }\n return null;\n }\n\n removeComp(id){\n const info=this.findParent(this.currentComps(),id);\n if(!info)return;\n const idx=info.list.findIndex(c=>c.id===id);\n if(idx>=0)info.list.splice(idx,1);\n }\n\n getAbsPos(id){\n // Walk up hierarchy to get absolute position\n const walk=(arr,tx,ty)=>{\n for(const c of arr){\n const ax=tx+c.x, ay=ty+c.y;\n if(c.id===id)return{x:ax,y:ay};\n if(c.children){const r=walk(c.children,ax,ay);if(r)return r;}\n }\n return null;\n };\n return walk(this.currentComps(),0,0);\n }\n\n deleteSelected(){\n if(!this.selectedId)return;\n this.removeComp(this.selectedId);\n this.selectedId=null;\n this.pushHistory();\n this.renderCanvas();\n this.renderHierarchy();\n this.clearProps();\n }\n\n // ===== SELECTION =====\n select(id){\n this.selectedId=id;\n document.querySelectorAll('.tft-comp').forEach(el=>el.classList.remove('selected'));\n if(id){\n const el=document.querySelector(`.tft-comp[data-id=\"${id}\"]`);\n if(el)el.classList.add('selected');\n this.renderProps();\n }else{this.clearProps();}\n this.renderHierarchy();\n }\n\n clearProps(){\n document.getElementById('propsContent').innerHTML='",
|
|
"Silakan klik 'X' untuk menutup jendela.",
|
|
".\\n\\n\"\n . \"Silakan sampaikan pertanyaan, laporan kendala, atau pesan Anda di sini kapan pun Anda butuhkan. Admin akan membalas pesan Anda dari Control Panel sesegera mungkin.\";\n \n $removeKeyboard = ['remove_keyboard' => true];\n \n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'HTML',\n 'reply_markup' => json_encode($removeKeyboard)\n ];\n file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n } else {\n // Nomor tidak terdaftar\n $text = \"Maaf, nomor handphone Anda ($phoneNumber) tidak ditemukan pada daftar Penghuni Identia.\\n\\nPastikan Anda mendaftar melalui Admin terlebih dahulu.\";\n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text\n ];\n file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n }\n exit;\n }\n \n // Check if the message contains media\n if (isset($update[\"message\"][\"photo\"]) || \n isset($update[\"message\"][\"document\"]) || \n isset($update[\"message\"][\"voice\"]) || \n isset($update[\"message\"][\"audio\"]) || \n isset($update[\"message\"][\"video\"]) ||\n isset($update[\"message\"][\"sticker\"]) ||\n isset($update[\"message\"][\"animation\"])) {\n \n $messageId = $update[\"message\"][\"message_id\"];\n \n // Hapus pesan media yang dikirimkan user tersebut langsung dari Telegram\n $deleteData = [\n 'chat_id' => $chatId,\n 'message_id' => $messageId\n ];\n file_get_contents($website.\"/deleteMessage?\".http_build_query($deleteData));\n\n // Kirim pesan peringatan ke user\n $text = \"⚠️",
|
|
"Waktu akses (5 menit) telah habis. Silakan gunakan perintah /forgot_password di Telegram lagi.",
|
|
"');\n \n if(response.status === 'success') {\n originalAccessValues[idOccupant] = accessValue; // Update original value\n showCustomAlert('Access berhasil diperbarui menjadi ' + accessValue);\n \n // Visual feedback on row\n var row = selectElement.closest('tr');\n row.css('background-color', '#ecfdf5');\n setTimeout(function() {\n row.css('background-color', '');\n }, 1000);\n } else {\n showCustomAlert(response.message || 'Gagal memperbarui access.');\n selectElement.val(originalAccessValues[idOccupant]); // Revert\n btn.prop('disabled', true);\n }\n },\n error: function() {\n selectElement.prop('disabled', false);\n selectElement.val(originalAccessValues[idOccupant]); // Revert\n btn.prop('disabled', true).html('",
|
|
"gagal menemukan jaringan WiFi dengan nama \"' + targetSsid + '\"",
|
|
"Detection Error';\n console.error('Face detection error:', err);\n });\n }\n\n document.getElementById('registerForm').addEventListener('submit', function(e) {\n if (document.getElementById('faceVerifiedFlag').value !== '1') {\n e.preventDefault();\n showCustomAlert('Foto Belum Diverifikasi', 'Upload foto profil dengan wajah yang terdeteksi terlebih dahulu.', 'error');\n return;\n }\n var pass = document.getElementById(\"register_password\").value;\n const pwValid = (pass.length >= 8 && /[a-z]/.test(pass) && /[A-Z]/.test(pass) && /\\d/.test(pass) && /[^A-Za-z0-9]/.test(pass));\n if (!pwValid) {\n e.preventDefault();\n showCustomAlert(\"Password belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!\", \"warning\");\n document.getElementById(\"register_password\").focus();\n }\n });\n\n document.getElementById('register_password').addEventListener('input', function() {\n var pass = this.value;\n var meter = document.getElementById('pwStrengthMeterReg');\n var bar = document.getElementById('pwStrengthBarReg');\n var text = document.getElementById('pwStrengthTextReg');\n\n if(pass.length > 0) {\n meter.style.display = 'flex';\n var hasLower = /[a-z]/.test(pass);\n var hasUpper = /[A-Z]/.test(pass);\n var hasNumber = /\\d/.test(pass);\n var hasSymbol = /[^A-Za-z0-9]/.test(pass);\n var typesCount = (hasLower?1:0) + (hasUpper?1:0) + (hasNumber?1:0) + (hasSymbol?1:0);\n \n if (pass.length >= 8 && typesCount === 4) {\n bar.style.width = '100%'; bar.style.backgroundColor = '#10b981'; // Kuat\n text.innerText = 'Password Kuat'; text.style.color = '#10b981';\n } else if (pass.length >= 6 && typesCount >= 2) {\n bar.style.width = '66%'; bar.style.backgroundColor = '#f59e0b'; // Sedang\n text.innerText = 'Password Sedang'; text.style.color = '#f59e0b';\n } else {\n bar.style.width = '33%'; bar.style.backgroundColor = '#ef4444'; // Lemah\n text.innerText = 'Password Lemah'; text.style.color = '#ef4444';\n }\n } else {\n meter.style.display = 'none';\n text.innerText = '';\n }\n });\n\n // Eye Toggle Logic\n var toggles = document.querySelectorAll('.eye-toggle');\n toggles.forEach(function(toggle) {\n toggle.addEventListener('click', function() {\n var targetId = this.getAttribute('data-target');\n var input = document.getElementById(targetId);\n var icon = this.querySelector('i');\n if (input.type === 'password') {\n input.type = 'text';\n icon.classList.remove('mdi-eye-off');\n icon.classList.add('mdi-eye');\n } else {\n input.type = 'password';\n icon.classList.remove('mdi-eye');\n icon.classList.add('mdi-eye-off');\n }\n });\n });\n });",
|
|
"' + msg;\n toast.style.display = 'flex';\n }\n\n function setProgress(pct) {\n if (loadProgress) loadProgress.style.width = Math.min(100, pct) + '%';\n }\n\n function updateSecChip(el, active) {\n if (!el) return;\n el.classList.remove('active','pending');\n el.classList.add(active ? 'active' : 'pending');\n }\n\n // ── Anti-screenshot / right-click ──\n document.addEventListener('contextmenu', function(e) { e.preventDefault(); });\n\n // ── Step 1: Validate Token ──\n function validateToken() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', 'validate_foto_token.php?token=' + encodeURIComponent(TOKEN), true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.valid) {\n occupantId = d.id_occupant;\n document.getElementById('nameTag').textContent = d.name;\n showPage(pgCamera);\n initCamera();\n startTokenCheck();\n } else {\n document.getElementById('errTitle').textContent =\n (d.message && d.message.indexOf('digunakan') !== -1) ? 'Token Sudah Digunakan' : 'Token Tidak Valid';\n document.getElementById('errMsg').textContent = d.message || 'Token tidak valid.';\n showPage(pgError);\n }\n } catch(e) {\n document.getElementById('errTitle').textContent = 'Kesalahan';\n document.getElementById('errMsg').textContent = 'Format respons tidak valid.';\n showPage(pgError);\n }\n };\n xhr.onerror = function() {\n document.getElementById('errTitle').textContent = 'Koneksi Gagal';\n document.getElementById('errMsg').textContent = 'Tidak dapat terhubung ke server.';\n showPage(pgError);\n };\n xhr.send();\n }\n\n // ── Periodic token validity check ──\n function startTokenCheck() {\n tokenCheckInterval = setInterval(function() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', 'validate_foto_token.php?token=' + encodeURIComponent(TOKEN), true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (!d.valid && !sessionSaved) {\n clearInterval(tokenCheckInterval);\n stopEverything();\n document.getElementById('errTitle').textContent = 'Token Kadaluwarsa';\n document.getElementById('errMsg').textContent = d.message || 'Token sudah tidak valid.';\n showPage(pgError);\n }\n } catch(e) {}\n };\n xhr.send();\n }, 30000);\n }\n\n function stopEverything() {\n clearInterval(detectLoop);\n clearInterval(brightInterval);\n if (stream) stream.getTracks().forEach(function(t) { t.stop(); });\n }\n\n // ── Step 2: Load models + camera ──\n var modelLoaded = false, landmarkLoaded = false, cameraStarted = false;\n\n function initCamera() {\n setProgress(5);\n loadText.textContent = 'Memuat library deteksi wajah...';\n\n function waitForLib(attempt) {\n if (attempt > 150) { // ~30s\n showLibError();\n return;\n }\n if (window._faceApiLoadFailed) {\n showLibError();\n return;\n }\n if (typeof faceapi === 'undefined') {\n setTimeout(function() { waitForLib(attempt + 1); }, 200);\n return;\n }\n setProgress(20);\n loadText.textContent = 'Library dimuat. Memulai kamera...';\n startAll();\n }\n\n function showLibError() {\n camLoading.style.display = 'none';\n document.getElementById('errTitle').textContent = 'Gagal Memuat Library';\n document.getElementById('errMsg').textContent = 'Library deteksi wajah tidak dapat dimuat. Periksa koneksi internet Anda dan coba lagi.';\n document.getElementById('errIconWrap').className = 'err-icon invalid';\n document.getElementById('errIcon').className = 'mdi mdi-cloud-off-outline';\n var retryBtn = document.getElementById('btnRetryLib');\n if (retryBtn) retryBtn.style.display = 'inline-flex';\n showPage(pgError);\n }\n\n function startAll() {\n var camPromise = startCamera();\n setProgress(30);\n\n // Load TinyFaceDetector\n loadText.textContent = 'Memuat model deteksi wajah...';\n loadModels(0);\n\n camPromise.then(function(ok) {\n cameraStarted = ok;\n setProgress(50);\n if (!ok) {\n camLoading.style.display = 'none';\n setStatus('err', 'Kamera tidak tersedia');\n return;\n }\n onReadyCheck();\n });\n }\n\n function loadModels(urlIdx) {\n if (urlIdx >= MODEL_URLS.length) {\n camLoading.style.display = 'none';\n setStatus('err', 'Gagal memuat model deteksi wajah');\n return;\n }\n var url = MODEL_URLS[urlIdx];\n Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(url),\n faceapi.nets.faceLandmark68TinyNet.loadFromUri(url)\n ]).then(function() {\n modelLoaded = true;\n landmarkLoaded = true;\n setProgress(80);\n loadText.textContent = 'Model siap!';\n onReadyCheck();\n }).catch(function() {\n loadModels(urlIdx + 1);\n });\n }\n\n function onReadyCheck() {\n if (cameraStarted && modelLoaded) {\n setProgress(100);\n setTimeout(function() {\n camLoading.style.display = 'none';\n setStatus('search', 'Posisikan wajah Anda...');\n startDetection();\n startBrightnessCheck();\n setTimeout(function() {\n if (instrPanel) instrPanel.style.opacity = '0';\n setTimeout(function() { if (instrPanel) instrPanel.style.display = 'none'; }, 500);\n }, 6000);\n }, 400);\n }\n }\n\n waitForLib(0);\n }\n\n function startCamera() {\n var isMobile = window.innerWidth",
|
|
"'error', 'message' => 'Invalid ID']);\n exit;\n }\n $stmt = $pdo->prepare(\"SELECT * FROM tft_designs WHERE id = ?\");\n $stmt->execute([$id]);\n $row = $stmt->fetch();\n if ($row) {\n echo json_encode(['status' => 'success', 'data' => $row]);\n } else {\n echo json_encode(['status' => 'error', 'message' => 'Design not found']);\n }\n break;\n\n // ========== LIST ALL DESIGNS ==========\n case 'list':\n $stmt = $pdo->query(\"SELECT id, name, version_hash, created_at, updated_at FROM tft_designs ORDER BY updated_at DESC\");\n $rows = $stmt->fetchAll();\n echo json_encode(['status' => 'success', 'data' => $rows]);\n break;\n\n // ========== DELETE DESIGN ==========\n case 'delete':\n $id = intval($_POST['id'] ?? 0);\n if ($id",
|
|
"Factory reset jika berulang",
|
|
"PIN diperlukan untuk melihat password di halaman operations.",
|
|
"\"\n data-no_hp=\"",
|
|
"WhatsApp belum diisi",
|
|
"— jarak terlalu jauh atau terhalang dinding tebal dapat menyebabkan sinyal tidak terdeteksi. Idealnya dalam jarak",
|
|
"dengan saluran pribadi Admin",
|
|
"'+tA(tf.last_seen);}\n if(bz){const e=bz.extra_data||{};el('info-bz').innerHTML=mr('mdi-music-note','Tone',e.last_tone||'None','c')+mr('mdi-counter','Beeps',e.beep_count||'0','b');el('ft-bz').innerHTML='",
|
|
"Forgot password?",
|
|
"function showCustomAlert(title, message, type='warning') {\n const alertBox = document.getElementById('customAlert');\n const icon = document.getElementById('customAlertIcon');\n \n alertBox.className = 'custom-alert ' + type;\n if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';\n else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';\n else icon.className = 'mdi mdi-alert-circle custom-alert-icon';\n\n document.getElementById('customAlertTitle').innerText = title;\n document.getElementById('customAlertMsg').innerText = message;\n \n alertBox.style.display = 'flex';\n alertBox.style.animation = 'slideIn 0.3s ease-out';\n\n setTimeout(() => closeCustomAlert(), 5000); // auto close after 5s\n }\n\n function closeCustomAlert() {\n const alertBox = document.getElementById('customAlert');\n alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';\n setTimeout(() => {\n alertBox.style.display = 'none';\n }, 300);\n }\n\n document.getElementById('loginFinger').addEventListener('click', async () => {\n try {\n if (!window.PublicKeyCredential) {\n showCustomAlert(\"Not Supported\", \"This browser does not support WebAuthn (Passkey / Fingerprint).\", \"error\");\n return;\n }\n\n const publicKey = {\n challenge: new Uint8Array(32),\n rpId: window.location.hostname,\n timeout: 60000,\n userVerification: \"required\"\n };\n\n const assertion = await navigator.credentials.get({ publicKey });\n if(assertion) {\n const credId = btoa(String.fromCharCode.apply(null, new Uint8Array(assertion.rawId)));\n \n const formData = new FormData();\n formData.append('credential_id', credId);\n\n const res = await fetch('verify_finger_login.php', {\n method: 'POST',\n body: formData\n });\n const result = await res.json();\n\n if (result.status === 'success') {\n showCustomAlert(\"Success\", \"Fingerprint login successful.\", \"success\");\n setTimeout(() => { window.location.href = 'dashboard.php'; }, 1000);\n } else {\n showCustomAlert(\"Access Denied\", result.message, \"error\");\n }\n }\n } catch(err) {\n console.error(err);\n // showCustomAlert(\"Cancelled\", \"Authentication cancelled or fingerprint not recognized.\", \"warning\");\n }\n });\n\n // Tampilkan error PHP jika ada menggunakan custom alert",
|
|
"saat melakukan scanning frekuensi 2.4GHz. Router mungkin sedang mati, berada di luar jangkauan, atau SSID dalam mode tersembunyi (hidden).' ;\n suggestions = [\n '",
|
|
"Bagikan QR code ini ke",
|
|
"Facebook belum diisi",
|
|
"0.6;\n\n // Face center validation\n var cx = box.x + box.width / 2;\n var cy = box.y + box.height / 2;\n var dx = Math.abs(cx - vw / 2) / vw;\n var dy = Math.abs(cy - vh / 2) / vh;\n var faceNotCenter = dx > 0.2 || dy > 0.2;\n\n if (faceTooSmall) {\n setFaceNotOk();\n setStatus('err', 'Wajah terlalu jauh — Dekatkan');\n updateSecChip(secFace, false);\n } else if (faceTooBig) {\n setFaceNotOk();\n setStatus('err', 'Wajah terlalu dekat — Mundur');\n updateSecChip(secFace, false);\n } else if (faceNotCenter) {\n setFaceNotOk();\n setStatus('err', 'Pusatkan wajah dalam bingkai');\n updateSecChip(secFace, false);\n } else {\n // Face is good\n faceOk = true;\n oval.classList.add('ok');\n corners.classList.add('ok');\n scanbar.classList.add('ok');\n updateSecChip(secFace, true);\n\n // Liveness: track landmark movement\n if (landmarks && landmarkLoaded) {\n var pts = landmarks.positions;\n if (prevLandmarks && pts.length === prevLandmarks.length) {\n var totalDiff = 0;\n for (var i = 0; i",
|
|
"/* ========================================================= */\n /* CUSTOM LOGOUT MODAL STYLING */\n /* ========================================================= */\n .custom-modal-logout {\n border-radius: 10px !important;\n box-shadow: none !important;\n overflow: hidden;\n background-color: #ffffff;\n }\n .custom-modal-logout .modal-body {\n position: relative;\n z-index: 1;\n }\n /* Hiasan background abstrak */\n .custom-modal-logout::before {\n content: \"\";\n position: absolute;\n top: -60px;\n left: -40px;\n width: 180px;\n height: 180px;\n background: rgba(255, 255, 255, 0.04);\n border-radius: 50%;\n z-index: 0;\n }\n .custom-modal-logout::after {\n content: \"\";\n position: absolute;\n bottom: -50px;\n right: -50px;\n width: 140px;\n height: 140px;\n background: rgba(255, 71, 71, 0.04);\n border-radius: 50%;\n z-index: 0;\n }\n \n .modal-icon-container {\n position: relative;\n width: 85px;\n height: 85px;\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 2;\n }\n .modal-icon-container .icon-circle {\n width: 100%;\n height: 100%;\n background: linear-gradient(135deg, #ff0000 0%, #ff4d4d 100%);\n border-radius: 50%;\n display: flex;\n justify-content: center;\n align-items: center;\n box-shadow: 0 8px 25px rgba(255, 0, 0, 0.5);\n animation: pulse-soft 2s infinite;\n }\n .modal-icon-container i {\n font-size: 36px;\n color: #ffffff;\n font-weight: bold;\n }\n .modal-title-custom {\n font-weight: 700;\n color: #2c2e33;\n font-size: 1.4rem;\n z-index: 2;\n position: relative;\n }\n .modal-desc-custom {\n font-size: 0.95rem;\n line-height: 1.6;\n z-index: 2;\n position: relative;\n padding: 0 10px;\n }\n .btn-cancel-custom {\n border-radius: 10px !important;\n font-weight: 600;\n padding: 10px 24px;\n color: #6c7383;\n background-color: #f4f5f7;\n border: 1px solid #e1e3e9;\n transition: all 0.3s ease;\n z-index: 2;\n position: relative;\n display: inline-flex;\n align-items: center;\n }\n .btn-cancel-custom:hover {\n background-color: #e4e6ea;\n transform: translateY(-2px);\n }\n .btn-logout-custom {\n border-radius: 10px !important;\n font-weight: 600;\n padding: 10px 26px;\n background: linear-gradient(135deg, #4B49AC 0%, #4B49AC 100%);\n border: none;\n transition: all 0.3s ease;\n z-index: 2;\n position: relative;\n color: #ffffff !important;\n display: inline-flex;\n align-items: center;\n }\n .btn-logout-custom:hover {\n background: linear-gradient(135deg, #4B49AC 0%, #4B49AC 100%);\n transform: translateY(-2px);\n color: #ffffff !important;\n }\n \n @keyframes pulse-soft {\n 0% { box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.5); }\n 70% { box-shadow: 0 0 0 20px rgba(255, 107, 107, 0); }\n 100% { box-shadow: 0 0 0 0 rgba(255, 107, 107, 0); }\n }\n /* Mengubah border radius semua button dengan class .btn */\n .btn {\n border-radius: 4px !important;\n }\n .btn-primary {\n border-radius: 6px !important;\n }\n .navbar {\n -webkit-box-shadow: none !important;\n -moz-box-shadow: none !important;\n box-shadow: none !important;\n border-bottom: 1px solid #ebedf2;\n }\n .sidebar {\n border-right: 1px solid #ebedf2;\n }\n .sidebar .nav {\n padding-top: 5px !important;\n margin-top: 5px !important;\n }\n .sidebar .nav > .nav-item:first-child {\n margin-top: 5px !important;\n padding-top: 5px !important;\n }\n .sidebar .nav .nav-item .nav-link {\n color: #504e55ff !important;\n display: flex !important;\n align-items: center !important;\n }\n .sidebar .nav .nav-item .nav-link i.menu-icon {\n color: #504e55ff !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n width: 20px !important;\n min-width: 20px !important;\n }\n .sidebar .nav .nav-item .nav-link .menu-title {\n color: #504e55ff !important;\n line-height: 1 !important;\n }\n .sidebar .nav .nav-item .nav-link i.menu-arrow {\n color: #504e55ff !important;\n display: flex !important;\n align-items: center !important;\n }\n /* Tetap putih saat active/hover */\n .sidebar .nav .nav-item.active > .nav-link,\n .sidebar .nav .nav-item.active > .nav-link .menu-title,\n .sidebar .nav .nav-item.active > .nav-link i.menu-icon,\n .sidebar .nav .nav-item.active > .nav-link i.menu-arrow,\n .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link,\n .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link .menu-title,\n .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link i.menu-icon,\n .sidebar .nav:not(.sub-menu) > .nav-item:hover > .nav-link i.menu-arrow {\n color: #fff !important;\n }\n\n /* ========================================================= */\n /* OVERRIDE TAMPILAN MENU AKTIF Identia */\n /* ========================================================= */\n \n /* 1. Hilangkan latar belakang biru dari seluruh blok",
|
|
"' + (d.message || 'Gagal menyimpan ke server');\n statusEl.className = \"mt-4 text-danger font-weight-bold\";\n btn.disabled = false;\n }\n }).catch(e => {\n statusEl.innerHTML = '",
|
|
"';\n return;\n }\n\n this.dropdown.innerHTML = '';\n filtered.forEach(item => {\n const div = document.createElement('div');\n div.className = 'ss-item';\n div.textContent = item.name;\n div.addEventListener('click', () => {\n this.input.value = item.name;\n this.hidden.value = (this.useIdAsValue && item.id) ? item.id : item.name;\n this.selectedId = item.id;\n this.dropdown.classList.remove('show');\n if (this.onSelect) this.onSelect(item);\n });\n this.dropdown.appendChild(div);\n });\n }\n}\n\n// ── Init Searchable Selects ──\nlet ssProvinsi, ssKabupaten, ssKecamatan, ssDesa, ssKodePos;\nlet provDataLoaded = false;\n\nfunction openEditAddressModal() {\n $('#modalEditAddress').modal('show');\n initAddressSelects();\n}\n\nfunction initAddressSelects() {\n if (!ssProvinsi) {\n ssProvinsi = new SearchableSelect('ssProvinsi', 'hid_provinsi', {\n onSelect: (item) => { loadAddrKabupaten(item.id); }\n });\n ssKabupaten = new SearchableSelect('ssKabupaten', 'hid_kabupaten', {\n onSelect: (item) => { loadAddrKecamatan(item.id); }\n });\n ssKecamatan = new SearchableSelect('ssKecamatan', 'hid_kecamatan', {\n onSelect: (item) => { loadAddrDesa(item.id); }\n });\n ssDesa = new SearchableSelect('ssDesa', 'hid_desa', {\n onSelect: (item) => { loadAddrKodePos(item.name); }\n });\n ssKodePos = new SearchableSelect('ssKodePos', 'hid_kode_pos', {\n useIdAsValue: true\n });\n }\n loadAddrProvinsi();\n}\n\nfunction loadAddrProvinsi() {\n if (provDataLoaded) return;\n provDataLoaded = true;\n ssProvinsi.showLoading();\n fetch(API_BASE + '/provinces.json').then(r => r.json()).then(data => {\n ssProvinsi.setItems(data.map(p => ({id: p.id, name: p.name})));\n ssProvinsi.dropdown.classList.remove('show');\n // Pre-populate if saved\n if (saved.provinsi) {\n const found = data.find(p => p.name === saved.provinsi);\n if (found) {\n ssProvinsi.setValue(found.name, found.id);\n loadAddrKabupaten(found.id);\n }\n }\n }).catch(() => { ssProvinsi.dropdown.classList.remove('show'); });\n}\n\nfunction loadAddrKabupaten(provId) {\n ssKabupaten.clear(); ssKecamatan.clear(); ssDesa.clear(); ssKodePos.clear();\n ssKabupaten.disable(); ssKecamatan.disable(); ssDesa.disable(); ssKodePos.disable();\n ssKabupaten.enable();\n ssKabupaten.showLoading();\n fetch(API_BASE + '/regencies/' + provId + '.json').then(r => r.json()).then(data => {\n ssKabupaten.setItems(data.map(k => ({id: k.id, name: k.name})));\n ssKabupaten.dropdown.classList.remove('show');\n if (saved.kabupaten) {\n const found = data.find(k => k.name === saved.kabupaten);\n if (found) {\n ssKabupaten.setValue(found.name, found.id);\n loadAddrKecamatan(found.id);\n }\n }\n }).catch(() => { ssKabupaten.dropdown.classList.remove('show'); });\n}\n\nfunction loadAddrKecamatan(kabId) {\n ssKecamatan.clear(); ssDesa.clear(); ssKodePos.clear();\n ssKecamatan.disable(); ssDesa.disable(); ssKodePos.disable();\n ssKecamatan.enable();\n ssKecamatan.showLoading();\n fetch(API_BASE + '/districts/' + kabId + '.json').then(r => r.json()).then(data => {\n ssKecamatan.setItems(data.map(k => ({id: k.id, name: k.name})));\n ssKecamatan.dropdown.classList.remove('show');\n if (saved.kecamatan) {\n const found = data.find(k => k.name === saved.kecamatan);\n if (found) {\n ssKecamatan.setValue(found.name, found.id);\n loadAddrDesa(found.id);\n }\n }\n }).catch(() => { ssKecamatan.dropdown.classList.remove('show'); });\n}\n\nfunction loadAddrDesa(kecId) {\n ssDesa.clear(); ssKodePos.clear();\n ssDesa.disable(); ssKodePos.disable();\n ssDesa.enable();\n ssDesa.showLoading();\n fetch(API_BASE + '/villages/' + kecId + '.json').then(r => r.json()).then(data => {\n ssDesa.setItems(data.map(d => ({id: d.id, name: d.name})));\n ssDesa.dropdown.classList.remove('show');\n if (saved.desa) {\n const found = data.find(d => d.name === saved.desa);\n if (found) {\n ssDesa.setValue(found.name, found.id);\n loadAddrKodePos(found.name);\n }\n }\n }).catch(() => { ssDesa.dropdown.classList.remove('show'); });\n}\n\nfunction loadAddrKodePos(desaName) {\n ssKodePos.clear();\n ssKodePos.disable();\n ssKodePos.enable();\n ssKodePos.showLoading();\n const q = encodeURIComponent(desaName);\n fetch(KODEPOS_API + '/search/?q=' + q).then(r => r.json()).then(res => {\n const results = res.data || res || [];\n const items = [];\n const seen = new Set();\n results.forEach(r => {\n const kp = r.postalcode || r.kodepos || r.postal_code || '';\n if (kp && !seen.has(kp)) {\n seen.add(kp);\n const label = kp + ' — ' + (r.village || r.urban || r.kelurahan || r.desa || desaName);\n items.push({id: kp, name: label});\n }\n });\n ssKodePos.setItems(items);\n ssKodePos.dropdown.classList.remove('show');\n if (saved.kode_pos) {\n const found = items.find(i => i.id === saved.kode_pos);\n if (found) ssKodePos.setValue(found.name, found.id);\n }\n // If no results from API, allow manual input\n if (items.length === 0) {\n ssKodePos.input.placeholder = 'Ketik kode pos manual...';\n ssKodePos.input.addEventListener('blur', function() {\n if (this.value.trim()) ssKodePos.hidden.value = this.value.trim();\n }, {once: true});\n }\n }).catch(() => {\n ssKodePos.dropdown.classList.remove('show');\n ssKodePos.input.placeholder = 'Ketik kode pos manual...';\n ssKodePos.input.addEventListener('blur', function() {\n if (this.value.trim()) ssKodePos.hidden.value = this.value.trim();\n }, {once: true});\n });\n}\n\n// ── Validasi ──\ndocument.getElementById('edit_email').addEventListener('blur', function() {\n const fb = document.getElementById('emailFeedback');\n const v = this.value.trim();\n if (!v) { fb.className = 'pf-fb'; return; }\n fb.innerHTML = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v) ? '",
|
|
"\";\n }\n $keyboard = ['keyboard' => [[['text' => '📱 Bagikan Kontak Saya', 'request_contact' => true]]], 'resize_keyboard' => true, 'is_persistent' => true];\n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'HTML',\n 'reply_markup' => json_encode($keyboard)\n ];\n file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n }\n }\n}\n\n\n// Handle Pesan Diedit (Occupant mengedit pesan di Telegram)\nif (isset($update[\"edited_message\"])) {\n $chatId = $update[\"edited_message\"][\"chat\"][\"id\"];\n \n if (isset($update[\"edited_message\"][\"text\"])) {\n $editedText = $update[\"edited_message\"][\"text\"];\n $messageId = $update[\"edited_message\"][\"message_id\"];\n \n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE telegram_id = ?\");\n $stmt->execute([$chatId]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n // Kita tandai sebagai (Diedit) di akhir teks agar admin tahu\n $newText = htmlspecialchars($editedText) . \"",
|
|
"Opsi Perangkat Lain",
|
|
"Kembali ke beranda",
|
|
"QR Code Registrasi Foto",
|
|
"function filterNtf(event, filterType, el) {\n event.preventDefault();\n \n // Update active class on filter tags\n document.querySelectorAll('.ntf-filter-item').forEach(e => e.classList.remove('active'));\n el.classList.add('active');\n \n // Logic to show/hide based on filterType\n const activeWrap = document.getElementById('activeListWrapper');\n const trashWrap = document.getElementById('trashListWrapper');\n const mainTitle = document.getElementById('mainListTitle');\n const btnEmpty = document.getElementById('btnEmptyTrash');\n \n if(filterType === 'trash') {\n activeWrap.style.display = 'none';\n trashWrap.style.display = 'block';\n mainTitle.textContent = 'Notifikasi di Sampah';\n btnEmpty.style.display = 'block';\n return;\n }\n \n activeWrap.style.display = 'block';\n trashWrap.style.display = 'none';\n mainTitle.textContent = 'Daftar Notifikasi Terbaru';\n btnEmpty.style.display = 'none';\n\n const allCards = activeWrap.querySelectorAll('.ntf-card-item');\n allCards.forEach(card => {\n let isMatch = false;\n if(filterType === 'all') isMatch = true;\n if(filterType === 'unread' && card.dataset.unread === '1') isMatch = true;\n if(filterType === 'danger' && card.dataset.danger === '1') isMatch = true;\n \n card.style.display = isMatch ? 'flex' : 'none';\n });\n \n // Hide date headers if they have no visible descendants\n document.querySelectorAll('.ntf-group-wrap').forEach(wrap => {\n const visibleCards = wrap.querySelectorAll('.ntf-card-item[style*=\"display: flex\"], .ntf-card-item:not([style*=\"display: none\"])');\n \n // Find the corresponding date-hdr using class matching\n const classList = Array.from(wrap.classList).find(c => c.startsWith('ntf-date-group-'));\n const hdr = document.querySelector('.ntf-date-hdr.'+classList);\n \n if(visibleCards.length === 0) {\n wrap.style.display = 'none';\n if(hdr) hdr.style.display = 'none';\n } else {\n wrap.style.display = 'block';\n if(hdr) hdr.style.display = 'block';\n }\n });\n}\n\nfunction showNtf(el, id) {\n const isDanger = el.dataset.danger === '1';\n \n // Fill data\n document.getElementById('nmdl-title').textContent = el.dataset.title;\n document.getElementById('nmdl-msg').textContent = el.dataset.msg;\n document.getElementById('nmdl-dtl').textContent = el.dataset.detail;\n document.getElementById('nmdl-time').textContent = el.dataset.time + ' - ' + el.querySelector('.nc-ago').textContent;\n \n // Icon\n document.getElementById('nmdl-icn').className = 'mdi ' + el.dataset.icnclass;\n \n // Theme\n const hdr = document.getElementById('nmdl-hdr');\n if(isDanger) {\n hdr.parentElement.className = 'modal-content nt-modal-simple modal-theme-danger';\n } else {\n hdr.parentElement.className = 'modal-content nt-modal-simple modal-theme-info';\n }\n\n // Mark as read conceptually and in DB\n if (el.classList.contains('unread')) {\n el.classList.remove('unread');\n el.dataset.unread = '0';\n \n // Update unread count in UI\n let cnt = document.getElementById('cntUnread');\n if(cnt) {\n let n = parseInt(cnt.textContent) - 1;\n cnt.textContent = n;\n if(n",
|
|
"');\n $('#esp32-ssid').text('Unknown');\n $('#esp32-ip').text('Unknown');\n }\n }, 'json').fail(function() {\n console.error(\"Failed to poll ESP32 status\");\n });\n }\n \n // Poll every 3 seconds\n setInterval(pollESP32Status, 3000);\n pollESP32Status();\n\n // SIMULATE BATTERY FULL (By clicking on battery percentage gauge)\n $('#battery-percent-wrap').css('cursor', 'pointer').attr('title', 'Click to simulate Battery Full').on('click', function() {\n batLevel = 100;\n $.post('api_notifications.php', {\n action: 'create_notification',\n icon: 'mdi-battery-charging-100',\n title: 'Baterai Smart Door Penuh',\n message: 'Kapasitas baterai telah mencapai 100%.',\n detail: 'Pengecasan dapat dihentikan untuk menjaga umur baterai.',\n is_danger: 0\n });\n alert('Simulasi Baterai Penuh dipicu! Cek notifikasi.');\n });\n\n // ===== BATTERY MONITORING CHART =====\n var maxPts = 40, batLevel = 85, batLabels = [], batData = [];\n var now = new Date();\n for (var i = maxPts - 1; i >= 0; i--) {\n var t = new Date(now.getTime() - i * 2000);\n batLabels.push(t.toLocaleTimeString('id-ID', {hour:'2-digit', minute:'2-digit', second:'2-digit'}));\n batData.push(batLevel + Math.random() * 2 - 1);\n }\n var ctx = document.getElementById('battery-chart').getContext('2d');\n var grad = ctx.createLinearGradient(0, 0, 0, 200);\n grad.addColorStop(0, 'rgba(75,73,172,0.25)');\n grad.addColorStop(1, 'rgba(75,73,172,0.02)');\n var batChart = new Chart(ctx, {\n type:'line',\n data:{ labels:batLabels, datasets:[{ label:'Battery %', data:batData, borderColor:'#4B49AC', backgroundColor:grad, borderWidth:2.5, fill:true, tension:0.4, pointRadius:0, pointHoverRadius:5, pointHoverBackgroundColor:'#4B49AC' }] },\n options:{ responsive:true, maintainAspectRatio:false, animation:{duration:600}, plugins:{ legend:{display:false}, tooltip:{ backgroundColor:'#2c2e33', padding:10, cornerRadius:8, callbacks:{ label:function(c){ return c.parsed.y.toFixed(1)+'%'; } } } }, scales:{ x:{ display:true, grid:{display:false}, ticks:{font:{size:10},color:'#bbb',maxRotation:0,maxTicksLimit:8} }, y:{ display:true, min:0, max:100, grid:{color:'#f5f5f5'}, ticks:{font:{size:10},color:'#bbb',callback:function(v){return v+'%';}} } } }\n });\n setInterval(function() {\n batLevel -= (Math.random()*0.5+0.1);\n if(batLevel",
|
|
"Belum Dibaca",
|
|
"tag - check immediately\n if (typeof faceapi === 'undefined') {\n showLibError();\n return;\n }\n setProgress(20);\n loadText.textContent = 'Library dimuat. Memulai kamera...';\n startAll();\n\n function showLibError() {\n camLoading.style.display = 'none';\n document.getElementById('errTitle').textContent = 'Failed Memuat Library';\n document.getElementById('errMsg').textContent = 'Library deteksi wajah tidak dapat dimuat. Periksa koneksi internet Anda dan coba lagi.';\n document.getElementById('errIconWrap').className = 'err-icon invalid';\n document.getElementById('errIcon').className = 'mdi mdi-cloud-off-outline';\n var retryBtn = document.getElementById('btnRetryLib');\n if (retryBtn) retryBtn.style.display = 'inline-flex';\n showPage(pgError);\n }\n\n function startAll() {\n var camPromise = startCamera();\n setProgress(30);\n\n // Load TinyFaceDetector\n loadText.textContent = 'Memuat model deteksi wajah...';\n loadModels(0);\n\n camPromise.then(function(ok) {\n cameraStarted = ok;\n setProgress(50);\n if (!ok) {\n camLoading.style.display = 'none';\n setStatus('err', 'Kamera tidak tersedia');\n return;\n }\n onReadyCheck();\n });\n }\n\n function loadModels(urlIdx) {\n if (urlIdx >= MODEL_URLS.length) {\n camLoading.style.display = 'none';\n setStatus('err', 'Failed memuat model deteksi wajah');\n return;\n }\n var url = MODEL_URLS[urlIdx];\n Promise.all([\n faceapi.nets.tinyFaceDetector.loadFromUri(url),\n faceapi.nets.faceLandmark68Net.loadFromUri(url)\n ]).then(function() {\n modelLoaded = true;\n landmarkLoaded = true;\n setProgress(80);\n loadText.textContent = 'Model siap!';\n onReadyCheck();\n }).catch(function() {\n loadModels(urlIdx + 1);\n });\n }\n\n function onReadyCheck() {\n if (cameraStarted && modelLoaded) {\n setProgress(100);\n setTimeout(function() {\n camLoading.style.display = 'none';\n setStatus('search', 'Posisikan wajah Anda...');\n startDetection();\n startBrightnessCheck();\n }, 400);\n }\n }\n\n }\n\n function startCamera() {\n var isMobile = window.innerWidth",
|
|
"Sisa waktu: 5:00",
|
|
"/* CSS for Chat Interface */\n.chat-wrapper {\n display: flex;\n height: 600px;\n background: #fff;\n border-radius: 12px;\n overflow: hidden;\n}\n\n/* Custom Minimalist SweetAlert */\ndiv.swal2-popup.swal2-custom-small {\n width: 270px !important;\n padding: 10px 15px 15px 15px !important;\n border-radius: 12px !important;\n}\n.swal2-custom-small .swal2-icon {\n width: 38px !important;\n height: 38px !important;\n margin: 5px auto 5px auto !important;\n border-width: 2px !important;\n}\n.swal2-custom-small .swal2-icon .swal2-icon-content {\n font-size: 24px !important;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.swal2-custom-small .swal2-title {\n font-size: 15px !important;\n padding: 0 !important;\n margin: 0 !important;\n font-weight: 600 !important;\n}\n.swal2-custom-small .swal2-html-container {\n font-size: 12px !important;\n margin: 4px 0 10px 0 !important;\n color: #666;\n}\n.swal2-custom-small .swal2-actions {\n margin-top: 5px;\n gap: 6px;\n}\n.swal2-custom-small .swal2-actions button {\n font-size: 12px !important;\n padding: 6px 10px !important;\n border-radius: 6px !important;\n border: none !important;\n outline: none !important;\n box-shadow: none !important;\n color: #fff !important;\n}\n.swal2-custom-small .swal-btn-delete-all { background-color: #ef4444 !important; }\n.swal2-custom-small .swal-btn-delete-me { background-color: #f59e0b !important; }\n.swal2-custom-small .swal-btn-cancel { background-color: #94a3b8 !important; }\n\n.chat-sidebar {\n width: 340px;\n border-right: 1px solid #f0f0f0;\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n}\n.chat-main {\n flex: 1;\n display: flex;\n flex-direction: column;\n background: #fafbfe;\n position: relative;\n}\n.chat-search-header {\n padding: 18px 20px;\n border-bottom: 1px solid #f0f0f0;\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.chat-search-header img {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n object-fit: cover;\n}\n.chat-search-box {\n flex: 1;\n position: relative;\n}\n.chat-search-box input {\n width: 100%;\n padding: 8px 16px 8px 36px;\n border-radius: 20px;\n border: 1px solid #e0e0e0;\n font-size: 13px;\n outline: none;\n transition: 0.2s;\n}\n.chat-search-box input:focus {\n border-color: #5856d6;\n box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);\n}\n.chat-search-box i {\n position: absolute;\n left: 12px;\n top: 50%;\n transform: translateY(-50%);\n color: #999;\n font-size: 14px;\n}\n\n.chat-list-wrapper {\n flex: 1;\n overflow-y: auto;\n}\n.chat-list-wrapper::-webkit-scrollbar { width: 6px; }\n.chat-list-wrapper::-webkit-scrollbar-thumb { background: #ddd; border-radius: 4px; }\n\n.section-title {\n padding: 16px 20px 8px;\n font-size: 14px;\n font-weight: 600;\n color: #5856d6;\n}\n\n.contact-item {\n display: flex;\n padding: 12px 20px;\n gap: 12px;\n cursor: pointer;\n transition: 0.2s;\n align-items: center;\n}\n.contact-item:hover {\n background: #f8faff;\n}\n.contact-item.active {\n background: #5856d6;\n}\n.contact-item.active .ci-name { color: #fff; }\n.contact-item.active .ci-msg { color: #e0e7ff; }\n.contact-item.active .ci-time { color: #c7d2fe; }\n\n.ci-avatar {\n width: 42px;\n height: 42px;\n border-radius: 50%;\n position: relative;\n}\n.ci-avatar img {\n width: 100%;\n height: 100%;\n border-radius: 50%;\n object-fit: cover;\n}\n.ci-status {\n position: absolute;\n bottom: 2px;\n right: 2px;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #10b981;\n border: 2px solid #fff;\n}\n.contact-item.active .ci-status { border-color: #5856d6; }\n\n.ci-info {\n flex: 1;\n min-width: 0;\n}\n.ci-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n margin-bottom: 4px;\n}\n.ci-name {\n font-size: 14px;\n color: #333;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ci-time {\n font-size: 11px;\n color: #999;\n flex-shrink: 0;\n}\n.ci-msg {\n font-size: 12px;\n color: #666;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ci-unread-badge {\n background: #5856d6;\n color: #fff;\n font-size: 10px;\n font-weight: bold;\n padding: 2px 6px;\n border-radius: 10px;\n margin-left: 6px;\n display: inline-block;\n}\n\n/* Right Blank View */\n.chat-blank {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n color: #666;\n}\n.chat-blank-icon {\n width: 100px;\n height: 100px;\n border-radius: 50%;\n background: #eff6ff;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 20px;\n}\n.chat-blank-icon i { font-size: 40px; color: #5856d6; }\n\n/* Right Active View */\n.chat-active-view {\n display: none;\n flex-direction: column;\n height: 100%;\n}\n.chat-header {\n padding: 16px 24px;\n background: #fff;\n border-bottom: 1px solid #f0f0f0;\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n.ch-left {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.ch-left img {\n width: 44px;\n height: 44px;\n border-radius: 50%;\n object-fit: cover;\n}\n.ch-name {\n font-size: 15px;\n font-weight: 600;\n color: #333;\n margin-bottom: 2px;\n}\n.ch-status {\n font-size: 12px;\n color: #666;\n}\n.ch-right {\n display: flex;\n gap: 16px;\n color: #666;\n}\n.ch-right i {\n font-size: 18px;\n cursor: pointer;\n transition: 0.2s;\n}\n.ch-right i:hover { color: #5856d6; }\n\n.chat-history {\n flex: 1;\n padding: 24px;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 20px;\n}\n.chat-history::-webkit-scrollbar { width: 6px; }\n.chat-history::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }\n\n.msg-row {\n display: flex;\n align-items: flex-end;\n gap: 8px;\n max-width: 80%;\n}\n.msg-row img {\n width: 28px;\n height: 28px;\n border-radius: 50%;\n object-fit: cover;\n flex-shrink: 0;\n}\n.msg-content {\n display: flex;\n flex-direction: column;\n}\n.msg-bubble {\n padding: 6px 7px 4px 9px;\n border-radius: 7.5px;\n font-size: 14.2px;\n line-height: 19px;\n box-shadow: 0 1px .5px rgba(11,20,26,.13);\n position: relative;\n color: #111b21;\n word-break: break-all; /* Force long URLs/strings to break anywhere */\n white-space: pre-wrap;\n}\n.msg-bubble p {\n margin-bottom: 0;\n margin-top: 0;\n}\n\n.msg-time {\n font-size: 11px;\n color: #667781;\n display: inline-flex;\n align-items: center;\n float: right;\n margin-top: 4px;\n margin-bottom: -4px;\n margin-left: 12px;\n}\n\n/* WhatsApp-like message status */\n.msg-status { display: inline-flex; align-items: center; margin-left: 3px; }\n.msg-status .mdi { font-size: 15.5px; line-height: 1; margin-top: 1px;}\n.msg-status-sending .mdi { color: #8696a0; animation: spinCheck 1s linear infinite; font-size: 13px;}\n.msg-status-sent .mdi { color: #8696a0; }\n.msg-status-delivered .mdi { color: #8696a0; }\n.msg-status-read .mdi { color: #53bdeb; }\n@keyframes spinCheck { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\n\n/* Optimistic message (temporary) */\n.msg-row.msg-sending { opacity: .7; }\n.msg-row.msg-sending .msg-bubble { position: relative; }\n\n/* Occupant Msg (Left) */\n.msg-left {\n align-self: flex-start;\n}\n.msg-left .msg-bubble {\n background: #ffffff;\n border-top-left-radius: 0;\n}\n.msg-left .msg-time { color: #667781; }\n\n/* Admin Msg (Right) - Updated to Primary Color */\n.msg-right {\n align-self: flex-end;\n flex-direction: row-reverse;\n}\n.msg-right .msg-bubble {\n background: #5856d6;\n color: #ffffff;\n border-top-right-radius: 0;\n}\n.msg-right .msg-time { color: #667781; }\n.msg-right .msg-status .mdi { color: #8696a0 !important; }\n.msg-right .msg-status-read .mdi { color: #53bdeb !important; }\n.edit-tag { \n float: right !important; \n margin-top: 2px !important; \n margin-left: 8px !important; \n font-size: 11.5px !important;\n}\n.msg-right .edit-tag { color: rgba(255,255,255,0.8) !important; }\n.msg-left .edit-tag { color: #8696a0 !important; }\n\n/* Chat Footer */\n.chat-footer {\n padding: 16px 24px;\n background: #fff;\n border-top: 1px solid #f0f0f0;\n display: flex;\n flex-direction: column;\n}\n.chat-footer-top {\n display: flex;\n align-items: center;\n gap: 16px;\n width: 100%;\n}\n.reply-preview-box {\n display: none;\n width: 100%;\n margin-bottom: 12px;\n padding: 10px 14px;\n background: #f8fafc;\n border-left: 4px solid #5856d6;\n border-radius: 6px;\n position: relative;\n font-size: 13px;\n}\n.rp-close {\n position: absolute;\n right: 12px;\n top: 50%;\n transform: translateY(-50%);\n cursor: pointer;\n color: #94a3b8;\n font-size: 18px;\n}\n.rp-close:hover { color: #ef4444; }\n.rp-title { font-weight: bold; color: #5856d6; margin-bottom: 2px;}\n.rp-text { color: #64748b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 20px;}\n\n.msg-reply-block {\n background: rgba(0,0,0,0.05);\n border-left: 3px solid #5856d6;\n padding: 6px 10px;\n border-radius: 4px;\n margin-bottom: 6px;\n font-size: 12px;\n cursor: pointer;\n}\n.msg-reply-block .replier { font-weight: bold; color: #5856d6; }\n.msg-right .msg-reply-block { background: rgba(255,255,255,0.2); border-color: #fff; }\n.msg-right .msg-reply-block .replier { color: #fff; }\n\n.chat-footer .input-wrapper {\n flex: 1;\n background: #f8fafc;\n border-radius: 20px;\n border: 1px solid #e2e8f0;\n display: flex;\n align-items: flex-end; /* Align to bottom for textarea growth */\n padding: 8px 16px;\n transition: 0.2s;\n}\n.chat-footer .input-wrapper:focus-within {\n border-color: #5856d6;\n background: #fff;\n box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1);\n}\n.chat-footer textarea {\n flex: 1;\n border: none;\n background: transparent;\n outline: none;\n padding: 8px;\n font-size: 14px;\n color: #333;\n font-family: inherit;\n resize: none;\n max-height: 120px;\n overflow-y: auto;\n box-sizing: border-box;\n /* Hide scrollbar for cleaner look */\n -ms-overflow-style: none;\n scrollbar-width: none;\n}\n.chat-footer textarea::-webkit-scrollbar {\n display: none;\n}\n\n#btnEmoji {\n background: transparent;\n border: none;\n outline: none;\n box-shadow: none;\n color: #64748b;\n font-size: 20px;\n padding: 0;\n margin: 0;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n#btnEmoji:hover {\n color: #5856d6;\n}\n\n.btn-send {\n background: #5856d6;\n color: #fff;\n border: none;\n border-radius: 8px;\n padding: 10px 20px;\n font-weight: 600;\n font-size: 14px;\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n transition: 0.2s;\n box-shadow: none;\n}\n.btn-send:hover { background: #4b49ac; transform: translateY(-1px); }\n\n/* Emoji Picker */\n.emoji-picker {\n display: none; position: absolute; bottom: 70px; right: 24px;\n width: 320px; background: #fff; border-radius: 12px;\n box-shadow: 0 8px 24px rgba(0,0,0,.12); border: 1px solid #edf2f7;\n z-index: 100; overflow: hidden;\n}\n.emoji-picker.show { display: block; }\n.emoji-tabs { display: flex; border-bottom: 1px solid #f1f5f9; padding: 0 4px; }\n.emoji-tab {\n flex: 1; padding: 8px 0; text-align: center; cursor: pointer;\n font-size: 16px; opacity: .5; transition: .12s; border-bottom: 2px solid transparent;\n}\n.emoji-tab:hover { opacity: .8; }\n.emoji-tab.active { opacity: 1; border-bottom-color: #5856d6; }\n.emoji-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 2px; padding: 8px; max-height: 200px; overflow-y: auto; }\n.emoji-grid::-webkit-scrollbar { width: 4px; }\n.emoji-grid::-webkit-scrollbar-thumb { background: #ddd; border-radius: 4px; }\n.emoji-item { font-size: 20px; text-align: center; padding: 6px; cursor: pointer; border-radius: 6px; transition: .1s; }\n.emoji-item:hover { background: #f1f5f9; transform: scale(1.2); }\n\n\n/* Custom Context Menu */\n.chat-context-menu {\n position: fixed; z-index: 9999; width: 180px; background: #fff;\n border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.15);\n padding: 6px 0; display: none; font-size: 13px;\n border: 1px solid #edf2f7;\n}\n.chat-context-menu ul { list-style: none; padding: 0; margin: 0; }\n.chat-context-menu li {\n display: flex; align-items: center; gap: 10px; padding: 9px 16px;\n cursor: pointer; transition: background .12s; color: #334155;\n}\n.chat-context-menu li:hover { background: #f1f5f9; }\n.chat-context-menu li i { font-size: 16px; width: 18px; text-align: center; color: #64748b; }\n.chat-context-menu li.ctx-danger { color: #ef4444; }\n.chat-context-menu li.ctx-danger i { color: #ef4444; }\n.chat-context-menu .ctx-sep { height: 1px; background: #f1f5f9; margin: 4px 0; }\n\n/* Selection Mode */\n.msg-checkbox {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n font-size: 22px;\n color: #94a3b8;\n display: none;\n cursor: pointer;\n z-index: 10;\n}\n.msg-row { position: relative; transition: 0.2s; }\n.msg-row.selectable {\n cursor: pointer;\n}\n/* Admin Chat Checkbox */\n.msg-right .msg-checkbox {\n left: 15px; /* sedikit ditambah jaraknya */\n}\n.msg-right.selectable {\n padding-left: 45px; /* memberi ruang */\n}\n/* Occupant Chat Checkbox */\n.msg-left .msg-checkbox {\n position: static;\n transform: none;\n margin-left: 15px;\n align-self: center;\n order: 3;\n}\n.msg-left.selectable {\n padding-left: 0;\n}\n.msg-row.selectable .msg-checkbox {\n display: block;\n}\n.msg-row.selected .msg-checkbox i::before {\n content: \"\\F0134\"; /* mdi-checkbox-marked-circle */\n color: #5856d6;\n}\n.msg-row.selected .msg-bubble {\n background: rgba(88, 86, 214, 0.15) !important;\n color: #111b21 !important;\n}\n.msg-row.msg-right.selected .msg-bubble {\n background: rgba(88, 86, 214, 0.8) !important;\n color: #fff !important;\n}\n.ch-right-actions {\n display: none;\n align-items: center;\n gap: 15px;\n}\n.ch-right-actions i {\n font-size: 22px;\n cursor: pointer;\n color: #64748b;\n}\n.ch-right-actions i.action-del { color: #ef4444; }\n.ch-right-actions i.action-close:hover { color: #333; }\n\n/* ===== RESPONSIVE MOBILE (WhatsApp-like) ===== */\n.chat-back-btn {\n display: none;\n background: none;\n border: none;\n font-size: 22px;\n color: #5856d6;\n cursor: pointer;\n padding: 0;\n margin-right: 8px;\n line-height: 1;\n}\n@media (max-width: 768px) {\n .chat-wrapper {\n height: calc(100vh - 120px);\n }\n .chat-sidebar {\n width: 100%;\n border-right: none;\n }\n .chat-main {\n display: none !important;\n width: 100%;\n }\n .chat-wrapper.mobile-chat-open .chat-sidebar {\n display: none !important;\n }\n .chat-wrapper.mobile-chat-open .chat-main {\n display: flex !important;\n width: 100%;\n }\n .chat-back-btn {\n display: inline-flex;\n }\n .emoji-picker {\n width: 280px;\n right: 10px;\n bottom: 60px;\n }\n}",
|
|
"PIN digunakan untuk melihat password asli. Pastikan Anda",
|
|
"Hanya 1 wajah yang boleh terdeteksi",
|
|
"\"\n data-title=\"",
|
|
"Simpan kode ini.",
|
|
"Informasi pribadi dan kontak Anda,",
|
|
"').removeClass('btn-light').addClass('btn-outline-secondary').attr('title', 'Select Multiple');\n $('#btnBulkDelete').hide();\n $('.btn-row-delete').css('display', 'inline-block');\n $('.bulk-cb').hide().prop('checked', false);\n }\n });\n\n $('#btnBulkDelete').on('click', function() {\n const selected = $('.bulk-cb:checked').map(function() { return $(this).val(); }).get();\n if(selected.length === 0) {\n showCustomAlert('Pilih minimal satu data untuk dihapus!', 'warning');\n return;\n }\n window.bulkDeleteIds = selected;\n document.getElementById(\"delete_id_account\").value = \"BULK\"; \n document.getElementById(\"delete_account_name\").innerText = selected.length + \" data terpilih\"; \n $(\"#modalDeleteAccount\").modal(\"show\"); \n });\n\n $(\"#formEditAccount\").submit(function(e) { \n e.preventDefault(); \n \n // Check face if a new photo was uploaded\n var editFoto = document.getElementById('edit_foto');\n if (editFoto.files.length > 0 && document.getElementById('faceVerifiedFlagEdit').value !== '1') {\n showCustomAlert('Foto yang diupload tidak memiliki wajah terdeteksi. Gunakan foto dengan wajah yang jelas.', 'warning');\n return;\n }\n \n var pw1 = document.getElementById(\"edit_password\").value;\n var pw2 = document.getElementById(\"edit_confirm_password\").value;\n \n if (pw1 !== \"\") {\n const pwValid = (pw1.length >= 8 && /[a-z]/.test(pw1) && /[A-Z]/.test(pw1) && /\\d/.test(pw1) && /[^A-Za-z0-9]/.test(pw1));\n if (!pwValid) {\n showCustomAlert(\"Password baru belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!\", \"warning\");\n document.getElementById(\"edit_password\").focus();\n return;\n }\n }\n \n if (pw1 !== pw2) {\n document.getElementById(\"editPasswordMismatch\").style.display = \"block\";\n return;\n } else {\n document.getElementById(\"editPasswordMismatch\").style.display = \"none\";\n }\n \n var btn = document.getElementById(\"btnEditAccount\"); \n btn.disabled = true; \n btn.innerHTML = '",
|
|
"Password salah atau sinyal lemah.",
|
|
"Belum diisi",
|
|
"Tidak ada kegiatan di tanggal ini",
|
|
"Klik nama di sebelah kiri untuk melihat pesan otomatis dari Telegram",
|
|
"Data Account",
|
|
"Simpan';\n if (d.status === \"success\") { \n $(\"#modalEdit\").modal(\"hide\"); \n showCustomAlert(\"Akun berhasil diubah.\");\n setTimeout(function(){ location.reload(); }, 1000);\n } else { \n showCustomAlert(d.message || \"Gagal menyimpan perubahan.\"); \n }\n }).catch(function(){ \n btn.disabled=false; \n btn.innerHTML='",
|
|
"Kelola akses penghuni dan pintu IoT,",
|
|
"/* ── MANUAL STRIPES & BORDERS ── */\n#myTable thead th { border-bottom: 1px solid #e2e8f0 !important; }\n#myTable tbody tr td { border-bottom: none !important; }\n\n.pill { display: inline-flex; align-items: center; font-weight: 600; font-size: 11px; padding: 3px 12px; border-radius: 20px; }\n.pill-green { background: #e8f5e9; color: #2e7d32; }\n.pill-red { background: #ffebee; color: #c62828; }\n.pill-amber { background: #fff8e1; color: #f57f17; }\n.pill-gray { background: #f5f5f5; color: #757575; }\n\n\n\n.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }\n.confirm-circle { width: 50px; height: 50px; border-radius: 50%; border: 2.5px solid #ef5350; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; }\n.confirm-circle span { font-size: 28px; color: #ef5350; line-height: 1; }\n.delete-ok-msg { display: none; margin-top: 12px; font-size: 13px; color: #43a047; font-weight: 500; }\n.frm-lbl { font-size: 12px; font-weight: 600; color: #555; margin-bottom: 3px; }\n.pin-warning { display: flex; align-items: flex-start; gap: 8px; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 6px; padding: 10px 12px; font-size: 12px; color: #92400e; }\n.btn-custom-edit { background: #e2e6ea; border: none; color: #212529; transition: background 0.2s ease; }\n.btn-custom-edit:hover, .btn-custom-edit:focus { background: #cbd3da; color: #212529; }",
|
|
"PIN diperlukan untuk membuka pintu secara emergency",
|
|
"Simpan';\n if (d.status === \"success\") { \n $(\"#modalTambah\").modal(\"hide\"); \n // Langsung tampilkan modal fingerprint dan proses akun id nya\n showFingerprintModal(d.id_account);\n } else { \n showCustomAlert(d.message || \"Gagal menyimpan akun.\"); \n }\n }).catch(function(){ \n btn.disabled=false; \n btn.innerHTML='",
|
|
"\"\n data-msg=\"",
|
|
"Koneksi gagal",
|
|
"';\n });\n \n document.getElementById('btnBack').addEventListener('click', function() {\n document.getElementById('step2-foto').style.display = 'none';\n document.getElementById('step1-data').style.display = 'block';\n infoAlertText.innerHTML = originalInfoText;\n });",
|
|
"Password Baru tidak cocok!",
|
|
"' + msg;\n toast.style.display = 'flex';\n }\n\n /* ── Step 1: Validate Token ──── */\n function validateToken() {\n var xhr = new XMLHttpRequest();\n xhr.open('GET', 'validate_foto_token.php?token=' + encodeURIComponent(TOKEN), true);\n xhr.onload = function() {\n try {\n var d = JSON.parse(xhr.responseText);\n if (d.valid) {\n occupantId = d.id_occupant;\n document.getElementById('nameTag').textContent = d.name;\n showPage(pgCamera);\n initCamera();\n } else {\n document.getElementById('errTitle').textContent =\n (d.message && d.message.indexOf('digunakan') !== -1) ? 'Token Sudah Digunakan' : 'Token Tidak Valid';\n document.getElementById('errMsg').textContent = d.message || 'Token tidak valid.';\n showPage(pgError);\n }\n } catch(e) {\n document.getElementById('errTitle').textContent = 'Kesalahan';\n document.getElementById('errMsg').textContent = 'Format respons tidak valid.';\n showPage(pgError);\n }\n };\n xhr.onerror = function() {\n document.getElementById('errTitle').textContent = 'Koneksi Gagal';\n document.getElementById('errMsg').textContent = 'Tidak dapat terhubung ke server. Periksa koneksi internet Anda.';\n showPage(pgError);\n };\n xhr.send();\n }\n\n /* ── Step 2: Load models + camera ─ */\n var modelLoaded = false;\n var cameraStarted = false;\n\n function initCamera() {\n // Wait for face-api.js library to be available, then start everything\n function waitAndInit() {\n if (typeof faceapi === 'undefined') {\n // face-api.js not loaded yet, wait\n setTimeout(waitAndInit, 200);\n return;\n }\n\n // Start camera immediately while models load in parallel\n var cameraReady = startCamera();\n\n // Load face detection model\n faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL).then(function() {\n modelLoaded = true;\n onReadyCheck();\n }).catch(function(e) {\n console.error('Model load error:', e);\n // Try alternate model URL as fallback\n faceapi.nets.tinyFaceDetector.loadFromUri('https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@master/weights').then(function() {\n modelLoaded = true;\n onReadyCheck();\n }).catch(function(e2) {\n console.error('Fallback model load error:', e2);\n camLoading.style.display = 'none';\n setStatus('err', 'Gagal memuat model deteksi wajah');\n });\n });\n\n cameraReady.then(function(ok) {\n cameraStarted = ok;\n if (!ok) {\n camLoading.style.display = 'none';\n setStatus('err', 'Kamera tidak tersedia');\n return;\n }\n onReadyCheck();\n });\n }\n\n // Called when either camera or model finishes - starts detection when both ready\n function onReadyCheck() {\n if (cameraStarted && modelLoaded) {\n camLoading.style.display = 'none';\n setStatus('search', 'Posisikan wajah Anda...');\n startDetection();\n startBrightnessCheck();\n setTimeout(function() {\n if (instrPanel) instrPanel.style.opacity = '0';\n setTimeout(function() { if (instrPanel) instrPanel.style.display = 'none'; }, 500);\n }, 6000);\n }\n }\n\n // Set a timeout - if everything takes too long, show error\n setTimeout(function() {\n if (!modelLoaded || !cameraStarted) {\n if (typeof faceapi === 'undefined') {\n camLoading.style.display = 'none';\n setStatus('err', 'Gagal memuat library deteksi wajah');\n } else if (!cameraStarted) {\n camLoading.style.display = 'none';\n setStatus('err', 'Kamera tidak dapat dimulai');\n }\n }\n }, 20000);\n\n waitAndInit();\n }\n\n function startCamera() {\n var isMobile = window.innerWidth",
|
|
"— ESP32 hanya mendukung WPA dan WPA2. Jika router menggunakan WPA3 atau WPA2/WPA3 mixed mode, ubah ke WPA2-Only di pengaturan router.',\n '",
|
|
"';\n };\n xhr.send(fd);\n }\n\n /* ── Events ──────────────────── */\n if (btnShutter) btnShutter.addEventListener('click', capture);\n if (btnRetake) btnRetake.addEventListener('click', retake);\n if (btnSave) btnSave.addEventListener('click', save);\n\n /* ── Init ────────────────────── */\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', validateToken);\n } else {\n validateToken();\n }\n\n /* ── PWA Service Worker ──────── */\n if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('sw_foto.js').catch(function() {});\n }\n\n /* ── Smooth instruction fade ── */\n var ip = document.getElementById('instrPanel');\n if (ip) ip.style.transition = 'opacity 0.5s ease';\n})();",
|
|
"`;\n case 'sidebar':\n case 'header':\n case 'card':\n return '';// containers render children\n default:return '';\n }\n }\n\n // ===== DRAG (MOVE) =====\n startDrag(e,comp){\n e.preventDefault();\n const startX=e.clientX,startY=e.clientY;\n const origX=comp.x,origY=comp.y;\n const move=ev=>{\n comp.x=Math.max(0,Math.round((origX+(ev.clientX-startX))/5)*5);\n comp.y=Math.max(0,Math.round((origY+(ev.clientY-startY))/5)*5);\n const el=document.querySelector(`.tft-comp[data-id=\"${comp.id}\"]`);\n if(el){el.style.left=comp.x+'px';el.style.top=comp.y+'px';}\n if(this.selectedId===comp.id)this.updatePropXY(comp);\n };\n const up=()=>{\n document.removeEventListener('mousemove',move);\n document.removeEventListener('mouseup',up);\n this.pushHistory();\n };\n document.addEventListener('mousemove',move);\n document.addEventListener('mouseup',up);\n }\n\n // ===== RESIZE =====\n startResize(e,comp){\n e.preventDefault();e.stopPropagation();\n const startX=e.clientX,startY=e.clientY;\n const origW=comp.w,origH=comp.h;\n const move=ev=>{\n comp.w=Math.max(20,Math.round((origW+(ev.clientX-startX))/5)*5);\n comp.h=Math.max(10,Math.round((origH+(ev.clientY-startY))/5)*5);\n const el=document.querySelector(`.tft-comp[data-id=\"${comp.id}\"]`);\n if(el){el.style.width=comp.w+'px';el.style.height=comp.h+'px';el.innerHTML=this.renderCompInner(comp);\n const lbl=document.createElement('div');lbl.className='tft-comp-label';lbl.textContent=comp.type;el.appendChild(lbl);\n const rh=document.createElement('div');rh.className='resize-handle';el.appendChild(rh);}\n if(this.selectedId===comp.id)this.updatePropWH(comp);\n };\n const up=()=>{\n document.removeEventListener('mousemove',move);\n document.removeEventListener('mouseup',up);\n this.pushHistory();\n this.renderCanvas();\n };\n document.addEventListener('mousemove',move);\n document.addEventListener('mouseup',up);\n }\n\n updatePropXY(c){const ex=document.getElementById('pi_x'),ey=document.getElementById('pi_y');if(ex)ex.value=c.x;if(ey)ey.value=c.y;}\n updatePropWH(c){const ew=document.getElementById('pi_w'),eh=document.getElementById('pi_h');if(ew)ew.value=c.w;if(eh)eh.value=c.h;}\n\n // ===== PROPERTY INSPECTOR =====\n renderProps(){\n const c=this.findComp(this.currentComps(),this.selectedId);\n if(!c){this.clearProps();return;}\n const isText=['button','label','datavalue','icon'].includes(c.type);\n const isContainer=CONTAINERS.includes(c.type);\n const rgb565=hexToRGB565(c.bg||'#1e2223');\n\n let h=`",
|
|
"Jaringan ditemukan",
|
|
"Memindai jaringan WiFi di sekitar ESP32",
|
|
"Ulangi Password Baru",
|
|
"Coba ganti channel WiFi router",
|
|
"// Toggle sidebar on mobile\n$('.btn-email-sidebar-toggler').on('click', function(e){\n e.preventDefault();\n $('.email-sidebar').toggleClass('show');\n});\n\n// Select All Logic\n$('#checkAll').on('change', function() {\n $('.chk-item').prop('checked', $(this).prop('checked'));\n});\n\n// Single check stop propagation\n$('.email-checkbox label, .email-checkbox input').on('click', function(e){\n // Don't trigger the email open\n e.stopPropagation();\n});\n\n// Star toggle logic\n$('.email-star').on('click', function(e) {\n e.stopPropagation();\n let id = $(this).data('id');\n let icon = $(this);\n \n $.post('history_ajax.php', { action: 'toggle_star', id: id }, function(res) {\n if(res.status === 'success') {\n if(res.starred == 1) {\n icon.removeClass('mdi-star-outline text-muted').addClass('mdi-star text-warning');\n } else {\n icon.removeClass('mdi-star text-warning').addClass('mdi-star-outline text-muted');\n }\n }\n });\n});\n\n// Open Email Detail View\nlet currentOpenId = null;\n\nfunction openDetail(data, element) {\n currentOpenId = data.id;\n \n // Update UI elements\n $('#detSubject, .detail-subject').text(data.activity);\n $('#detName').text(data.name);\n $('#detNik').text('NIK/NIP: ' + data.nik);\n $('#detTime').text(data.time_full);\n $('#detLargeFoto').attr('src', data.foto);\n $('#detAvatar').attr('src', data.occupant_foto);\n $('#detActivityText').text(data.activity);\n $('#detSign').text(data.name);\n $('#detHp').text('Tel: ' + data.hp);\n \n if (data.is_starred == 1) {\n $('#detStarIcon').removeClass('mdi-star-outline').addClass('mdi-star text-warning');\n } else {\n $('#detStarIcon').removeClass('mdi-star text-warning').addClass('mdi-star-outline');\n }\n \n // Switch views\n $('#emailListWrapper, .email-toolbar').addClass('d-none');\n $('#emailDetailWrapper').removeClass('d-none');\n \n // Remove bold if it was unread\n $(element).parent().removeClass('font-weight-bold').addClass('read');\n \n // Remove the green dot visually\n $(element).find('.unread-dot').fadeOut(200, function(){ $(this).remove(); });\n \n // Mark as read in DB via AJAX if not already\n if (data.is_read == 0) {\n $.post('history_ajax.php', { action: 'mark_read', id: data.id });\n data.is_read = 1; // Update local data\n }\n}\n\n// Back to Inbox\n$('#btnBackToInbox').on('click', function() {\n $('#emailDetailWrapper').addClass('d-none');\n $('#emailListWrapper, .email-toolbar').removeClass('d-none');\n});\n\n// Detail Delete Button\n$('#btnDetailDelete').on('click', function() {\n if(currentOpenId) {\n $.post('history_ajax.php', { action: 'delete', id: currentOpenId }, function(res){\n if(res.status === 'success') {\n window.location.reload();\n }\n });\n }\n});\n\n// Inline Hover Delete button\n$('.btn-del').on('click', function(e){\n e.stopPropagation();\n let id = $(this).data('id');\n $.post('history_ajax.php', { action: 'delete', id: id }, function(res){\n if(res.status === 'success') {\n window.location.reload();\n }\n });\n});\n\n// Inline Hover Restore button\n$('.btn-res').on('click', function(e){\n e.stopPropagation();\n let id = $(this).data('id');\n $.post('history_ajax.php', { action: 'restore', id: id }, function(res){\n if(res.status === 'success') {\n window.location.reload();\n }\n });\n});\n\n// Bulk Delete\n$('#btnDeleteAll').on('click', function() {\n let ids = [];\n $('.chk-item:checked').each(function(){ ids.push($(this).val()); });\n if(ids.length > 0) {\n $.post('history_ajax.php', { action: 'delete', ids: ids }, function(res){\n if(res.status === 'success') window.location.reload();\n });\n }\n});\n\n// Bulk Restore\n$('#btnRestoreAll').on('click', function() {\n let ids = [];\n $('.chk-item:checked').each(function(){ ids.push($(this).val()); });\n if(ids.length > 0) {\n $.post('history_ajax.php', { action: 'restore', ids: ids }, function(res){\n if(res.status === 'success') window.location.reload();\n });\n }\n});\n\n// Hard Delete All\nlet deleteIds = [];\n$('#btnDeleteAllHard').on('click', function() {\n deleteIds = [];\n $('.chk-item:checked').each(function(){ deleteIds.push($(this).val()); });\n if(deleteIds.length > 0) {\n $('#modalDeleteHistory').modal('show');\n }\n});\n\n$('#formDeleteHistory').on('submit', function(e) {\n e.preventDefault();\n if(deleteIds.length > 0) {\n $('#btnDeleteHistoryConfirm').prop('disabled', true).html('",
|
|
"ke Admin.\\n\\nGambar, stiker, dokumen, atau pesan suara akan otomatis dihapus oleh sistem.\";\n $postData = [\n 'chat_id' => $chatId,\n 'text' => $text,\n 'parse_mode' => 'HTML'\n ];\n file_get_contents($website.\"/sendMessage?\".http_build_query($postData));\n exit;\n }\n \n // Handle Teks Biasa (Occupant membalas/chat ke Admin atau kirim command khusus)\n if (isset($update[\"message\"][\"text\"])) {\n $textMessage = trim($update[\"message\"][\"text\"]);\n \n // Verifikasi bahwa user telegram ini ada telegram_id nya di sistem\n try { $pdo->exec(\"ALTER TABLE occupant ADD COLUMN telegram_id VARCHAR(100) NULL AFTER no_hp\"); } catch(PDOException $e) {}\n \n $stmt = $pdo->prepare(\"SELECT id_occupant FROM occupant WHERE telegram_id = ?\");\n $stmt->execute([$chatId]);\n $occupant = $stmt->fetch(PDO::FETCH_ASSOC);\n \n if ($occupant) {\n $id_occupant = $occupant['id_occupant'];\n \n // Dapatkan URL dasar untuk Web App\n $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? \"https\" : \"http\";\n $host = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';\n $uri = rtrim(dirname($_SERVER['PHP_SELF']), '/\\\\');\n $baseUrl = $protocol . \"://\" . $host . $uri;\n \n // --- Cek Command Khusus (Tidak masuk ke database chats admin) ---\n if ($textMessage === '/help') {\n $text = \"📚",
|
|
"Full Name wajib diisi.",
|
|
"Penting!",
|
|
"Ketik kabupaten/kota lalu pilih...",
|
|
"Simpan';\n if (d.status === \"success\") { $(\"#modalTambah\").modal(\"hide\"); document.getElementById(\"formTambahOccupant\").reset(); document.getElementById(\"previewFoto\").src = \"images/faces/user.jpg\"; document.getElementById(\"hasilCodeRegister\").value = d.code_register; $(\"#modalCodeRegister\").modal({ backdrop:'static', keyboard:false }); if (d.id_occupant) { generateQrInCodeModal(d.id_occupant); } }\n else { showCustomAlert(d.message || \"Gagal menyimpan data.\"); }\n }).catch(function(){ btn.disabled=false; btn.innerHTML='",
|
|
"DATA & TYPOGRAPHY",
|
|
"Masukkan PIN 6 digit untuk melihat password asli.",
|
|
"Pastikan router 2.4GHz aktif"
|
|
] |