// GLOBAL VARIABLES let typeChart = null; let urgencyChart = null; let lastClickedId = null; let knownPostIds = new Set(); let readPostIds = new Set(); let isLoggedIn = false; // track login state let isInitialLoadComplete = false; // flag to prevent false alerts on page load let currentUsername = ''; // stores the logged-in user's username document.addEventListener("DOMContentLoaded", function() { console.log("ALISTO Dashboard Loaded"); // 1. Check Login Status Immediately checkLoginStatus(); // 2. Init Data initCharts(); fetchPosts(); fetchStats(); setInterval(() => { fetchPosts(); fetchStats(); }, 30000); // 3. Filter Listeners document.getElementById('sort-select').addEventListener('change', () => fetchPosts()); document.getElementById('view-select').addEventListener('change', () => fetchPosts()); document.getElementById('urgency-select').addEventListener('change', () => fetchPosts()); document.getElementById('type-select').addEventListener('change', () => fetchPosts()); document.getElementById('assist-select').addEventListener('change', () => fetchPosts()); // 4. Export (Check login first) document.getElementById('export-btn').addEventListener('click', () => { if(!isLoggedIn) { alert("Responders Only. Please Log In."); return; } window.location.href = '/api/export'; }); // 5. Stats Modal const statsModal = document.getElementById('stats-modal'); document.getElementById('show-stats-btn').addEventListener('click', () => { statsModal.classList.remove('hidden'); fetchStats(); }); document.getElementById('close-stats-btn').addEventListener('click', () => statsModal.classList.add('hidden')); // 6. Login Modal Logic setupLoginLogic(); setupSearch(); setupProfileDropdown(); // 🚨 The manual button listeners were correctly removed from here in the previous step. }); // ---------------------------------------------------------------------- // ACTION BUTTON VISUAL SYNC LOGIC // ---------------------------------------------------------------------- function updateActionButtons(postStatus) { const verifyBtn = document.getElementById('verify-btn'); const resolveBtn = document.getElementById('resolve-btn'); if (!verifyBtn || !resolveBtn) return; // Safety check // 1. Reset all active states first (CRITICAL STEP) verifyBtn.classList.remove('is-verified'); resolveBtn.classList.remove('is-resolved'); // Reset button text document.querySelector('#verify-btn .btn-text').textContent = 'Verify'; document.querySelector('#resolve-btn .btn-text').textContent = 'Resolve'; // 2. Apply Active State Classes based *only* on the current status if (postStatus === 'Verified') { // If Verified, apply the 'is-verified' style verifyBtn.classList.add('is-verified'); document.querySelector('#verify-btn .btn-text').textContent = 'Verified'; } else if (postStatus === 'Resolved') { // If Resolved, apply the 'is-resolved' style resolveBtn.classList.add('is-resolved'); document.querySelector('#resolve-btn .btn-text').textContent = 'Resolved'; } } // ---------------------------------------------------------------------- // AUTHENTICATION LOGIC // ---------------------------------------------------------------------- // checks the user's current login status via API function checkLoginStatus() { fetch('/api/user_status') .then(res => res.json()) .then(data => { isLoggedIn = data.is_logged_in; currentUsername = data.username || ''; updateUIForAuth(); }); } // toggles the visibility of login links, profile dropdown, and action buttons function updateUIForAuth() { const navBtn = document.getElementById('nav-login-btn'); const profileWrap = document.getElementById('profile-container-wrap'); const dropdownUsername = document.getElementById('dropdown-username'); const actionButtonsContainer = document.querySelector('.action-buttons'); if (isLoggedIn) { // LOGGED IN: Show Profile Icon, Update Name, Hide Login Link if (navBtn) navBtn.style.display = 'none'; if (profileWrap) profileWrap.style.display = 'flex'; // Show the profile icon container if (dropdownUsername) dropdownUsername.innerText = currentUsername; if (actionButtonsContainer) actionButtonsContainer.style.visibility = 'visible'; } else { // LOGGED OUT: Show Login Link, Hide Profile Icon if (navBtn) navBtn.style.display = 'inline-block'; if (profileWrap) profileWrap.style.display = 'none'; // Hide the profile icon container if (actionButtonsContainer) actionButtonsContainer.style.visibility = 'hidden'; } } // handles click events for the new profile icon and logout button inside the dropdown function setupProfileDropdown() { const profileToggle = document.getElementById('profile-toggle'); const profileDropdown = document.getElementById('profile-dropdown'); const logoutBtn = document.getElementById('dropdown-logout-btn'); // Get the reference to the existing logout modal element const logoutModal = document.getElementById('logout-modal'); // 1. Toggle visibility when clicking the icon (Unchanged) if (profileToggle) { profileToggle.addEventListener('click', (e) => { e.stopPropagation(); profileDropdown.classList.toggle('hidden'); }); } // 2. Logout button handler (MODIFIED BLOCK) if (logoutBtn) { logoutBtn.addEventListener('click', (e) => { e.preventDefault(); // Action: Show the confirmation modal instead of redirecting if (logoutModal) { logoutModal.classList.remove('hidden'); } else { // Fallback, should not happen if index.html is correct window.location.href = '/api/logout'; } }); } // 3. Close when clicking outside (Unchanged) document.addEventListener('click', (e) => { if (profileToggle && profileDropdown && !profileToggle.contains(e.target) && !profileDropdown.contains(e.target)) { if (!profileDropdown.classList.contains('hidden')) { profileDropdown.classList.add('hidden'); } } }); } // sets up handlers for the login and logout modals function setupLoginLogic() { const loginModal = document.getElementById('login-modal'); const logoutModal = document.getElementById('logout-modal'); const navBtn = document.getElementById('nav-login-btn'); const closeBtn = document.getElementById('close-login-btn'); const submitBtn = document.getElementById('login-submit-btn'); // 1. NAV BUTTON CLICK HANDLER navBtn.addEventListener('click', (e) => { e.preventDefault(); if (isLoggedIn) { logoutModal.classList.remove('hidden'); } else { loginModal.classList.remove('hidden'); } }); // 2. LOGOUT MODAL HANDLERS document.getElementById('confirm-logout-btn').addEventListener('click', () => { window.location.href = '/api/logout'; }); document.getElementById('cancel-logout-btn').addEventListener('click', () => { logoutModal.classList.add('hidden'); }); // 3. LOGIN MODAL HANDLERS closeBtn.addEventListener('click', () => loginModal.classList.add('hidden')); submitBtn.addEventListener('click', () => { const u = document.getElementById('username').value; const p = document.getElementById('password').value; fetch('/api/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username: u, password: p}) }) .then(res => { if(res.ok) return res.json(); throw new Error('Invalid credentials'); }) .then(data => { isLoggedIn = true; updateUIForAuth(); loginModal.classList.add('hidden'); document.getElementById('username').value = ''; document.getElementById('password').value = ''; document.getElementById('login-error').innerText = ''; }) .catch(err => { document.getElementById('login-error').innerText = "Invalid Username or Password"; }); }); } // calculates and formats the time difference for 'X hrs ago' function formatRelativeTime(timestamp) { const now = new Date(); const posted = new Date(timestamp); const diffMs = now - posted; const diffMins = Math.floor(diffMs / 60000); if (isNaN(diffMins)) return "Unknown time"; if (diffMins < 1) return "Just now"; if (diffMins < 60) return diffMins + " mins ago"; if (diffMins < 1440) { const hours = Math.floor(diffMins / 60); return hours + (hours === 1 ? " hr ago" : " hrs ago"); // Use "hrs ago" } const days = Math.floor(diffMins / 1440); return days + (days === 1 ? " day ago" : " days ago"); } // ---------------------------------------------------------------------- // RENDER LOGIC // ---------------------------------------------------------------------- // renders the list of incident posts in the sidebar feed function renderSidebar(data) { const sidebar = document.getElementById('incident-feed'); if (!sidebar) return; sidebar.innerHTML = ''; const countEl = document.getElementById('dashboard-alert-count'); if (countEl) { countEl.innerText = data.length; } if (data.length === 0) { sidebar.innerHTML = `

