312 lines
13 KiB
JavaScript
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
|