Initial commit with updated Auth and media ignored
This commit is contained in:
341
ride/location/driversTime.html
Executable file
341
ride/location/driversTime.html
Executable file
@@ -0,0 +1,341 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>متابعة السائقين - انطلق</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700;800&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
body { font-family: 'Tajawal', sans-serif; background-color: #f8fafc; }
|
||||
.driver-card { transition: all 0.2s ease; }
|
||||
.driver-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); }
|
||||
/* Loading Spinner */
|
||||
.loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
|
||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-slate-800">
|
||||
|
||||
<div class="bg-slate-900 text-white p-6 shadow-lg sticky top-0 z-50">
|
||||
<div class="container mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold flex items-center gap-3">
|
||||
<i class="fa-solid fa-taxi text-yellow-400"></i>
|
||||
لوحة متابعة السائقين
|
||||
</h1>
|
||||
<p class="text-slate-400 text-xs mt-1 flex items-center gap-2">
|
||||
<i class="fa-solid fa-clock"></i>
|
||||
توقيت آخر تحديث للبيانات:
|
||||
<span id="lastUpdatedBadge" class="font-bold text-yellow-400 dir-ltr">جاري التحميل...</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button onclick="location.reload()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm flex items-center gap-2 transition">
|
||||
<i class="fa-solid fa-rotate-right"></i>
|
||||
تحديث الصفحة
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loadingState" class="text-center py-20">
|
||||
<div class="loader"></div>
|
||||
<p class="text-gray-500 mt-4">جاري جلب ملف البيانات (active_drivers_cache.json)...</p>
|
||||
</div>
|
||||
|
||||
<div id="errorState" class="hidden container mx-auto p-6 text-center">
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-8 rounded-lg">
|
||||
<i class="fa-solid fa-triangle-exclamation text-4xl mb-4"></i>
|
||||
<h3 class="font-bold text-lg">تعذر تحميل البيانات</h3>
|
||||
<p class="mt-2">تأكد من وجود ملف <b>active_drivers_cache.json</b> في نفس المجلد.</p>
|
||||
<p class="text-sm mt-2 text-gray-500">ملاحظة تقنية: يجب تشغيل هذا الملف عبر سيرفر محلي (Localhost) ليعمل بشكل صحيح.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mainContent" class="container mx-auto p-6 hidden animate-fade-in">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white p-4 rounded-xl shadow-sm border-r-4 border-blue-500">
|
||||
<p class="text-gray-500 text-xs font-bold">إجمالي السائقين</p>
|
||||
<h3 class="text-2xl font-bold text-blue-700" id="statTotal">0</h3>
|
||||
</div>
|
||||
<div class="bg-white p-4 rounded-xl shadow-sm border-r-4 border-yellow-500">
|
||||
<p class="text-gray-500 text-xs font-bold">العمالقة (النخبة)</p>
|
||||
<h3 class="text-2xl font-bold text-yellow-600" id="statElite">0</h3>
|
||||
</div>
|
||||
<div class="bg-white p-4 rounded-xl shadow-sm border-r-4 border-red-500">
|
||||
<p class="text-gray-500 text-xs font-bold">الخاملون (للاتصال)</p>
|
||||
<h3 class="text-2xl font-bold text-red-600" id="statInactive">0</h3>
|
||||
</div>
|
||||
<div class="bg-white p-4 rounded-xl shadow-sm border-r-4 border-green-500">
|
||||
<p class="text-gray-500 text-xs font-bold">أعلى نشاط مسجل</p>
|
||||
<h3 class="text-xl font-bold text-green-600 dir-ltr" id="statMaxTime">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<div class="bg-white p-5 rounded-xl shadow-sm border border-gray-100">
|
||||
<h3 class="font-bold text-sm mb-4 text-gray-700">توزيع حالة السائقين</h3>
|
||||
<div class="h-48">
|
||||
<canvas id="driversChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-xl shadow-sm border border-gray-100 lg:col-span-2 flex flex-col">
|
||||
<h3 class="font-bold text-sm mb-4 text-gray-700">تصفية وفرز القائمة</h3>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-4" id="filterButtons">
|
||||
<button onclick="filterData('all')" class="px-3 py-1.5 rounded-md bg-slate-800 text-white text-xs hover:bg-slate-700 ring-2 ring-offset-1 ring-slate-800 active-filter">الكل</button>
|
||||
<button onclick="filterData('elite')" class="px-3 py-1.5 rounded-md bg-yellow-500 text-white text-xs hover:bg-yellow-600">النخبة (+50س)</button>
|
||||
<button onclick="filterData('stable')" class="px-3 py-1.5 rounded-md bg-green-500 text-white text-xs hover:bg-green-600">مستقرون</button>
|
||||
<button onclick="filterData('experimental')" class="px-3 py-1.5 rounded-md bg-blue-400 text-white text-xs hover:bg-blue-500">تجريبيون</button>
|
||||
<button onclick="filterData('inactive')" class="px-3 py-1.5 rounded-md bg-red-500 text-white text-xs hover:bg-red-600">خاملون (أولوية)</button>
|
||||
</div>
|
||||
|
||||
<div class="relative mt-auto w-full">
|
||||
<input type="text" id="searchInput" placeholder="ابحث عن اسم أو رقم..."
|
||||
class="w-full p-2.5 pr-10 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none">
|
||||
<i class="fa-solid fa-magnifying-glass absolute top-3 right-3 text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-right">
|
||||
<thead class="bg-gray-50 text-gray-600 border-b">
|
||||
<tr>
|
||||
<th class="px-6 py-3 font-bold">السائق</th>
|
||||
<th class="px-6 py-3 font-bold">الهاتف</th>
|
||||
<th class="px-6 py-3 font-bold cursor-pointer hover:text-blue-600" onclick="sortData('minutes')">
|
||||
الأداء (ساعات) <i class="fa-solid fa-sort"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 font-bold">الحالة</th>
|
||||
<th class="px-6 py-3 text-center font-bold">اتصال</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody" class="divide-y divide-gray-100">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 text-xs text-gray-500 flex justify-between">
|
||||
<span id="showingCount"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const JSON_FILE_NAME = 'active_drivers_cache.json';
|
||||
let rawData = [];
|
||||
let filteredData = [];
|
||||
let chartInstance = null;
|
||||
|
||||
// عند تحميل الصفحة
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
// دالة جلب البيانات تلقائياً
|
||||
async function fetchData() {
|
||||
try {
|
||||
// إضافة طابع زمني لمنع الكاش وضمان جلب أحدث ملف
|
||||
const timestamp = new Date().getTime();
|
||||
const response = await fetch(`${JSON_FILE_NAME}?t=${timestamp}`);
|
||||
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
// إخفاء اللودر وإظهار المحتوى
|
||||
document.getElementById('loadingState').classList.add('hidden');
|
||||
document.getElementById('mainContent').classList.remove('hidden');
|
||||
|
||||
// تحديث تاريخ آخر تحديث في الهيدر
|
||||
if(json.last_updated) {
|
||||
document.getElementById('lastUpdatedBadge').innerText = json.last_updated;
|
||||
}
|
||||
|
||||
processData(json);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
document.getElementById('loadingState').classList.add('hidden');
|
||||
document.getElementById('errorState').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function processData(json) {
|
||||
rawData = json.data.map(driver => {
|
||||
const timeStr = driver.active_time || "0 ساعة و 0 دقيقة";
|
||||
const hoursMatch = timeStr.match(/(\d+)\s*ساعة/);
|
||||
const minsMatch = timeStr.match(/(\d+)\s*دقيقة/);
|
||||
|
||||
const hours = hoursMatch ? parseInt(hoursMatch[1]) : 0;
|
||||
const mins = minsMatch ? parseInt(minsMatch[1]) : 0;
|
||||
const totalMinutes = (hours * 60) + mins;
|
||||
|
||||
// تصنيف السائقين
|
||||
let category = 'inactive';
|
||||
let catLabel = 'خامل';
|
||||
let catColor = 'bg-red-100 text-red-700 border border-red-200';
|
||||
|
||||
if (totalMinutes >= 3000) { // 50 hours
|
||||
category = 'elite';
|
||||
catLabel = 'نخبة';
|
||||
catColor = 'bg-yellow-100 text-yellow-700 border border-yellow-200';
|
||||
} else if (totalMinutes >= 1200) { // 20 hours
|
||||
category = 'stable';
|
||||
catLabel = 'مستقر';
|
||||
catColor = 'bg-green-100 text-green-700 border border-green-200';
|
||||
} else if (totalMinutes >= 300) { // 5 hours
|
||||
category = 'experimental';
|
||||
catLabel = 'تجريبي';
|
||||
catColor = 'bg-blue-100 text-blue-700 border border-blue-200';
|
||||
}
|
||||
|
||||
return { ...driver, totalMinutes, hours, mins, category, catLabel, catColor };
|
||||
});
|
||||
|
||||
// الترتيب الافتراضي: حسب الأكثر نشاطاً
|
||||
rawData.sort((a, b) => b.totalMinutes - a.totalMinutes);
|
||||
|
||||
updateStats();
|
||||
filterData('all');
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
const total = rawData.length;
|
||||
const elite = rawData.filter(d => d.category === 'elite').length;
|
||||
const inactive = rawData.filter(d => d.category === 'inactive').length;
|
||||
|
||||
document.getElementById('statTotal').innerText = total;
|
||||
document.getElementById('statElite').innerText = elite;
|
||||
document.getElementById('statInactive').innerText = inactive;
|
||||
|
||||
if(rawData.length > 0) {
|
||||
document.getElementById('statMaxTime').innerText = rawData[0].active_time;
|
||||
}
|
||||
|
||||
// Chart.js
|
||||
const ctx = document.getElementById('driversChart').getContext('2d');
|
||||
if (chartInstance) chartInstance.destroy();
|
||||
|
||||
const stable = rawData.filter(d => d.category === 'stable').length;
|
||||
const exp = rawData.filter(d => d.category === 'experimental').length;
|
||||
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['النخبة', 'مستقر', 'تجريبي', 'خامل'],
|
||||
datasets: [{
|
||||
data: [elite, stable, exp, inactive],
|
||||
backgroundColor: ['#EAB308', '#22C55E', '#60A5FA', '#EF4444'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'left', labels: { font: { family: 'Tajawal', size: 10 }, boxWidth: 10 } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function filterData(type) {
|
||||
// Update UI Buttons
|
||||
document.querySelectorAll('#filterButtons button').forEach(btn => {
|
||||
btn.classList.remove('ring-2', 'ring-offset-1', 'ring-slate-800');
|
||||
if(btn.getAttribute('onclick').includes(`'${type}'`)) {
|
||||
btn.classList.add('ring-2', 'ring-offset-1', 'ring-slate-800');
|
||||
}
|
||||
});
|
||||
|
||||
filteredData = type === 'all' ? rawData : rawData.filter(d => d.category === type);
|
||||
|
||||
document.getElementById('searchInput').value = '';
|
||||
renderTable(filteredData);
|
||||
}
|
||||
|
||||
// البحث
|
||||
document.getElementById('searchInput').addEventListener('input', (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
const currentData = filteredData;
|
||||
// ملاحظة: البحث يتم داخل الفئة المختارة حالياً
|
||||
const searchResults = filteredData.filter(driver =>
|
||||
(driver.name_arabic && driver.name_arabic.toLowerCase().includes(term)) ||
|
||||
(driver.phone && driver.phone.includes(term))
|
||||
);
|
||||
renderTable(searchResults);
|
||||
});
|
||||
|
||||
function renderTable(data) {
|
||||
const tbody = document.getElementById('tableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="5" class="text-center py-8 text-gray-400">لا توجد بيانات</td></tr>`;
|
||||
document.getElementById('showingCount').innerText = '';
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(driver => {
|
||||
const createdDate = driver.created_at ? driver.created_at.split(' ')[0] : '-';
|
||||
|
||||
const row = `
|
||||
<tr class="hover:bg-slate-50 driver-card bg-white">
|
||||
<td class="px-6 py-3">
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold text-slate-700">${driver.name_arabic || 'غير مسمى'}</span>
|
||||
<span class="text-[10px] text-gray-400">انضم: ${createdDate}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3 font-mono text-xs text-gray-600 dir-ltr text-right select-all">
|
||||
${driver.phone}
|
||||
</td>
|
||||
<td class="px-6 py-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="w-full bg-gray-100 rounded-full h-2 overflow-hidden">
|
||||
<div class="h-2 ${driver.category === 'elite' ? 'bg-yellow-400' : driver.category === 'inactive' ? 'bg-red-400' : 'bg-blue-400'}" style="width: ${Math.min((driver.totalMinutes / 6000) * 100, 100)}%"></div>
|
||||
</div>
|
||||
<span class="text-[10px] text-gray-500 font-bold">${driver.active_time}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-3">
|
||||
<span class="${driver.catColor} px-2 py-0.5 rounded text-[10px] font-bold inline-block min-w-[50px] text-center">
|
||||
${driver.catLabel}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-3 text-center">
|
||||
<div class="flex justify-center gap-2">
|
||||
<a href="https://wa.me/${driver.phone}" target="_blank" class="w-8 h-8 rounded-full bg-green-100 text-green-600 hover:bg-green-600 hover:text-white flex items-center justify-center transition">
|
||||
<i class="fa-brands fa-whatsapp"></i>
|
||||
</a>
|
||||
<a href="tel:${driver.phone}" class="w-8 h-8 rounded-full bg-blue-100 text-blue-600 hover:bg-blue-600 hover:text-white flex items-center justify-center transition">
|
||||
<i class="fa-solid fa-phone"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
tbody.insertAdjacentHTML('beforeend', row);
|
||||
});
|
||||
|
||||
document.getElementById('showingCount').innerText = `عرض ${data.length} من أصل ${rawData.length}`;
|
||||
}
|
||||
|
||||
let sortDir = 'desc';
|
||||
function sortData(key) {
|
||||
sortDir = sortDir === 'desc' ? 'asc' : 'desc';
|
||||
filteredData.sort((a, b) => sortDir === 'asc' ? a.totalMinutes - b.totalMinutes : b.totalMinutes - a.totalMinutes);
|
||||
renderTable(filteredData);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user