// In frontend/js/station_selection.js // const API_BASE = 'http://10.10.1.169:5000/api'; // --- CONFIGURATION --- const SOCKET_URL = window.location.origin; // Connects to the server that served the page const API_BASE = "/api"; // Relative path for API calls const grid = document.getElementById('stations-grid'); const addStationCardTmpl = document.getElementById('add-station-card-template'); const stationCardTmpl = document.getElementById('station-card-template'); const searchEl = document.getElementById('search'); const emptyState = document.getElementById('empty-state'); const errorState = document.getElementById('error-state'); // THEMED STATUS DROPDOWN LOGIC const statusBtn = document.getElementById('statusBtn'); const statusMenu = document.getElementById('statusMenu'); const statusLabel = document.getElementById('statusLabel'); let statusValue = 'all'; // Modals const userModal = document.getElementById('userModal'); const stationModal = document.getElementById('stationModal'); const openModal = (el) => { el.classList.remove('hidden'); el.classList.add('block'); }; const closeModal = (el) => { el.classList.add('hidden'); el.classList.remove('block'); }; // Header buttons document.getElementById('addUserBtn').onclick = () => openModal(userModal); document.getElementById('cancelUserBtn').onclick = () => closeModal(userModal); document.getElementById('logoutBtn').onclick = () => { localStorage.clear(); window.location.href = './index.html'; }; document.getElementById('cancelStationBtn').onclick = () => closeModal(stationModal); // Forms document.getElementById('userForm').addEventListener('submit', async (e)=>{ e.preventDefault(); const payload = { username: newUsername.value.trim(), password: newPassword.value, is_admin: isAdmin.checked }; try { const res = await fetch(`${API_BASE}/users`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload), credentials: 'include' }); if(!res.ok) throw new Error('Failed to add user'); closeModal(userModal); alert('User added'); } catch(err){ alert(err.message); } }); document.getElementById('stationForm').addEventListener('submit', async (e)=>{ e.preventDefault(); const payload = { station_id: stationId.value.trim(), product_id: stationProductId.value.trim(), name: stationName.value.trim(), location: stationLocation.value.trim(), mqtt_broker: mqttBroker.value.trim(), mqtt_port: Number(mqttPort.value), mqtt_user: mqttUsername.value || null, mqtt_password: mqttPassword.value || null, }; try { const res = await fetch(`${API_BASE}/stations`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload), credentials: 'include' }); if(!res.ok) throw new Error('Failed to add station'); closeModal(stationModal); await loadStations(); } catch(err){ alert(err.message); } }); function statusStyles(status){ const online = { dot:'bg-emerald-400 animate-pulseDot', badge:'bg-emerald-500/15 text-emerald-300 border border-emerald-400/20', text:'Online' }; const offline = { dot:'bg-rose-500', badge:'bg-rose-500/15 text-rose-300 border border-rose-400/20', text:'Offline' }; return String(status).toLowerCase()==='online'?online:offline; } function setStatus(val, label) { statusValue = val; statusLabel.textContent = label; statusMenu.classList.add('hidden'); applyFilters(); // reuse your existing function } let allStations = []; function render(stations){ grid.innerHTML = ''; if(!stations || stations.length===0){ emptyState.classList.remove('hidden'); } else { emptyState.classList.add('hidden'); for(const s of stations){ const node = stationCardTmpl.content.cloneNode(true); const card = node.querySelector('div'); card.dataset.stationId = s.id || s.station_id; card.querySelector('.station-name').textContent = s.name ?? `Station ${s.id || s.station_id}`; // const productIdVal = s.product_id || '—'; // const productIdEl = card.querySelector('.product-id'); // if (productIdEl) { // // Use .innerHTML and add a styled for the title // productIdEl.innerHTML = `Product ID: ${productIdVal}`; // } const productIdVal = s.product_id || '—'; const productIdEl = card.querySelector('.product-id'); if (productIdEl) { productIdEl.textContent = productIdVal; } card.querySelector('.station-location').textContent = s.location ?? '—'; const idVal = s.id || s.station_id || '—'; const idEl = card.querySelector('.station-id'); idEl.textContent = idVal; idEl.setAttribute('title', idVal); const styles = statusStyles(s.status); const dot = card.querySelector('.status-dot'); dot.className = `status-dot h-2.5 w-2.5 rounded-full ${styles.dot}`; const badge = card.querySelector('.status-badge'); badge.className = `status-badge rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${styles.badge}`; badge.textContent = styles.text; // Metrics const starts = s.total_swaps_started ?? s.metrics?.total_starts ?? 0; const success = s.total_swaps_success ?? s.metrics?.total_completed ?? 0; const aborted = s.total_swaps_aborted ?? s.metrics?.total_aborted ?? 0; card.querySelector('.metric-starts').textContent = starts; card.querySelector('.metric-success').textContent = success; card.querySelector('.metric-aborted').textContent = aborted; // Open card.querySelector('.open-btn').addEventListener('click', () => { localStorage.setItem('selected_station', JSON.stringify(s)); const id = encodeURIComponent(s.id || s.station_id); window.location.href = `./dashboard.html?stationId=${id}`; }); // --- ADD THIS NEW BLOCK FOR THE REMOVE BUTTON --- card.querySelector('.remove-btn').addEventListener('click', async () => { const stationId = s.id || s.station_id; const stationName = s.name; // 1. Confirm with the user if (!confirm(`Are you sure you want to permanently remove "${stationName}"?`)) { return; } try { // 2. Call the DELETE API endpoint const response = await fetch(`${API_BASE}/stations/${stationId}`, { method: 'DELETE', }); if (response.ok) { alert(`Station "${stationName}" removed successfully.`); // 3. Refresh the entire list from the server loadStations(); } else { const error = await response.json(); alert(`Failed to remove station: ${error.message}`); } } catch (error) { console.error('Error removing station:', error); alert('An error occurred while trying to remove the station.'); } }); grid.appendChild(node); } } // Finally, append the Add Station card LAST const addNode = addStationCardTmpl.content.cloneNode(true); const addCard = addNode.querySelector('div'); addCard.addEventListener('click', () => openModal(stationModal)); grid.appendChild(addNode); if (window.lucide) { lucide.createIcons(); } } statusBtn.addEventListener('click', () => { statusMenu.classList.toggle('hidden'); }); statusMenu.querySelectorAll('button').forEach(b=>{ b.addEventListener('click', () => setStatus(b.dataset.value, b.textContent.trim())); }); function applyFilters(){ const q = (searchEl.value||'').trim().toLowerCase(); const status = statusValue; // 'all' | 'online' | 'offline' const filtered = allStations.filter(s=>{ const matchesQ = !q || [s.name, s.id, s.station_id, s.location].filter(Boolean).some(v=>String(v).toLowerCase().includes(q)); const matchesStatus = status==='all' || String(s.status).toLowerCase()===status; return matchesQ && matchesStatus; }); render(filtered); } searchEl.addEventListener('input', ()=> setTimeout(applyFilters,150)); async function loadStations() { try { // Step 1: Fetch both the station list and the daily stats at the same time. const [stationsResponse, statsResponse] = await Promise.all([ fetch(`${API_BASE}/stations`), fetch(`${API_BASE}/stations/daily-stats`) ]); if (!stationsResponse.ok) { throw new Error('Failed to fetch station list'); } const stationsList = await stationsResponse.json(); // It's okay if stats fail; we can just show 0. const statsDict = statsResponse.ok ? await statsResponse.json() : {}; // Step 2: Merge the stats into the station list. const mergedStations = stationsList.map(station => { const stats = statsDict[station.id] || { total_starts: 0, completed: 0, aborted: 0 }; return { ...station, // Keep all original station properties // Add the stats properties that the render() function expects metrics: { total_starts: stats.total_starts, total_completed: stats.completed, total_aborted: stats.aborted } }; }); allStations = Array.isArray(mergedStations) ? mergedStations : []; // Hide error message if successful errorState.classList.add('hidden'); // Step 3: Render the page with the combined data. applyFilters(); } catch (err) { console.error("Error loading stations:", err); allStations = []; // Clear any old data applyFilters(); // Render the empty state errorState.textContent = 'Failed to load stations. Please ensure the API is running and reachable.'; errorState.classList.remove('hidden'); } } document.addEventListener('click', (e)=>{ if (!document.getElementById('statusFilterWrap').contains(e.target)) statusMenu.classList.add('hidden'); }); async function refreshData() { try { console.log("Refreshing data..."); // For debugging const [stationsResponse, statsResponse] = await Promise.all([ fetch(`${API_BASE}/stations`), fetch(`${API_BASE}/stations/daily-stats`) ]); if (!stationsResponse.ok) return; // Fail silently on refresh const stationsList = await stationsResponse.json(); const statsDict = statsResponse.ok ? await statsResponse.json() : {}; const mergedStations = stationsList.map(station => { const stats = statsDict[station.id] || { total_starts: 0, completed: 0, aborted: 0 }; return { ...station, metrics: { total_starts: stats.total_starts, total_completed: stats.completed, total_aborted: stats.aborted } }; }); // If a station has been added or removed, do a full reload to redraw the grid if (mergedStations.length !== allStations.length) { loadStations(); return; } // Update the master list allStations = mergedStations; // Update each card in the DOM without rebuilding it allStations.forEach(s => { const card = grid.querySelector(`[data-station-id="${s.id}"]`); if (card) { const styles = statusStyles(s.status); card.querySelector('.status-dot').className = `status-dot h-2.5 w-2.5 rounded-full ${styles.dot}`; const badge = card.querySelector('.status-badge'); badge.className = `status-badge rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${styles.badge}`; badge.textContent = styles.text; card.querySelector('.metric-starts').textContent = s.metrics.total_starts; card.querySelector('.metric-success').textContent = s.metrics.total_completed; card.querySelector('.metric-aborted').textContent = s.metrics.total_aborted; } }); } catch (err) { console.error("Auto-refresh failed:", err); } } document.addEventListener('DOMContentLoaded', loadStations); setInterval(refreshData, 15000); // 15000 milliseconds = 15 seconds