SwapStation_WebApp/frontend/js/station_selection.js

312 lines
13 KiB
JavaScript

// 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 <span> for the title
// productIdEl.innerHTML = `<span class="font-semibold text-white-500">Product ID: </span>${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