No alerts found.

`; return; } data.forEach(post => { const box = document.createElement('div'); box.className = 'alert-box'; if (post.id === lastClickedId) box.classList.add('selected'); else if (post.status === 'New' && !readPostIds.has(post.id)) box.classList.add('unread'); const relativeTimeStr = formatRelativeTime(post.timestamp); // const timeStr = new Date(post.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); let statusColor = '#ed4801'; /*ed4801*/ if (post.status === 'Verified') statusColor = '#ffc107'; /*00C851*/ if (post.status === 'Resolved') statusColor = '#00C851'; /*33b5e5*/ let statusText = `(${post.status})`; if (post.status === 'New' && readPostIds.has(post.id)) statusText = ""; // Determine Badge Color Class let assistClass = 'assist-badge'; // Default const type = (post.assistance_type || "").toLowerCase(); if (type.includes('medical')) assistClass += ' assist-medical'; else if (type.includes('rescue')) assistClass += ' assist-rescue'; else if (type.includes('food')) assistClass += ' assist-food'; else if (type.includes('evac')) assistClass += ' assist-evac'; box.innerHTML = `
${post.disaster_type} ${statusText}
${post.assistance_type || "General"}
${post.location} • ${relativeTimeStr}
`; box.addEventListener('click', () => { const detailBox = document.getElementById('postInfo'); if (lastClickedId === post.id) { detailBox.classList.remove('active'); box.classList.remove('selected');     lastClickedId = null; renderSidebar(data); } else { document.querySelectorAll('.alert-box').forEach(el => el.classList.remove('selected')); box.classList.add('selected'); lastClickedId = post.id; readPostIds.add(post.id); updateDetailView(post); detailBox.classList.add('active'); // CRITICAL: Check auth again to show/hide buttons for this specific detail view updateUIForAuth(); renderSidebar(data); } }); sidebar.appendChild(box); }); } // updates the detail panel with the selected post's information function updateDetailView(post) { const setText = (id, text) => { const el = document.getElementById(id); if (el) el.innerText = text; }; setText('detail-title', post.title); setText('detail-location', post.location || "Unknown"); setText('detail-assistance', post.assistance_type || "General"); setText('detail-contact-name', post.author ? ( // Check if the name contains a space (suggests a full name) // If no space is found, assume it is a username and prepend 'u/' post.author.includes(' ') ? post.author : `u/${post.author}` ) : "Unknown"); setText('detail-contact-number', post.contact_number || "Check Post"); setText('detail-body', post.content); setText('detail-status', post.status); const statusBadge = document.getElementById('detail-status'); if (statusBadge) { // Clear all previous status classes statusBadge.classList.remove('status-new', 'status-verified', 'status-resolved'); // Apply the correct new class if (post.status) { statusBadge.classList.add(`status-${post.status.toLowerCase()}`); } } const link = document.getElementById('detail-link'); if (link) { const isSimulated = typeof post.reddit_id === 'string' && (post.reddit_id.startsWith('fake') || post.reddit_id.startsWith('sim')); link.href = isSimulated ? '#' : `https://reddit.com/comments/${post.reddit_id.replace('t3_', '')}`; } const urgEl = document.getElementById('detail-urgency'); if (urgEl) { urgEl.innerText = post.urgency_level; urgEl.style.color = post.urgency_level === 'High' ? '#ff4444' : '#00C851'; } const timeEl = document.getElementById('detail-time'); if (timeEl) { // 1. Calculate relative and exact time strings const posted = new Date(post.timestamp); const relativeTime = formatRelativeTime(post.timestamp); // e.g., "2 hrs ago" // 2. Format the exact time (e.g., 10:34 PM) const exactTimeStr = posted.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }); // 3. Generate multi-line HTML structure with inline CSS timeEl.innerHTML = `
${exactTimeStr}
${relativeTime}
`; // The redundant if/else logic block for combining text is now removed. } // Synchronize buttons with the loaded post status updateActionButtons(post.status); } // ---------------------------------------------------------------------- // STANDARD FUNCTIONS (FINAL WORKING VERSION) // ---------------------------------------------------------------------- // handles the logic for updating the post status (Verify/Resolve) function updateStatus(intendedStatus) { if (!lastClickedId) return; const badge = document.getElementById('detail-status'); // FIX: Read status and clean it by converting to lowercase and trimming whitespace const currentStatus = badge ? badge.innerText.trim() : 'New'; let finalStatus; if (intendedStatus === 'Verified') { // ... (rest of the logic) // Check against the current, clean status if (currentStatus.toLowerCase() === 'verified') { finalStatus = 'New'; } else { finalStatus = 'Verified'; } } else if (intendedStatus === 'Resolved') { // ... (rest of the logic) // Check against the current, clean status if (currentStatus.toLowerCase() === 'resolved') { finalStatus = 'New'; } else { finalStatus = 'Resolved'; } } else { return; } // Safety check: ensure we are actually changing the status if (finalStatus === currentStatus) { return; } // --- API CALL AND UI UPDATE --- fetch(`/api/posts/${lastClickedId}/status`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: finalStatus }) }) .then(res => { if(res.status === 401) { alert("Unauthorized. Please log in."); return; } // If the API call returns success (200), proceed with UI updates based on the finalStatus. return res.json(); }) .then(data => { if(data) { fetchPosts(); fetchStats(); if (badge) { // 1. Update text badge.innerText = finalStatus; // 2. Apply color class (visual sync fix) badge.classList.remove('status-new', 'status-verified', 'status-resolved'); badge.classList.add(`status-${finalStatus.toLowerCase()}`); } // 3. Update buttons' appearance updateActionButtons(finalStatus); } }) .catch(error => { console.error("Status Update Failed:", error); alert("Failed to update status due to network error."); }); } // pop up notificaiton for new alerts function showNotification(message) { const popup = document.getElementById('new-alert-notification'); const msgEl = document.getElementById('notification-message'); if (!popup || !msgEl) return; msgEl.innerText = message; // 1. Prepare for animation (ensure it's visible but off-screen) popup.classList.remove('hidden'); // 2. Force reflow to ensure CSS animation starts correctly void popup.offsetWidth; // 3. Trigger slide-in animation popup.classList.add('visible'); // 4. Set timeout to slide out after 6 seconds setTimeout(() => { popup.classList.remove('visible'); // 5. Hide completely after animation finishes (0.5s transition time in CSS) setTimeout(() => { popup.classList.add('hidden'); }, 500); }, 6000); // Display for 6 seconds } // initializes and draws the chart.js graphs for stats modal function initCharts() { const ctxType = document.getElementById('typeChart'); const ctxUrg = document.getElementById('urgencyChart'); if (!ctxType || !ctxUrg) return; typeChart = new Chart(ctxType.getContext('2d'), { type: 'doughnut', data: { labels: [], datasets: [{ data: [], backgroundColor: ['#ed4801', '#33b5e5', '#00C851', '#ffbb33', '#aa66cc'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: 'white' } } } } }); urgencyChart = new Chart(ctxUrg.getContext('2d'), { type: 'bar', data: { labels: ['High', 'Low/Med'], datasets: [{ label: 'Count', data: [0, 0], backgroundColor: ['#ff4444', '#00C851'], borderRadius: 5 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: 'white' } }, x: { ticks: { color: 'white' } } }, plugins: { legend: { display: false } } } }); } // fetches statistical data from the API function fetchStats() { fetch('/api/stats').then(res=>res.json()).then(data=>updateCharts(data)).catch(err=>console.error(err)); } // updates the chart data and redraws the graphs function updateCharts(data) { if(!typeChart || !urgencyChart) return; const types = data.disaster_types || {}; typeChart.data.labels = Object.keys(types); typeChart.data.datasets[0].data = Object.values(types); typeChart.update(); const levels = data.urgency_levels || {}; urgencyChart.data.datasets[0].data = [levels['High']||0, (levels['Low']||0)+(levels['Medium']||0)]; urgencyChart.update(); } // fetches post data from the API based on current filter selections function fetchPosts(q='') { const sort = document.getElementById('sort-select')?.value||'newest'; const view = document.getElementById('view-select')?.value||'active'; const urgency = document.getElementById('urgency-select')?.value||'all'; const type = document.getElementById('type-select')?.value||'all'; const assist = document.getElementById('assist-select')?.value||'all'; const searchVal = document.querySelector('.search-input')?.value||''; let url = `/api/posts?sort=${sort}&view=${view}&urgency=${urgency}&type=${type}&assist=${assist}`; if(searchVal) url += `&query=${encodeURIComponent(searchVal)}`; url += `&_=${new Date().getTime()}`; fetch(url).then(r=>r.json()).then(d=>{renderSidebar(d); checkAudioAlert(d);}).catch(e=>console.error(e)); } // checks for new high-urgency alerts and plays sound + shows notification function checkAudioAlert(p){ const a=document.getElementById('alert-sound'); if(!a)return; a.volume = 0.1; let newAlertFound = false; if (!isInitialLoadComplete) { // 1. On the very first load after page navigation/refresh: // Populate the known set with ALL current IDs and skip the alert. p.forEach(x => knownPostIds.add(x.id)); isInitialLoadComplete = true; return; } p.forEach(x => { if(x.urgency_level === 'High' && !knownPostIds.has(x.id) && x.status !== 'Resolved') { newAlertFound = true; } }); if(newAlertFound) { a.play().catch(e=>{}); showNotification("NEW HIGH PRIORITY ALERT: Check Feed"); } p.forEach(x => { knownPostIds.add(x.id); }); } // sets up the search input logic with clear button function setupSearch(){ const i=document.querySelector('.search-input'), c=document.querySelector('.clear-icon'); if(!i)return; i.addEventListener('input',()=>{if(i.value)c?.classList.remove('hidden');else{c?.classList.add('hidden');fetchPosts();}}); i.addEventListener('keydown',e=>{if(e.key==='Enter')fetchPosts();}); c?.addEventListener('click',()=>{i.value='';c.classList.add('hidden');fetchPosts();}); } // Attach listeners once DOM is ready (function () { // Select all elements with data_tooltip attribute const tooltipElements = document.querySelectorAll('[data_tooltip]'); tooltipElements.forEach(el => { // When clicked: hide tooltip immediately by adding class el.addEventListener('mousedown', (e) => { // Add class so CSS hides tooltip; use mousedown for immediate feedback el.classList.add('tooltip-hidden'); // Also remove focus so :focus doesn't keep hiding/showing unpredictably if (typeof el.blur === 'function') { el.blur(); } }); // When mouse leaves the element: remove the hiding class so future hovers work el.addEventListener('mouseleave', (e) => { el.classList.remove('tooltip-hidden'); }); // Also remove hidden class on touchend for touch devices (optional) el.addEventListener('touchend', () => { el.classList.remove('tooltip-hidden'); }); }); })();