347 lines
171 KiB
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 = '"
|
|
] |