334 lines
12 KiB
JavaScript
334 lines
12 KiB
JavaScript
/* ==========================================================================
|
|
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();
|
|
}
|
|
|
|
// Auth Functions
|
|
function checkAuth() {
|
|
if (currentUser) {
|
|
authWrapper.classList.add('hidden');
|
|
} else {
|
|
authWrapper.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
loginForm?.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
const email = document.getElementById('loginEmail').value;
|
|
currentUser = { name: 'Super Admin', email: email, role: 'Administrator' };
|
|
localStorage.setItem('siro_admin_user', JSON.stringify(currentUser));
|
|
authWrapper.classList.add('hidden');
|
|
showNotification('Welcome back, Super Admin!', '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 => `
|
|
<tr>
|
|
<td><strong>${ride.id}</strong></td>
|
|
<td>${ride.rider}</td>
|
|
<td>${ride.driver}</td>
|
|
<td><span style="font-size:0.82rem; color: var(--text-muted);"><i class="ph ph-map-pin"></i> ${ride.pickup} → ${ride.dropoff}</span></td>
|
|
<td><strong>${ride.fare}</strong></td>
|
|
<td><span class="badge ${getStatusBadge(ride.status)}">${ride.status}</span></td>
|
|
<td>${ride.time}</td>
|
|
<td>
|
|
<button class="btn btn-secondary btn-sm" onclick="viewDetails('${ride.id}')">
|
|
<i class="ph ph-eye"></i> View
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function renderDriversTable() {
|
|
const tbody = document.getElementById('driversTableBody');
|
|
if (!tbody) return;
|
|
tbody.innerHTML = mockDrivers.map(drv => `
|
|
<tr>
|
|
<td><strong>${drv.id}</strong></td>
|
|
<td>
|
|
<div style="display:flex; align-items:center; gap:0.5rem;">
|
|
<span>${drv.name}</span>
|
|
${drv.verified ? '<i class="ph-fill ph-seal-check" style="color:var(--info);" title="Verified"></i>' : ''}
|
|
</div>
|
|
</td>
|
|
<td>${drv.phone}</td>
|
|
<td>${drv.vehicle}</td>
|
|
<td><i class="ph-fill ph-star" style="color:var(--warning);"></i> ${drv.rating}</td>
|
|
<td>${drv.trips}</td>
|
|
<td><span class="badge ${getStatusBadge(drv.status)}">${drv.status}</span></td>
|
|
<td>
|
|
<button class="btn btn-secondary btn-sm"><i class="ph ph-gear"></i> Manage</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function renderPassengersTable() {
|
|
const tbody = document.getElementById('passengersTableBody');
|
|
if (!tbody) return;
|
|
tbody.innerHTML = mockPassengers.map(p => `
|
|
<tr>
|
|
<td><strong>${p.id}</strong></td>
|
|
<td>${p.name}</td>
|
|
<td>${p.email}</td>
|
|
<td>${p.trips} trips</td>
|
|
<td><i class="ph-fill ph-star" style="color:var(--warning);"></i> ${p.rating}</td>
|
|
<td>${p.joined}</td>
|
|
<td><span class="badge ${getStatusBadge(p.status)}">${p.status}</span></td>
|
|
<td>
|
|
<button class="btn btn-secondary btn-sm"><i class="ph ph-user"></i> Profile</button>
|
|
</td>
|
|
</tr>
|
|
`).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 = `<i class="ph ph-check-circle" style="color:var(--success);"></i> ${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 = `
|
|
<h3 style="margin-bottom:1rem; color:#fff;">Ride Details: ${id}</h3>
|
|
<p style="color:var(--text-muted); margin-bottom:0.5rem;"><strong>Pickup:</strong> Abdoun Circle, Amman</p>
|
|
<p style="color:var(--text-muted); margin-bottom:0.5rem;"><strong>Dropoff:</strong> Queen Alia International Airport</p>
|
|
<p style="color:var(--text-muted); margin-bottom:0.5rem;"><strong>Fare Breakdown:</strong> Base Fare ($4.00) + Distance ($20.00) + Peak Surge ($4.00)</p>
|
|
<p style="color:var(--text-muted); margin-bottom:1.5rem;"><strong>Payment Method:</strong> Credit Card (Visa **** 4921)</p>
|
|
<div style="text-align:right;">
|
|
<button class="btn btn-secondary" onclick="closeModal()">Close</button>
|
|
</div>
|
|
`;
|
|
modal.classList.add('active');
|
|
};
|
|
|
|
window.closeModal = function() {
|
|
const modal = document.getElementById('detailsModal');
|
|
modal?.classList.remove('active');
|
|
};
|
|
|
|
// Run initial state
|
|
initApp();
|
|
});
|