TKK_E32230273/extract_12_files.json

347 lines
171 KiB
JSON

[
"\"\n data-danger=\"",
"\"\n data-detail=\"",
"\"\n data-icnclass=\"",
"\"\n data-msg=\"",
"\"\n data-time=\"",
"\"\n data-title=\"",
"\"\n data-unread=\"",
"\"\n data-access=\"",
"\"\n data-code=\"",
"\"\n data-fingerid=\"",
"\"\n data-fingerok=\"",
"\"\n data-foto=\"",
"\"\n data-hasfoto=\"",
"\"\n data-name=\"",
"\"\n data-nik=\"",
"\"\n data-no_hp=\"",
"\"\n data-progress=\"",
"\"\n data-status=\"",
"\" alt=\"foto\" class=\"ml-2\" style=\"width: 24px; height: 24px; object-fit: cover; border-radius: 4px; border: 1px solid #ddd;\">",
"\" data-id=\"",
"\" 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\">",
"' + (d.message || 'Gagal generate QR') + '",
"' + (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 = '",
"' + 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('",
"') {\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});",
"').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 = '",
"').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('",
"').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 = '",
"').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 = '",
"');\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('",
"');\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('",
"');\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('",
"');\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('",
"');\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",
"');\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('",
"');\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('",
"');\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 = '",
"');\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('",
"'+jam+'  · ",
"';\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",
"';\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 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 }\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(/",
"';\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}",
"';\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'",
"';\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 $('#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('",
"(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 '",
")\"\n data-id=\"",
"*Minimal 8 karakter, huruf besar, huruf kecil, angka & simbol jika ingin mengubah.",
"*Minimal 8 karakter, huruf besar, huruf kecil, angka & simbol.",
"/* ========================================================= */\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; }",
"/* 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}",
"/* ── 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 }",
"/* ── 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; } }",
"/* ── 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; } }",
"/* ── 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; }",
"/* ── 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; }",
"/* ── 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); } }",
"// 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('",
"// Tambah\ndocument.getElementById(\"formTambahOccupant\").addEventListener(\"submit\", function(e) {\n e.preventDefault();\n var btn = document.getElementById(\"btnSubmitOccupant\"); btn.disabled = true; btn.innerHTML = '",
"// 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('",
"15 detik namun tidak mendapat respons",
"6 digit PIN",
"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",
"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 = '",
"Aktivitas sistem dan peringatan keamanan",
"Autentikasi biometrik diperlukan untuk membuka akses pintu IoT.",
"Bagikan QR code ini ke",
"Batal",
"Belum Dibaca",
"Belum ada kegiatan",
"Belum diisi",
"Berhasil",
"Berhasil Terhubung!",
"Berhasil dihapus.",
"Berhasil!",
"Cek profil",
"Coba Lagi",
"Coba dari perangkat lain",
"Coba ganti channel WiFi router",
"Confirm Password",
"Confirm Password Baru",
"Daftar Notifikasi Terbaru",
"Daftarkan akun baru,",
"Daftarkan pengguna baru untuk ruangan yang tersedia,",
"Data Access Occupant",
"Data Account",
"Data Berhasil Disimpan!",
"Data Berhasil diperbarui!",
"Data Occupant",
"Dekatkan ESP32 dengan router",
"Dekatkan ESP32 ke router WiFi target",
"Delete Data",
"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});",
"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 });",
"Door Status",
"Download Foto",
"ESP32 berhasil menemukan jaringan WiFi",
"ESP32 gagal terhubung ke jaringan",
"ESP32 juga gagal kembali ke WiFi sebelumnya. Saat ini ESP32 memancarkan hotspot konfigurasi. Hubungkan ke WiFi",
"ESP32 mencoba menghubungkan ke jaringan",
"ESP32 mendeteksi jaringan",
"ESP32 sudah berhasil kembali terhubung ke jaringan WiFi yang sebelumnya pernah terhubung. Sistem kembali online seperti semula.",
"Edit",
"Edit Account",
"Edit Address",
"Edit Foto",
"Edit Occupant",
"Edit Personal Information",
"Edit Social Media",
"Email",
"Email address",
"Enter password",
"Facebook belum diisi",
"Factory reset jika berulang",
"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",
"Format email valid' : '",
"Foto Belum Diverifikasi",
"Foto Profil",
"Foto Profil (Opsional)",
"Foto diambil via QR code setelah data tersimpan",
"Foto yang diupload tidak memiliki wajah terdeteksi. Gunakan foto dengan wajah yang jelas.",
"Gagal",
"Gagal Terhubung!",
"Gagal memeriksa username.",
"Gagal memicu pemindaian.",
"Gagal memindai jaringan (Timeout). Pastikan ESP32 menyala.",
"Gagal memuat QR",
"Gagal terkirim",
"Ganti Jaringan WiFi ESP32:",
"Gunakan menu ini untuk Memindahkan ESP32 ke koneksi WiFi lain (Misal dari WiFi A ke WiFi B). Tekan tombol",
"Gunakan sensor biometrik perangkat Anda saat ini.",
"Hapus",
"Hapus Account?",
"Hapus Occupant?",
"Hapus Permanen",
"Hapus Permanen?",
"Hari Ini",
"Harus 10-15 digit';\n fb.className = (v.length >= 10 && v.length",
"Informasi pribadi dan kontak Anda,",
"Input ulang password dengan benar",
"Instagram belum diisi",
"Jadwal kegiatan dan agenda mendatang,",
"Jangan bagikan password ini kepada siapapun!",
"Jaringan ditemukan",
"Kelola akses penghuni dan pintu IoT,",
"Kelola data akun operasional,",
"Kelola data penghuni yang sudah disetujui,",
"Kembali",
"Kesalahan koneksi ke server.",
"Ketik desa lalu pilih...",
"Ketik kabupaten/kota lalu pilih...",
"Ketik kecamatan lalu pilih...",
"Ketik provinsi lalu pilih...",
"Klik foto di atas untuk mengubah via QR Code",
"Klik ikon di atas untuk generate QR Code",
"Koneksi Dipulihkan:",
"Koneksi gagal",
"Konfirmasi password terbaru",
"Kosongkan jika tidak ingin mengubah foto.",
"Kosongkan jika tidak ubah",
"Lengkapi profil sekarang!",
"Lihat",
"Lihat semua jadwal disini !",
"LinkedIn belum diisi",
"Masukkan PIN 6 digit untuk melihat password asli.",
"Masukkan PIN 6 digit untuk verifikasi akses.",
"Memindai jaringan WiFi di sekitar ESP32",
"Memuat data...",
"Memverifikasi Data...",
"Memverifikasi...';\n btn.disabled = true;\n \n $.post(\"verify_pin_account.php\", { id_account: id, pin: pin }, function(d) {\n btn.innerHTML = '",
"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 = '",
"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 = '",
"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' ? '",
"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});",
"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 !== '",
"Menghubungkan ke jaringan baru...",
"Menghubungkan ke server.",
"Mengirim konfigurasi ke ESP32...",
"Menunggu Sensor...';\n statusEl.style.display = \"block\";\n \n if (!window.PublicKeyCredential) {\n statusEl.style.display = \"none\";\n errorEl.innerHTML = '",
"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 = '",
"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 = '",
"Menyimpan pengaturan ESP32...",
"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 = '",
"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 = '",
"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 = '",
"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 = '",
"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 = '",
"Modal akan tertutup otomatis",
"Monitor dan kelola sistem IoT Smart Door Anda dari sini.",
"NIK atau NIP",
"Nama",
"Nama Kegiatan",
"Nama Lengkap",
"Nama lengkap",
"Nomor valid' : '",
"Notifikasi di menu sampah akan otomatis terhapus permanen setelah 30 hari.",
"Opsi Perangkat Lain",
"PIN",
"PIN (6 Digit)",
"PIN digunakan untuk melihat password asli. Pastikan Anda",
"PIN diperlukan untuk melihat password di halaman detail.",
"PIN diperlukan untuk melihat password di halaman operations.",
"PIN diperlukan untuk membuka pintu secara emergency",
"Password",
"Password Asli",
"Password Baru (Opsional)",
"Password Baru tidak cocok!",
"Password baru belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!",
"Password belum memenuhi kriteria (min. 8 karakter, huruf besar, kecil, angka, dan simbol)!",
"Password dan Confirm Password tidak sama!",
"Password salah atau sinyal lemah.",
"Pastikan mengunci pintu kembali jika Anda sudah selesai.",
"Pastikan router 2.4GHz aktif",
"Pastikan router WiFi target menyala",
"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",
"Pendaftaran Gagal",
"Penting!",
"Periksa MAC filtering pada router",
"Periksa ejaan nama SSID",
"Periksa firewall dan isolasi klien",
"Peringatan",
"Pilih",
"Pilih Bulan",
"Pilih desa untuk memuat kode pos...",
"Pilih minimal satu data untuk dihapus!",
"Pindah ke Sampah",
"Preview Foto",
"QR Code Registrasi Foto",
"QR Registrasi Foto",
"QR hanya bisa digunakan",
"Real-time door status",
"Sampah (30 Hari)",
"Scan QR Code di bawah untuk mendaftar sidik jari via smartphone Anda.",
"Scan QR Code di bawah untuk mendaftar via smartphone.",
"Scan di Perangkat Ini",
"Scan ulang jaringan",
"Selesai",
"Semua Notifikasi",
"Sentuh sensor pada perangkat.",
"Siap memindai sidik jari Anda...",
"Sidik jari berhasil ditambahkan.",
"Silakan ikuti langkah di bawah ini untuk memperbarui foto. Jika",
"Silakan klik 'X' untuk menutup jendela.",
"Silakan klik button",
"Silakan klik foto di bawah ini untuk memperbarui foto. Jika",
"Silakan mendaftarkan sidik jari untuk akun ini.",
"Simpan",
"Simpan kode dibawah ini untuk melakukan registrasi di Finger Print.",
"Simpan kode ini.",
"Simpan';\n alert('Koneksi gagal.');\n });\n });\n}\n\nsubmitForm('formEditSocial', 'btnSaveSocial', 'modalEditSocial');\nsubmitForm('formEditPersonal', 'btnSavePersonal', 'modalEditPersonal');\nsubmitForm('formEditAddress', 'btnSaveAddress', 'modalEditAddress');",
"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='",
"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='",
"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 = '",
"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='",
"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='",
"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='",
"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('",
"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('",
"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 = '",
"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}",
"Status",
"Status Pintu",
"Status pintu IoT dan waktu akses terakhir.",
"Tambah",
"Tambah Account",
"Tambah Occupant",
"Tandai Semua Dibaca",
"Tekan untuk menyimpan perubahan",
"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 = '",
"Tidak ada jaringan WiFi terdeteksi.",
"Tidak ada kegiatan di tanggal ini",
"Tidak ada notifikasi.",
"Tidak ada obrolan aktif.",
"Tidak ditemukan",
"Tulis moto atau bio singkat...",
"Tutup",
"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 });",
"Ulangi Password",
"Ulangi Password Baru",
"Upload Foto",
"Upload foto profil dengan wajah yang terdeteksi terlebih dahulu.",
"Username ini sudah digunakan.",
"Verifikasi PIN",
"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 = '",
"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('",
"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('",
"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 = '",
"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 = '",
"WhatsApp belum diisi",
"Yakin menghapus item yang dipilih secara permanen?",
"`).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",
"`;\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 = '",
"`;\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});",
"alasan yang tidak dapat diidentifikasi secara spesifik",
"dari router. Koneksi tidak berhasil dibuat dalam batas waktu yang ditentukan.';\n suggestions = [\n '",
"dengan password yang dikonfigurasi pada access point/router tersebut.' ;\n suggestions = [\n '",
"di bawah ini untuk membuka Pintu Ruang FTM.",
"di browser untuk konfigurasi manual.",
"di sebelah nama SSID untuk mencari WiFi di sekitar ESP32.",
"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",
"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",
"email-star\" data-id=\"",
"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",
"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 = '",
"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",
"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)",
"gagal menemukan jaringan WiFi dengan nama \"' + targetSsid + '\"",
"ingin memperbarui foto, klik",
"karena",
"kekuatan sinyal terlalu lemah",
"lalu buka",
"mengingat PIN",
"menolak koneksi karena password yang dikirim tidak sesuai",
"name@email.com",
"oleh firmware mikrokontroler.' ;\n suggestions = [\n '",
"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 = '",
"saat melakukan scanning frekuensi 2.4GHz. Router mungkin sedang mati, berada di luar jangkauan, atau SSID dalam mode tersembunyi (hidden).' ;\n suggestions = [\n '",
"showCustomAlert(\"Pendaftaran Gagal\", \"",
"tidak",
"yang dimasukkan.",
"— ESP32 hanya mendukung WPA dan WPA2. Jika router menggunakan WPA3 atau WPA2/WPA3 mixed mode, ubah ke WPA2-Only di pengaturan router.',\n '",
"— 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 = '",
"— beberapa router membatasi perangkat yang boleh terhubung berdasarkan MAC address. Tambahkan MAC address ESP32 ke whitelist jika fitur ini aktif.',\n '",
"— 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 = '",
"— 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 '",
"— interferensi dari WiFi tetangga pada channel yang sama dapat mengganggu koneksi. Ubah ke channel 1, 6, atau 11 yang paling sedikit interferensinya.',\n '",
"— jarak terlalu jauh atau terhalang dinding tebal dapat menyebabkan sinyal tidak terdeteksi. Idealnya dalam jarak",
"— 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 = '",
"— jika ESP32 harus ditempatkan jauh dari router, pasang repeater WiFi di titik tengah untuk memperkuat jangkauan sinyal.',\n '",
"— jika SSID disembunyikan (hidden network) di pengaturan router, aktifkan broadcast SSID terlebih dahulu agar ESP32 dapat menemukannya.',\n '",
"— 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 = '",
"— jika password WiFi baru saja diperbahui oleh admin jaringan, minta password yang benar dan masukkan kembali.',\n '",
"— jika router sudah mencapai batas maksimal perangkat yang terhubung (umumnya 32 device), putuskan beberapa perangkat yang tidak digunakan.',\n '",
"— nama WiFi harus diketik persis sama termasuk huruf besar/kecil. Contoh: \"WiFi_Rumah\" ≠ \"wifi_rumah\" ≠ \"WiFi Rumah\".',\n '",
"— pastikan antena router menghadap vertikal (tegak). Untuk jangkauan multi-lantai, miringkan satu antena horizontal dan satu vertikal.',\n '",
"— pastikan router tidak dalam mode maintenance, firmware tidak sedang update, dan tidak ada konfigurasi firewall yang memblokir perangkat baru.',\n '",
"— pastikan tidak ada kesalahan ketik, perhatikan huruf besar/kecil (case-sensitive), spasi, dan karakter khusus seperti @, #, !, dsb.',\n '",
"— periksa apakah lampu indikator WiFi pada router berkedip/aktif. Jika router mati, nyalakan terlebih dahulu.',\n '",
"— pindahkan ESP32 agar berada dalam jarak 5-10 meter dari router tanpa penghalang besar. Sinyal ideal adalah di atas -65 dBm.',\n '",
"— router yang sudah berjalan lama mungkin mengalami memory leak atau channel congestion. Restart bisa menyelesaikan masalah ini.',\n '",
"— tekan tombol Scan dan pilih jaringan WiFi target kembali, pastikan jaringan masih aktif dan terdeteksi.',\n '",
"— tekan tombol reset atau cabut dan pasang kembali power ESP32, lalu coba hubungkan ulang.',\n '",
"— 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 = '"
]