// 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');
});
});
})();