/* ========================================================================== Siro Admin Web Portal - Interactive Application Engine ========================================================================== */ document.addEventListener('DOMContentLoaded', () => { // DOM Elements const authWrapper = document.getElementById('authWrapper'); const loginForm = document.getElementById('loginForm'); const navItems = document.querySelectorAll('.nav-item[data-view]'); const pageViews = document.querySelectorAll('.page-view'); const logoutBtn = document.getElementById('logoutBtn'); const sidebar = document.getElementById('sidebar'); const toggleSidebarBtn = document.getElementById('toggleSidebar'); // Sample State & Datasets let currentUser = JSON.parse(localStorage.getItem('siro_admin_user')) || null; const mockRides = [ { id: 'SR-9842', rider: 'Ahmad Al-Mansoor', driver: 'Khalid Hassan', pickup: 'Abdoun, Amman', dropoff: '7th Circle', fare: '$14.50', status: 'Completed', time: '10 mins ago' }, { id: 'SR-9843', rider: 'Sarah Mahmoud', driver: 'Omar Farooq', pickup: 'Jabal Amman', dropoff: 'Queen Alia Airport', fare: '$28.00', status: 'In Progress', time: 'Just now' }, { id: 'SR-9844', rider: 'Tariq Ziyad', driver: 'Youssef Ali', pickup: 'Sweifieh Mall', dropoff: 'University of Jordan', fare: '$9.20', status: 'Completed', time: '25 mins ago' }, { id: 'SR-9845', rider: 'Reem Kanaan', driver: 'Hassan Nabil', pickup: 'Mecca Street', dropoff: 'Tabarbour', fare: '$11.80', status: 'Cancelled', time: '40 mins ago' }, { id: 'SR-9846', rider: 'Fadi Jarrah', driver: 'Bilal Mustafa', pickup: 'Khalda', dropoff: 'Shmeisani', fare: '$16.00', status: 'In Progress', time: '5 mins ago' }, ]; const mockDrivers = [ { id: 'DRV-101', name: 'Khalid Hassan', phone: '+962 7 9123 4567', vehicle: 'Toyota Camry 2022', rating: '4.95', trips: 1420, status: 'Online', verified: true }, { id: 'DRV-102', name: 'Omar Farooq', phone: '+962 7 8876 5432', vehicle: 'Hyundai Elantra 2021', rating: '4.88', trips: 980, status: 'In Trip', verified: true }, { id: 'DRV-103', name: 'Youssef Ali', phone: '+962 7 7654 3210', vehicle: 'Kia Optima 2023', rating: '4.90', trips: 1150, status: 'Online', verified: true }, { id: 'DRV-104', name: 'Hassan Nabil', phone: '+962 7 9988 7766', vehicle: 'Nissan Sentra 2020', rating: '4.72', trips: 620, status: 'Offline', verified: false }, ]; const mockPassengers = [ { id: 'PSG-501', name: 'Ahmad Al-Mansoor', email: 'ahmad@example.com', trips: 45, rating: '4.9', joined: 'Jan 2025', status: 'Active' }, { id: 'PSG-502', name: 'Sarah Mahmoud', email: 'sarah.m@example.com', trips: 89, rating: '5.0', joined: 'Nov 2024', status: 'Active' }, { id: 'PSG-503', name: 'Reem Kanaan', email: 'reem.k@example.com', trips: 12, rating: '4.6', joined: 'Mar 2025', status: 'Suspended' }, ]; // Initialize App function initApp() { checkAuth(); setupNavigation(); renderRidesTable(); renderDriversTable(); renderPassengersTable(); initCharts(); setupEvents(); } // Web Device Fingerprint Generator function getWebFingerprint() { let fp = localStorage.getItem('siro_web_fp'); if (!fp) { fp = 'web_' + Math.random().toString(36).substring(2) + Date.now().toString(36); localStorage.setItem('siro_web_fp', fp); } return fp; } const deviceFingerprint = getWebFingerprint(); // Mode State (Live vs Demo) let isLiveMode = true; const modeLiveBtn = document.getElementById('modeLiveBtn'); const modeDemoBtn = document.getElementById('modeDemoBtn'); modeLiveBtn?.addEventListener('click', () => { isLiveMode = true; modeLiveBtn.style.background = 'var(--primary)'; modeLiveBtn.style.color = '#fff'; modeDemoBtn.style.background = 'transparent'; modeDemoBtn.style.color = 'var(--text-muted)'; }); modeDemoBtn?.addEventListener('click', () => { isLiveMode = false; modeDemoBtn.style.background = 'var(--primary)'; modeDemoBtn.style.color = '#fff'; modeLiveBtn.style.background = 'transparent'; modeLiveBtn.style.color = 'var(--text-muted)'; }); // Auth Functions function checkAuth() { if (currentUser && currentUser.isLive && currentUser.jwt) { authWrapper.classList.add('hidden'); fetchLiveDashboardData(); } else { // Clear stale/demo session to force fresh Live Backend Login localStorage.removeItem('siro_admin_user'); currentUser = null; authWrapper.classList.remove('hidden'); } } let pendingOtpPhone = ''; let pendingOtpPassword = ''; loginForm?.addEventListener('submit', async (e) => { e.preventDefault(); const phone = document.getElementById('loginEmail').value.trim(); const password = document.getElementById('loginPass').value.trim(); if (isLiveMode) { showNotification('Authenticating credentials with server...', 'info'); try { const formData = new FormData(); formData.append('phone', phone); formData.append('id', phone); // send both phone and id for maximum compatibility formData.append('password', password); formData.append('fingerprint', deviceFingerprint); formData.append('aud', 'admin'); // Try Admin auth login endpoint let response = await fetch('/backend/Admin/auth/login.php', { method: 'POST', body: formData }); // Fallback to loginAdmin.php if 404 if (response.status === 404) { response = await fetch('/backend/loginAdmin.php', { method: 'POST', body: formData }); } const resText = await response.text(); console.log('Server Raw Response:', resText); let res; try { res = JSON.parse(resText); } catch (jsonErr) { showNotification(`Server Error (${response.status}): ${resText.substring(0, 100)}`, 'danger'); return; } console.log('Parsed API Response:', res); // Check if login succeeded and JWT was returned const jwtToken = res.jwt || res.data?.jwt || (typeof res.message === 'object' ? res.message?.jwt : null); const adminInfo = res.admin || res.data?.admin || (typeof res.message === 'object' ? res.message?.admin : {}) || {}; if (response.ok && (res.status === 'success' || jwtToken)) { if (jwtToken) { currentUser = { name: adminInfo.name || 'Admin', email: adminInfo.email || phone, role: adminInfo.role || 'Administrator', jwt: jwtToken, isLive: true }; localStorage.setItem('siro_admin_user', JSON.stringify(currentUser)); authWrapper.classList.add('hidden'); showNotification('Access Granted! Welcome to Admin Portal.', 'success'); fetchLiveDashboardData(); } else if (res.status === 'otp_required' || (res.message && (res.message.status === 'otp_required' || res.message === 'otp_required'))) { pendingOtpPhone = phone; pendingOtpPassword = password; const masked = res.phone || (typeof res.message === 'object' ? res.message.phone : null) || phone; document.getElementById('otpPhoneText').textContent = `Verification code sent to WhatsApp (${masked})`; document.getElementById('otpModal').classList.add('active'); } else { showNotification('Login successful, loading dashboard...', 'success'); currentUser = { name: adminInfo.name || 'Admin', email: phone, role: 'Administrator', isLive: true }; localStorage.setItem('siro_admin_user', JSON.stringify(currentUser)); authWrapper.classList.add('hidden'); fetchLiveDashboardData(); } } else if (res.status === 'otp_required' || (res.message && (res.message.status === 'otp_required' || res.message === 'otp_required'))) { pendingOtpPhone = phone; pendingOtpPassword = password; const masked = res.phone || (typeof res.message === 'object' ? res.message.phone : null) || phone; document.getElementById('otpPhoneText').textContent = `Verification code sent to WhatsApp (${masked})`; document.getElementById('otpModal').classList.add('active'); } else { // Show REAL backend API error message const errorMsg = (typeof res.message === 'string' ? res.message : null) || res.error || 'Invalid credentials or user not found.'; showNotification(`Login Error: ${errorMsg}`, 'danger'); } } catch (err) { console.error('Login Exception:', err); showNotification(`Network Error: ${err.message || 'Could not connect to backend'}`, 'danger'); } } else { currentUser = { name: 'Super Admin (Demo)', email: phone, role: 'Administrator', isLive: false }; localStorage.setItem('siro_admin_user', JSON.stringify(currentUser)); authWrapper.classList.add('hidden'); showNotification('Welcome to Interactive Demo Mode!', 'success'); } }); // Submit OTP Code document.getElementById('submitOtpBtn')?.addEventListener('click', async () => { const otp = document.getElementById('otpInput').value.trim(); if (!otp || otp.length < 3) { showNotification('Please enter the 3-digit OTP code', 'warning'); return; } try { const formData = new FormData(); formData.append('otp', otp); formData.append('fingerprint', deviceFingerprint); formData.append('aud', 'admin'); const response = await fetch('/backend/Admin/auth/verify_login.php', { method: 'POST', body: formData }); const res = await response.json(); if (response.ok && res.status === 'success') { currentUser = { name: res.admin?.name || 'Admin', email: res.admin?.email || pendingOtpPhone, role: res.admin?.role || 'Administrator', jwt: res.jwt, isLive: true }; localStorage.setItem('siro_admin_user', JSON.stringify(currentUser)); closeOtpModal(); authWrapper.classList.add('hidden'); showNotification('OTP Verified! Welcome back, Admin.', 'success'); fetchLiveDashboardData(); } else { showNotification(res.message || 'Invalid OTP code', 'danger'); } } catch (err) { showNotification('OTP verification error', 'danger'); } }); window.closeOtpModal = function() { document.getElementById('otpModal')?.classList.remove('active'); }; async function fetchLiveDashboardData() { try { showNotification('Fetching live fleet metrics from database...', 'info'); const response = await fetch('/backend/Admin/dashbord.php', { headers: { 'Authorization': `Bearer ${currentUser?.jwt || ''}` } }); const data = await response.json(); let stats = null; if (data && data.status === 'success') { stats = Array.isArray(data.data) ? data.data[0] : (data.data?.data ? data.data.data[0] : data.data); } else if (data && data[0]) { stats = data[0]; } if (stats) { // Update Revenue & Stats Cards const statCards = document.querySelectorAll('.stat-card'); statCards.forEach(card => { const title = card.querySelector('.stat-title')?.textContent.trim(); const valEl = card.querySelector('.stat-value'); if (!valEl) return; if (title?.includes('Revenue')) { const rev = stats.total_driver_earnings || stats.total_revenue || '42,850'; valEl.textContent = typeof rev === 'number' || !isNaN(rev) ? `$${parseFloat(rev).toLocaleString()}` : rev; } else if (title?.includes('Rides') || title?.includes('Trips')) { valEl.textContent = (stats.countRide || stats.total_rides || '1,482').toLocaleString(); } else if (title?.includes('Captains') || title?.includes('Drivers')) { valEl.textContent = (stats.countDriver || stats.num_Driver || '324').toLocaleString(); } else if (title?.includes('Passengers') || title?.includes('Satisfaction')) { valEl.textContent = (stats.countPassengers || '8,920').toLocaleString(); } }); // Update system health metrics if present const totalDistanceEl = document.getElementById('totalDistanceMetric'); if (totalDistanceEl && stats.total_distance) { totalDistanceEl.textContent = `${stats.total_distance} km`; } showNotification('Live Database Stats Synchronized!', 'success'); // Fetch Live Detailed Tables fetchLiveRides(); fetchLivePassengers(); fetchLivePendingStaff(); } } catch (err) { console.log('Using local fallback metrics for display:', err); } } async function fetchLiveRides() { try { const response = await fetch('/backend/Admin/rides/get_rides_by_status.php?status=All', { headers: { 'Authorization': `Bearer ${currentUser?.jwt || ''}` } }); const data = await response.json(); const rides = Array.isArray(data) ? data : (data.data || []); if (rides.length > 0) { const tbody = document.getElementById('ridesTableBody'); if (tbody) { tbody.innerHTML = rides.slice(0, 15).map(r => ` SR-${r.id} ${r.p_fname || 'Passenger'} ${r.p_lname || ''} ${r.d_fname || 'Driver'} ${r.d_lname || ''} ${r.address_start || 'Pickup'} → ${r.address_end || 'Dropoff'} $${r.price || r.price_for_passenger || '0.00'} ${r.status || 'Active'} ${r.created_at ? r.created_at.substring(11, 16) : 'Just now'} `).join(''); } } } catch (err) { console.log('Error fetching live rides:', err); } } async function fetchLivePassengers() { try { const response = await fetch('/backend/Admin/getPassengerDetails.php', { headers: { 'Authorization': `Bearer ${currentUser?.jwt || ''}` } }); const data = await response.json(); const passengers = Array.isArray(data) ? data : (data.data || []); if (passengers.length > 0) { const tbody = document.getElementById('passengersTableBody'); if (tbody) { tbody.innerHTML = passengers.slice(0, 15).map(p => ` PSG-${p.id} ${p.first_name || 'Passenger'} ${p.last_name || ''} ${p.email || p.phone || 'N/A'} ${p.countPassengerRide || 0} trips ${p.passengerAverageRating || '5.0'} ${p.created_at ? p.created_at.substring(0, 10) : 'Jan 2025'} ${p.status || 'Active'} `).join(''); } } } catch (err) { console.log('Error fetching live passengers:', err); } } async function fetchLivePendingStaff() { try { const response = await fetch('/backend/Admin/Staff/pending.php', { headers: { 'Authorization': `Bearer ${currentUser?.jwt || ''}` } }); const data = await response.json(); const pending = data?.data || []; if (pending.length > 0) { showNotification(`Notice: You have ${pending.length} pending staff approval requests!`, 'info'); } } catch (err) { console.log('Pending staff lookup:', err); } } const fingerprintLoginBtn = document.getElementById('fingerprintLoginBtn'); fingerprintLoginBtn?.addEventListener('click', async () => { showNotification('Touch Fingerprint sensor on your device...', 'info'); // Check WebAuthn support if (window.PublicKeyCredential) { try { // Biometric WebAuthn prompt simulation / API call setTimeout(() => { currentUser = { name: 'Super Admin (Biometric)', email: 'admin@siromove.com', role: 'Administrator', authMethod: 'Fingerprint/TouchID' }; localStorage.setItem('siro_admin_user', JSON.stringify(currentUser)); authWrapper.classList.add('hidden'); showNotification('Fingerprint Verified! Access Granted.', 'success'); }, 1200); } catch (err) { showNotification('Fingerprint verification failed', 'danger'); } } else { currentUser = { name: 'Super Admin', email: 'admin@siromove.com', role: 'Administrator', authMethod: 'Biometric' }; localStorage.setItem('siro_admin_user', JSON.stringify(currentUser)); authWrapper.classList.add('hidden'); showNotification('Biometric Auth Verified!', 'success'); } }); logoutBtn?.addEventListener('click', () => { localStorage.removeItem('siro_admin_user'); currentUser = null; authWrapper.classList.remove('hidden'); }); // Sidebar & Navigation function setupNavigation() { navItems.forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const targetView = item.getAttribute('data-view'); navItems.forEach(n => n.classList.remove('active')); item.classList.add('active'); pageViews.forEach(view => { if (view.id === targetView) { view.classList.add('active'); } else { view.classList.remove('active'); } }); // Close sidebar on mobile if (window.innerWidth <= 992) { sidebar.classList.remove('open'); } }); }); toggleSidebarBtn?.addEventListener('click', () => { sidebar.classList.toggle('open'); }); } // Render Data Tables function renderRidesTable() { const tbody = document.getElementById('ridesTableBody'); if (!tbody) return; tbody.innerHTML = mockRides.map(ride => ` ${ride.id} ${ride.rider} ${ride.driver} ${ride.pickup} → ${ride.dropoff} ${ride.fare} ${ride.status} ${ride.time} `).join(''); } function renderDriversTable() { const tbody = document.getElementById('driversTableBody'); if (!tbody) return; tbody.innerHTML = mockDrivers.map(drv => ` ${drv.id}
${drv.name} ${drv.verified ? '' : ''}
${drv.phone} ${drv.vehicle} ${drv.rating} ${drv.trips} ${drv.status} `).join(''); } function renderPassengersTable() { const tbody = document.getElementById('passengersTableBody'); if (!tbody) return; tbody.innerHTML = mockPassengers.map(p => ` ${p.id} ${p.name} ${p.email} ${p.trips} trips ${p.rating} ${p.joined} ${p.status} `).join(''); } function getStatusBadge(status) { switch (status) { case 'Completed': case 'Online': case 'Active': return 'badge-success'; case 'In Progress': case 'In Trip': return 'badge-primary'; case 'Cancelled': case 'Offline': case 'Suspended': return 'badge-danger'; default: return 'badge-info'; } } // Dynamic Canvas Charts function initCharts() { const canvas = document.getElementById('revenueChart'); if (!canvas) return; const ctx = canvas.getContext('2d'); // Resize canvas canvas.width = canvas.parentElement.clientWidth; canvas.height = canvas.parentElement.clientHeight; const data = [12000, 18500, 15000, 24000, 29000, 34500, 42000]; const labels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const W = canvas.width; const H = canvas.height; const padding = 40; // Draw Grid Lines ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)'; ctx.lineWidth = 1; for (let i = 0; i <= 4; i++) { const y = padding + (i * (H - padding * 2) / 4); ctx.beginPath(); ctx.moveTo(padding, y); ctx.lineTo(W - padding, y); ctx.stroke(); } // Draw Smooth Line const max = 50000; const points = data.map((val, idx) => { const x = padding + (idx * (W - padding * 2) / (data.length - 1)); const y = H - padding - (val / max * (H - padding * 2)); return { x, y }; }); // Area Gradient const gradient = ctx.createLinearGradient(0, 0, 0, H); gradient.addColorStop(0, 'rgba(99, 102, 241, 0.4)'); gradient.addColorStop(1, 'rgba(99, 102, 241, 0.0)'); ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length; i++) { const xc = (points[i].x + points[i - 1].x) / 2; const yc = (points[i].y + points[i - 1].y) / 2; ctx.quadraticCurveTo(points[i - 1].x, points[i - 1].y, xc, yc); } ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y); ctx.lineTo(points[points.length - 1].x, H - padding); ctx.lineTo(points[0].x, H - padding); ctx.closePath(); ctx.fillStyle = gradient; ctx.fill(); // Line Path ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length; i++) { const xc = (points[i].x + points[i - 1].x) / 2; const yc = (points[i].y + points[i - 1].y) / 2; ctx.quadraticCurveTo(points[i - 1].x, points[i - 1].y, xc, yc); } ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y); ctx.strokeStyle = '#6366f1'; ctx.lineWidth = 3; ctx.stroke(); // Draw Points points.forEach((p, idx) => { ctx.beginPath(); ctx.arc(p.x, p.y, 5, 0, Math.PI * 2); ctx.fillStyle = '#6366f1'; ctx.fill(); ctx.lineWidth = 2; ctx.strokeStyle = '#fff'; ctx.stroke(); // Labels ctx.fillStyle = '#94a3b8'; ctx.font = '12px Inter'; ctx.textAlign = 'center'; ctx.fillText(labels[idx], p.x, H - 15); }); } // Setup Event Listeners & Search Filters function setupEvents() { window.addEventListener('resize', () => { initCharts(); }); const searchInput = document.getElementById('globalSearch'); searchInput?.addEventListener('input', (e) => { const query = e.target.value.toLowerCase(); // Highlight matching items in tables if any }); } // Notifications Helper function showNotification(msg, type = 'info') { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; bottom: 24px; right: 24px; background: var(--bg-elevated); border: 1px solid var(--border-active); color: #fff; padding: 12px 20px; border-radius: 12px; box-shadow: var(--shadow-md); z-index: 9999; font-size: 0.9rem; animation: fadeIn 0.3s ease; `; toast.innerHTML = ` ${msg}`; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3500); } window.viewDetails = function(id) { const modal = document.getElementById('detailsModal'); const modalBody = document.getElementById('modalBodyContent'); if (!modal || !modalBody) return; modalBody.innerHTML = `

Ride Details: ${id}

Pickup: Abdoun Circle, Amman

Dropoff: Queen Alia International Airport

Fare Breakdown: Base Fare ($4.00) + Distance ($20.00) + Peak Surge ($4.00)

Payment Method: Credit Card (Visa **** 4921)

`; modal.classList.add('active'); }; window.closeModal = function() { const modal = document.getElementById('detailsModal'); modal?.classList.remove('active'); }; // Run initial state initApp(); });