يوبر سوريا
رحلتك الآمنة تبدأ من هنا
تسجيل دخول سريع وآمن بنقرة واحدة
ربط رقم الهاتف
يرجى إدخال رقم هاتفك لإضافته إلى حسابك وإتمام عملية التسجيل.
شكاوى واقتراحات
إرسال شكوى أو اقتراح
✅ تم إرسال شكواك بنجاح. سيتواصل معك الفريق قريباً.
شكاواي السابقة
جاري البحث عن كابتن
نقوم الآن بالبحث في محيطك لتوفير أسرع سيارة
الكابتن في الطريق
إليك
سيصل خلال 3
دقائق
وصلت بسلامة! 🎉
كيف كانت تجربتك مع الكابتن؟
المبلغ المطلوب للدفع
--- ل.س
0 كم
•
0 دقيقة
// --- الإعدادات الأساسية الديناميكية ---
const API_BASE = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://127.0.0.1:3010/api/v1'
: 'https://eyadbasher-upersyria-backend.hf.space/api/v1';
let map, userMarker, destMarker, driverMarker, routeLayer;
let socket;
let userLocation = [36.2765, 33.5138]; // دمشق افتراضياً
let destination = null; // {lat, lng, name}
let currentTrip = null;
// --- عناصر الواجهة ---
const loginScreen = document.getElementById('login-screen');
const appScreen = document.getElementById('app-screen');
const bottomPanel = document.getElementById('bottom-panel');
const stateSearch = document.getElementById('search-state');
const stateConfirm = document.getElementById('confirm-state');
const stateFinding = document.getElementById('finding-state');
const stateArriving = document.getElementById('driver-arriving-state');
const dropoffInput = document.getElementById('dropoff-input');
const searchResults = document.getElementById('search-results');
const searchSpinner = document.getElementById('search-spinner');
// --- التهيئة والتسجيل ---
document.addEventListener('DOMContentLoaded', () => {
if (localStorage.getItem('rider_token')) {
showApp();
}
});
// تبديل بين تبويب الدخول والتسجيل
function switchAuthTab(tab) {
const loginForm = document.getElementById('login-form');
const registerForm = document.getElementById('register-form');
const tabLogin = document.getElementById('tab-login');
const tabRegister = document.getElementById('tab-register');
const errorDiv = document.getElementById('login-error');
const successDiv = document.getElementById('login-success');
errorDiv.classList.add('hidden');
successDiv.classList.add('hidden');
if (tab === 'login') {
loginForm.classList.remove('hidden');
registerForm.classList.add('hidden');
tabLogin.classList.add('bg-white', 'text-slate-900', 'shadow-sm');
tabLogin.classList.remove('text-slate-500');
tabRegister.classList.remove('bg-white', 'text-slate-900', 'shadow-sm');
tabRegister.classList.add('text-slate-500');
} else {
loginForm.classList.add('hidden');
registerForm.classList.remove('hidden');
tabRegister.classList.add('bg-white', 'text-slate-900', 'shadow-sm');
tabRegister.classList.remove('text-slate-500');
tabLogin.classList.remove('bg-white', 'text-slate-900', 'shadow-sm');
tabLogin.classList.add('text-slate-500');
}
}
// نموذج الدخول
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const phone = document.getElementById('phone').value.trim();
const password = document.getElementById('login-password').value;
const errorDiv = document.getElementById('login-error');
const btn = e.target.querySelector('button[type=submit]');
errorDiv.classList.add('hidden');
btn.disabled = true;
btn.innerHTML = ' جاري الدخول...';
try {
const formattedPhone = phone.startsWith('+') ? phone : `+${phone}`;
const res = await fetch(`${API_BASE}/auth/rider/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, password })
});
const data = await res.json();
if (!res.ok) throw new Error(data.message || 'رقم الهاتف أو كلمة المرور غير صحيحة');
localStorage.setItem('rider_token', data.access_token);
localStorage.setItem('rider_user', JSON.stringify(data.user));
showApp();
} catch (err) {
errorDiv.innerText = err.message;
errorDiv.classList.remove('hidden');
btn.disabled = false;
btn.innerHTML = 'دخول';
}
});
// نموذج التسجيل - تم تعديله لطلب OTP أولاً
document.getElementById('register-form').addEventListener('submit', async (e) => {
e.preventDefault();
const phone = document.getElementById('reg-phone').value.trim();
const name = document.getElementById('reg-name').value.trim();
const password = document.getElementById('reg-password').value;
if (password.length < 6) {
const errorDiv = document.getElementById('login-error');
errorDiv.innerText = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
errorDiv.classList.remove('hidden');
return;
}
// حفظ البيانات مؤقتاً لإتمام التسجيل بعد التحقق
window.pendingRegData = { phone, name, password };
// إظهار شاشة OTP وملئ الرقم تلقائياً
document.getElementById('otp-screen').classList.remove('hidden');
document.getElementById('otp-phone').value = phone;
});
// --- نظام الـ OTP والتحقق ---
const otpScreen = document.getElementById('otp-screen');
const requestOtpForm = document.getElementById('request-otp-form');
const verifyOtpForm = document.getElementById('verify-otp-form');
const otpError = document.getElementById('otp-error');
function cancelOtp() {
otpScreen.classList.add('hidden');
window.pendingRegData = null;
}
async function handleGoogleLogin(response) {
try {
const decoded = jwt_decode(response.credential);
const res = await fetch(`${API_BASE}/auth/google-login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: decoded.email,
name: decoded.name,
googleId: decoded.sub,
picture: decoded.picture,
role: 'rider'
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.message || 'فشل تسجيل الدخول بجوجل');
if (data.needs_phone) {
window.pendingGoogleUserId = data.user.id;
document.getElementById('otp-screen').classList.remove('hidden');
} else {
localStorage.setItem('rider_token', data.access_token);
localStorage.setItem('rider_user', JSON.stringify(data.user));
showApp();
}
} catch (err) {
const errorDiv = document.getElementById('login-error');
errorDiv.innerText = err.message;
errorDiv.classList.remove('hidden');
}
}
requestOtpForm.addEventListener('submit', async (e) => {
e.preventDefault();
const phone = document.getElementById('otp-phone').value.trim();
const btn = document.getElementById('btn-link-phone');
otpError.classList.add('hidden');
btn.disabled = true;
btn.innerHTML = ' جاري الربط...';
try {
const formattedPhone = phone.startsWith('+') ? phone : `+${phone}`;
if (window.pendingGoogleUserId) {
const res = await fetch(`${API_BASE}/auth/link-phone`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: window.pendingGoogleUserId, phone: formattedPhone, role: 'rider' })
});
const data = await res.json();
if (!res.ok) throw new Error(data.message || 'فشل ربط الرقم');
localStorage.setItem('rider_token', data.access_token);
localStorage.setItem('rider_user', JSON.stringify(data.user));
otpScreen.classList.add('hidden');
showApp();
} else {
// Fallback for standard register without OTP
window.pendingRegData.phone = formattedPhone;
await completeRegistration(window.pendingRegData);
}
} catch (err) {
otpError.innerText = err.message;
otpError.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.innerHTML = 'تأكيد وربط الرقم';
}
});
async function completeRegistration(userData) {
try {
const formattedPhone = userData.phone.startsWith('+') ? userData.phone : `+${userData.phone}`;
const res = await fetch(`${API_BASE}/auth/rider/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: formattedPhone,
password: userData.password,
name: userData.name || undefined
})
});
const data = await res.json();
if (!res.ok) throw new Error(data.message || 'فشل إنشاء الحساب النهائي');
localStorage.setItem('rider_token', data.access_token);
localStorage.setItem('rider_user', JSON.stringify(data.user));
otpScreen.classList.add('hidden');
showApp();
} catch (err) {
alert('فشل إتمام التسجيل: ' + err.message);
cancelOtp();
}
}
function logout() {
localStorage.removeItem('rider_token');
localStorage.removeItem('rider_user');
if (socket) socket.disconnect();
location.reload();
}
function switchState(state) {
[stateSearch, stateConfirm, stateFinding, stateArriving].forEach(el => el.classList.add('hidden'));
state.classList.remove('hidden');
}
// --- الملف الشخصي ---
function updateTopAvatar() {
const userStr = localStorage.getItem('rider_user');
if (userStr) {
const user = JSON.parse(userStr);
const avatarUrl = user.avatar_url || user.picture || 'https://cdn-icons-png.flaticon.com/512/847/847969.png';
document.getElementById('top-avatar').src = avatarUrl;
}
}
function openProfileModal() {
const userStr = localStorage.getItem('rider_user');
if (userStr) {
const user = JSON.parse(userStr);
document.getElementById('profile-name').innerText = user.name || 'مستخدم بدون اسم';
document.getElementById('profile-email').innerText = user.email || '';
document.getElementById('profile-phone-input').value = user.phone || '';
const avatarUrl = user.avatar_url || user.picture || 'https://cdn-icons-png.flaticon.com/512/847/847969.png';
document.getElementById('profile-avatar').src = avatarUrl;
}
document.getElementById('profile-modal').classList.remove('hidden');
}
async function updateProfilePhone() {
const phoneInput = document.getElementById('profile-phone-input').value.trim();
const btn = document.getElementById('btn-update-phone');
const msgDiv = document.getElementById('profile-phone-msg');
const userStr = localStorage.getItem('rider_user');
if (!userStr || !phoneInput) return;
const user = JSON.parse(userStr);
msgDiv.classList.add('hidden');
btn.disabled = true;
btn.innerHTML = '';
try {
const formattedPhone = phoneInput.startsWith('+') ? phoneInput : `+${phoneInput}`;
const res = await fetch(`${API_BASE}/auth/link-phone`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id, phone: formattedPhone, role: 'rider' })
});
const data = await res.json();
if (!res.ok) throw new Error(data.message || 'فشل تحديث الرقم');
localStorage.setItem('rider_token', data.access_token);
localStorage.setItem('rider_user', JSON.stringify(data.user));
msgDiv.className = 'mt-3 text-sm font-bold text-center rounded-xl p-2 bg-green-50 text-green-600 border border-green-100';
msgDiv.innerText = 'تم تحديث الرقم بنجاح!';
msgDiv.classList.remove('hidden');
setTimeout(() => msgDiv.classList.add('hidden'), 3000);
} catch (err) {
msgDiv.className = 'mt-3 text-sm font-bold text-center rounded-xl p-2 bg-red-50 text-red-600 border border-red-100';
msgDiv.innerText = err.message;
msgDiv.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.innerText = 'حفظ';
}
}
// --- الخريطة (MapLibre) ---
function showApp() {
loginScreen.classList.add('hidden');
appScreen.classList.remove('hidden');
updateTopAvatar();
initMap();
initSocket();
}
function initMap() {
if (map) return;
map = new maplibregl.Map({
container: 'map',
style: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
center: userLocation,
zoom: 15,
attributionControl: false
});
if (maplibregl.getRTLTextPluginStatus() === 'unavailable') {
maplibregl.setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js', null, true);
}
// --- أيقونة الراكب --- تُنشأ مباشرة على الموقع الافتراضي
function buildUserMarkerEl() {
const el = document.createElement('div');
el.innerHTML = `
`;
return el;
}
userMarker = new maplibregl.Marker({ element: buildUserMarkerEl(), anchor: 'center' })
.setLngLat(userLocation)
.addTo(map);
// طلب GPS فوري ومستمر
locateUser();
}
let userWatchId = null;
function locateUser() {
if (!navigator.geolocation) return;
// أولاً: موقع سريع وحيد لتوسيط الخريطة فوراً
navigator.geolocation.getCurrentPosition(
(pos) => {
userLocation = [pos.coords.longitude, pos.coords.latitude];
if (userMarker) userMarker.setLngLat(userLocation);
if (map) map.flyTo({ center: userLocation, zoom: 16, duration: 1000 });
simulateNearbyCars(userLocation);
},
() => { simulateNearbyCars(userLocation); },
{ enableHighAccuracy: true, timeout: 8000, maximumAge: 0 }
);
// ثانياً: تتبع مستمر للموقع الحي (watchPosition)
if (userWatchId !== null) navigator.geolocation.clearWatch(userWatchId);
userWatchId = navigator.geolocation.watchPosition(
(pos) => {
userLocation = [pos.coords.longitude, pos.coords.latitude];
if (userMarker) userMarker.setLngLat(userLocation);
},
(err) => console.log('GPS watch error:', err.code),
{ enableHighAccuracy: true, maximumAge: 3000, timeout: 15000 }
);
}
// --- محاكاة السيارات القريبة ---
let nearbyCarMarkers = [];
let simulationInterval = null;
function simulateNearbyCars(center) {
// إزالة السيارات القديمة
nearbyCarMarkers.forEach(m => m.marker.remove());
nearbyCarMarkers = [];
if (simulationInterval) clearInterval(simulationInterval);
// إنشاء 3 إلى 5 سيارات عشوائية
const numCars = Math.floor(Math.random() * 3) + 3;
for (let i = 0; i < numCars; i++) {
const lng = center[0] + (Math.random() - 0.5) * 0.015;
const lat = center[1] + (Math.random() - 0.5) * 0.015;
const el = document.createElement('div');
el.innerHTML = '
';
const marker = new maplibregl.Marker({ element: el }).setLngLat([lng, lat]).addTo(map);
nearbyCarMarkers.push({ marker, target: [lng, lat] });
}
// تحريك السيارات ببطء
simulationInterval = setInterval(() => {
nearbyCarMarkers.forEach(car => {
if (Math.random() > 0.3) {
car.target[0] += (Math.random() - 0.5) * 0.001;
car.target[1] += (Math.random() - 0.5) * 0.001;
car.marker.setLngLat(car.target);
// تدوير أيقونة السيارة بناءً على الاتجاه تقريبياً
const icon = car.marker.getElement().querySelector('div');
const angle = Math.random() * 360;
icon.style.transform = `rotate(${angle}deg)`;
}
});
}, 3000);
}
// --- قائمة المواقع المسبقة (لضمان عمل البحث دائماً وبسرعة مثل أوبر) ---
const popularLocations = [
{ name: "ساحة الأمويين", sub: "دمشق، سوريا", lat: 33.5138, lng: 36.2765 },
{ name: "المزة - فيلات غربية", sub: "دمشق، سوريا", lat: 33.5061, lng: 36.2555 },
{ name: "المزة - شيخ سعد", sub: "دمشق، سوريا", lat: 33.5015, lng: 36.2621 },
{ name: "القصاع - ساحة جورج خوري", sub: "دمشق، سوريا", lat: 33.5180, lng: 36.3150 },
{ name: "باب توما", sub: "دمشق، سوريا", lat: 33.5126, lng: 36.3153 },
{ name: "المرجة - الساحة", sub: "دمشق، سوريا", lat: 33.5133, lng: 36.2974 },
{ name: "أبو رمانة", sub: "دمشق، سوريا", lat: 33.5222, lng: 36.2878 },
{ name: "الشعلان", sub: "دمشق، سوريا", lat: 33.5186, lng: 36.2911 },
{ name: "مشروع دمر", sub: "دمشق، سوريا", lat: 33.5358, lng: 36.2301 },
{ name: "كفرسوسة - ساحة الجمارك", sub: "دمشق، سوريا", lat: 33.5032, lng: 36.2825 },
{ name: "المالكي", sub: "دمشق، سوريا", lat: 33.5230, lng: 36.2800 },
{ name: "البرامكة - جامعة دمشق", sub: "دمشق، سوريا", lat: 33.5113, lng: 36.2882 },
{ name: "الميدان - الميدان التحتاني", sub: "دمشق، سوريا", lat: 33.4930, lng: 36.3021 },
{ name: "جرمانا - ساحة السيوف", sub: "ريف دمشق، سوريا", lat: 33.4862, lng: 36.3533 },
{ name: "ضاحية قدسيا", sub: "ريف دمشق، سوريا", lat: 33.5417, lng: 36.2081 }
];
let searchTimeout;
dropoffInput.addEventListener('input', (e) => {
const query = e.target.value.trim().toLowerCase();
clearTimeout(searchTimeout);
if (query.length < 2) {
searchResults.style.display = 'none';
searchSpinner.classList.add('hidden');
return;
}
searchSpinner.classList.remove('hidden');
// البحث الفوري في القائمة المحلية أولاً (أسرع وأكثر موثوقية)
const localResults = popularLocations.filter(loc =>
loc.name.toLowerCase().includes(query) || loc.sub.toLowerCase().includes(query)
);
if (localResults.length > 0) {
renderSearchResults(localResults);
searchSpinner.classList.add('hidden');
} else {
// إذا لم نجد، نحاول جلبها من API (مع User-Agent)
searchTimeout = setTimeout(async () => {
try {
const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=5&countrycodes=sy&accept-language=ar`, {
headers: { 'Accept': 'application/json' }
});
const data = await res.json();
if (data.length > 0) {
const formattedData = data.map(place => {
const nameParts = place.display_name.split(',');
return {
name: nameParts[0],
sub: nameParts.slice(1, 3).join(','),
lat: parseFloat(place.lat),
lng: parseFloat(place.lon)
};
});
renderSearchResults(formattedData);
} else {
searchResults.style.display = 'none';
}
} catch (err) {
console.error('Search error', err);
searchResults.style.display = 'none';
} finally {
searchSpinner.classList.add('hidden');
}
}, 600);
}
});
function renderSearchResults(results) {
searchResults.innerHTML = '';
results.forEach(place => {
const div = document.createElement('div');
div.className = 'search-result-item flex items-center gap-3';
div.innerHTML = `
${place.name}
${place.sub}
`;
div.onclick = () => selectDestination(place.lat, place.lng, place.name);
searchResults.appendChild(div);
});
searchResults.style.display = 'block';
}
// إخفاء النتائج عند النقر خارجاً
document.addEventListener('click', (e) => {
if (!dropoffInput.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
async function selectDestination(lat, lng, name) {
searchResults.style.display = 'none';
dropoffInput.value = name;
destination = { lat: parseFloat(lat), lng: parseFloat(lng), name };
if (destMarker) destMarker.remove();
const el = document.createElement('div');
el.innerHTML = `
`;
destMarker = new maplibregl.Marker({ element: el, anchor: 'bottom' })
.setLngLat([destination.lng, destination.lat])
.addTo(map);
// حساب السعر وعرض شاشة التأكيد
switchState(stateConfirm);
document.getElementById('confirm-dest-text').innerText = name;
// تقدير السعر الحقيقي عبر الباك إند
const priceEl = document.getElementById('price-estimate');
priceEl.innerHTML = `
جاري حساب التعرفة الحقيقية...
`;
try {
const url = `${API_BASE}/pricing/estimate?pickupLat=${userLocation[1]}&pickupLng=${userLocation[0]}&destinationLat=${destination.lat}&destinationLng=${destination.lng}`;
const res = await fetch(url);
const data = await res.json();
if (data && data.total !== undefined) {
priceEl.innerHTML = `
${data.distanceKm || 0} كم •
${data.durationMin || 0} دقيقة
${(data.total || 0).toLocaleString()}
ل.س
تعرفة أساسية ${(data.baseFare || 0).toLocaleString()} + المسافة والوقت
`;
} else {
throw new Error('Invalid data');
}
// رسم مسار حقيقي
drawRouteOSRM(userLocation, [destination.lng, destination.lat]);
} catch (e) {
console.error("Pricing Error:", e);
priceEl.innerHTML = `
خطأ في الاتصال بالخادم
سيتم تحديد السعر النهائي عند الطلب
`;
drawRouteOSRM(userLocation, [destination.lng, destination.lat]);
}
}
function backToSearch() {
switchState(stateSearch);
dropoffInput.value = '';
destination = null;
currentTrip = null;
if (destMarker) destMarker.remove();
if (driverMarker) { driverMarker.remove(); driverMarker = null; }
removePickupMarker();
if (map.getLayer('route')) map.removeLayer('route');
if (map.getSource('route')) map.removeSource('route');
map.flyTo({ center: userLocation, zoom: 15 });
}
// فك تشفير مسار Polyline الخاص بـ OSRM
function decodePolyline(str, precision) {
var index = 0, lat = 0, lng = 0, coordinates = [], shift = 0, result = 0, byte = null, latitude_change, longitude_change, factor = Math.pow(10, precision !== undefined ? precision : 5);
while (index < str.length) {
byte = null; shift = 0; result = 0;
do { byte = str.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20);
latitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1));
shift = result = 0;
do { byte = str.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20);
longitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1));
lat += latitude_change; lng += longitude_change;
coordinates.push([lng / factor, lat / factor]);
}
return coordinates;
}
let lastRouteDrawTime = 0;
async function drawRouteOSRM(start, end, fit = true) {
// منع الاستدعاء المتكرر (كل 5 ثوانٍ كحد أدنى عند fit=false)
const now = Date.now();
if (!fit && now - lastRouteDrawTime < 5000) return;
lastRouteDrawTime = now;
try {
const osrmUrl = `https://router.project-osrm.org/route/v1/driving/${start[0]},${start[1]};${end[0]},${end[1]}?overview=full`;
const res = await fetch(osrmUrl);
const data = await res.json();
let coordinates = [start, end];
if (data.routes && data.routes.length > 0) {
coordinates = decodePolyline(data.routes[0].geometry, 5);
}
const geojson = {
'type': 'Feature',
'properties': {},
'geometry': { 'type': 'LineString', 'coordinates': coordinates }
};
// تحديث المصدر إن وُجد بدلاً من إعادة الإنشاء
if (map.getSource('route')) {
map.getSource('route').setData(geojson);
} else {
map.addSource('route', { 'type': 'geojson', 'data': geojson });
map.addLayer({
'id': 'route',
'type': 'line',
'source': 'route',
'layout': { 'line-join': 'round', 'line-cap': 'round' },
'paint': { 'line-color': '#f97316', 'line-width': 3, 'line-opacity': 0.8 }
});
}
if (fit) {
const bounds = new maplibregl.LngLatBounds(start, start).extend(end);
map.fitBounds(bounds, { padding: 80 });
}
} catch (err) {
console.error("OSRM Error:", err);
}
}
// --- WebSocket (Socket.IO) & الرحلات ---
let tripStatusPoll = null;
function startTripPolling() {
stopTripPolling();
tripStatusPoll = setInterval(async () => {
if (!currentTrip) return stopTripPolling();
try {
const token = localStorage.getItem('rider_token');
const res = await fetch(`${API_BASE}/trips/${currentTrip.id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
if (!res.ok) return;
if (data.status === 'accepted' || data.status === 'driver_arrived' || data.status === 'started') {
stopTripPolling();
handleTripAccepted(data);
syncTripUIStatus(data.status);
} else if (data.status === 'cancelled') {
stopTripPolling();
alert('تم إلغاء الرحلة');
backToSearch();
}
} catch (e) { }
}, 4000);
}
function stopTripPolling() {
if (tripStatusPoll) { clearInterval(tripStatusPoll); tripStatusPoll = null; }
}
// أيقونة نقطة الركوب (تظهر على خريطة الراكب)
let pickupMarker = null;
function createPickupMarker() {
if (pickupMarker) return;
const pickupLng = currentTrip ? currentTrip.pickup_lng : userLocation[0];
const pickupLat = currentTrip ? currentTrip.pickup_lat : userLocation[1];
const el = document.createElement('div');
el.innerHTML = `
`;
pickupMarker = new maplibregl.Marker({ element: el, anchor: 'bottom' }).setLngLat([pickupLng, pickupLat]).addTo(map);
}
function removePickupMarker() {
if (pickupMarker) { pickupMarker.remove(); pickupMarker = null; }
}
function handleTripAccepted(data) {
// Ensure we are in the right UI panel
const arrivedPanel = document.getElementById('driver-arriving-state');
if (!arrivedPanel || arrivedPanel.classList.contains('hidden')) {
switchState(stateArriving);
fetchTripDetails();
}
// إذا البيانات فيها إحداثيات (من polling أو أحداث مجمعة) أضفها للرحلة
if (data && currentTrip) {
if (data.pickup_lat) currentTrip.pickup_lat = currentTrip.pickup_lat || data.pickup_lat;
if (data.pickup_lng) currentTrip.pickup_lng = currentTrip.pickup_lng || data.pickup_lng;
if (data.destination_lat) currentTrip.destination_lat = currentTrip.destination_lat || data.destination_lat;
if (data.destination_lng) currentTrip.destination_lng = currentTrip.destination_lng || data.destination_lng;
}
}
function syncTripUIStatus(status) {
const el = document.getElementById('trip-status-text');
if (!el) return;
if (status === 'accepted') {
el.style.color = '#f97316';
el.innerHTML = '🚗 الكابتن في الطريق إليك';
} else if (status === 'driver_arrived') {
el.style.color = '#3b82f6';
el.innerHTML = '📍 السائق وصل وينتظرك في الخارج!';
// Flash the panel to grab attention
const panel = document.getElementById('driver-arriving-state');
if (panel) {
panel.style.transition = 'background 0.3s';
panel.style.background = 'rgba(59,130,246,0.08)';
setTimeout(() => { panel.style.background = ''; }, 1500);
}
// Show a browser notification if supported
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('UperSyria', { body: 'السائق وصل وينتظرك! 📍', icon: '/favicon.ico' });
}
} else if (status === 'started') {
el.style.color = '#22c55e';
el.innerHTML = '🛣️ الرحلة بدأت — استمتع بالرحلة!';
const panel = document.getElementById('driver-arriving-state');
if (panel) {
panel.style.transition = 'background 0.3s';
panel.style.background = 'rgba(34,197,94,0.08)';
setTimeout(() => { panel.style.background = ''; }, 1500);
}
}
}
function initSocket() {
const token = localStorage.getItem('rider_token');
const socketUrl = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:3010/trips'
: 'https://eyadbasher-upersyria-backend.hf.space/trips';
socket = io(socketUrl, {
auth: { token: token },
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionDelay: 1000,
});
socket.on('connect', () => console.log('[Socket] Connected:', socket.id));
socket.on('connect_error', (e) => console.warn('[Socket] Error:', e.message));
// مسار 1: حدث مباشر للتحديثات (كان اسمه notifyDriverAccepted)
socket.on('trip:accepted', (data) => {
console.log('[Socket] trip:accepted direct:', data);
if (!currentTrip || data.trip_id !== currentTrip.id) return;
const newStatus = data.status || 'accepted';
currentTrip.status = newStatus; // تحديث محلي
stopTripPolling();
if (['accepted', 'driver_arrived', 'started'].includes(newStatus)) {
handleTripAccepted(data);
syncTripUIStatus(newStatus);
} else if (newStatus === 'completed' || newStatus === 'paid') {
showRatingModal(data);
}
});
// مسار 2: حدث عبر غرفة الرحلة
socket.on('trip:status_update', (data) => {
console.log('[Socket] trip:status_update:', data);
if (!currentTrip || data.trip_id !== currentTrip.id) return;
currentTrip.status = data.status; // Update local state
if (['accepted', 'driver_arrived', 'started'].includes(data.status)) {
stopTripPolling();
handleTripAccepted(data);
syncTripUIStatus(data.status);
} else if (data.status === 'completed' || data.status === 'paid') {
showRatingModal(data);
}
});
socket.on('trip:cancelled', (data) => {
if (currentTrip && currentTrip.id === data.trip_id) {
stopTripPolling();
alert('تم إلغاء الرحلة: ' + data.reason);
backToSearch();
}
});
socket.on('driver:location', (data) => {
if (!data || data.lat === undefined || data.lng === undefined) return;
if (!currentTrip) return; // Ignore driver location if no active trip
console.log('[Socket] driver:location', data.lat, data.lng, 'status:', currentTrip.status);
if (!map) return; // Map not ready yet
// إنشاء أو تحديث أيقونة السائق
if (!driverMarker) {
const el = document.createElement('div');
el.innerHTML = `
`;
driverMarker = new maplibregl.Marker({ element: el, anchor: 'center' }).setLngLat([data.lng, data.lat]).addTo(map);
} else {
driverMarker.setLngLat([data.lng, data.lat]);
}
// رسم المسار بناءً على حالة الرحلة
const st = currentTrip.status;
if (st === 'started') {
// الرحلة بدأت: مسار من السائق → الوجهة
removePickupMarker();
if (currentTrip.destination_lng && currentTrip.destination_lat) {
drawRouteOSRM([data.lng, data.lat], [Number(currentTrip.destination_lng), Number(currentTrip.destination_lat)], false);
}
} else if (['accepted', 'driver_arrived'].includes(st)) {
// السائق في الطريق: مسار من السائق → نقطة الركوب
createPickupMarker();
if (currentTrip.pickup_lng && currentTrip.pickup_lat) {
drawRouteOSRM([data.lng, data.lat], [Number(currentTrip.pickup_lng), Number(currentTrip.pickup_lat)], false);
}
}
});
}
document.getElementById('request-btn').addEventListener('click', async () => {
if (!destination) return;
switchState(stateFinding);
const token = localStorage.getItem('rider_token');
try {
const res = await fetch(`${API_BASE}/trips`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({
pickupLat: userLocation[1],
pickupLng: userLocation[0],
pickupAddress: 'موقعي الحالي',
destinationLat: destination.lat,
destinationLng: destination.lng,
destinationAddress: destination.name
})
});
if (res.status === 401) {
throw new Error('انتهت الجلسة أو ملف المستخدم غير مكتمل. حاول تسجيل الدخول مرة أخرى.');
}
const data = await res.json();
if (!res.ok) throw new Error(data.message);
currentTrip = data.trip;
// الانضمام لغرفة الرحلة في السوكيت
socket.emit('trip:subscribe', { trip_id: currentTrip.id, role: 'rider' });
// Polling fallback: يتحقق كل 4 ثوان من حالة الرحلة
startTripPolling();
} catch (err) {
alert('حدث خطأ أثناء الطلب: ' + err.message);
switchState(stateConfirm);
}
});
async function cancelTrip() {
if (!currentTrip) return;
const token = localStorage.getItem('rider_token');
try {
await fetch(`${API_BASE}/trips/${currentTrip.id}/cancel`, {
method: 'PATCH',
headers: { 'Authorization': `Bearer ${token}` }
});
alert('تم إلغاء الطلب بنجاح');
backToSearch();
} catch (err) {
console.error(err);
backToSearch();
}
}
async function fetchTripDetails() {
if (!currentTrip) return;
const token = localStorage.getItem('rider_token');
try {
const res = await fetch(`${API_BASE}/trips/${currentTrip.id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
console.log('[DEBUG] Trip details response:', JSON.stringify(data).substring(0, 300));
// Backend returns driver: { user: { name, phone }, vehicle: {...} }
// or driver_user: { name, phone } depending on schema
const driverUser = (data.driver && data.driver.user) ? data.driver.user
: (data.driver_user ? data.driver_user : null);
if (driverUser) {
document.getElementById('driver-name').innerText = driverUser.name || 'كابتن';
// Save driver phone for call button
driverPhone = driverUser.phone || null;
console.log('[DEBUG] driverPhone set to:', driverPhone);
}
if (data.driver && data.driver.vehicle) {
document.getElementById('driver-car').innerText = `${data.driver.vehicle.make || ''} ${data.driver.vehicle.model || ''}`.trim();
document.getElementById('driver-plate').innerText = data.driver.vehicle.plate || '';
}
// Setup chat (only once - listener dedup handled inside)
setupRiderChatListeners();
} catch (e) { console.error('[fetchTripDetails error]', e); }
}
// ==========================================
// نظام التقييم والإيصال - الزبون
// ==========================================
let selectedRating = 5;
async function showRatingModal(data) {
selectedRating = 5;
document.getElementById('rating-modal').classList.remove('hidden');
// عرض السعر النهائي — إذا لم يكن في بيانات السوكيت، نجلبه من الباك إند
let fare = data && data.fare ? data.fare : null;
let km = data && data.distance_km ? data.distance_km : null;
let min = data && data.duration_min ? data.duration_min : null;
if (!fare && currentTrip) {
try {
const token = localStorage.getItem('rider_token');
const res = await fetch(`${API_BASE}/trips/${currentTrip.id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const tripData = await res.json();
fare = tripData.fare || tripData.estimated_fare || null;
km = tripData.distance_km || null;
} catch (e) { console.warn('Could not fetch fare details'); }
}
if (fare) {
document.getElementById('final-fare-display').innerText = `${Number(fare).toLocaleString()} ل.س`;
}
if (km !== null) document.getElementById('final-km').innerText = Number(km).toFixed(1);
if (min !== null) document.getElementById('final-min').innerText = Math.round(min);
setRating(5);
}
function setRating(val) {
selectedRating = val;
const btns = document.querySelectorAll('#star-rating .star-btn');
btns.forEach((btn, i) => {
if (i < val) btn.classList.remove('grayscale', 'opacity-30');
else btn.classList.add('grayscale', 'opacity-30');
});
}
async function submitRating() {
if (!currentTrip) return skipRating();
const comment = document.getElementById('rating-comment').value.trim();
const token = localStorage.getItem('rider_token');
try {
await fetch(`${API_BASE}/trips/${currentTrip.id}/rate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ rating: selectedRating, comment })
});
} catch (e) { console.log('Rating failed'); }
skipRating();
}
function skipRating() {
document.getElementById('rating-modal').classList.add('hidden');
currentTrip = null;
if (driverMarker) { driverMarker.remove(); driverMarker = null; }
removePickupMarker();
if (destMarker) { destMarker.remove(); destMarker = null; }
if (map.getLayer('route')) map.removeLayer('route');
if (map.getSource('route')) map.removeSource('route');
switchState(stateSearch);
map.flyTo({ center: userLocation, zoom: 15 });
}
// ==========================================
// زر الطوارئ SOS
// ==========================================
function triggerSOS() {
const confirmed = confirm('🆘 هل تريد الاتصال بالطوارئ؟\n\nاضغط موافق للاتصال بـ 110');
if (confirmed) window.location.href = 'tel:110';
}
async function fetchTripHistory() {
const token = localStorage.getItem('rider_token');
if (!token) return;
try {
const res = await fetch(`${API_BASE}/trips/my-trips`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
const list = document.getElementById('history-list');
list.innerHTML = '';
if (res.status === 401) {
list.innerHTML = 'الجلسة بحاجة لإعادة تسجيل الدخول
';
return;
}
if (!res.ok) {
list.innerHTML = `${data.message || 'فشل تحميل التاريخ'}
`;
return;
}
if (!Array.isArray(data) || data.length === 0) {
list.innerHTML = 'لا توجد رحلات سابقة
';
return;
}
data.forEach(trip => {
const date = new Date(trip.created_at).toLocaleDateString('ar-SY', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
let statusHtml = '';
if (trip.status === 'completed' || trip.status === 'paid') statusHtml = 'مكتملة';
else if (trip.status === 'cancelled') statusHtml = 'ملغاة';
else statusHtml = `${trip.status}`;
const html = `
${trip.destination_address}
الكابتن: ${trip.driver?.name || '-'}
${trip.fare ? trip.fare.toLocaleString() : '0'} ل.س
`;
list.insertAdjacentHTML('beforeend', html);
});
} catch (err) {
console.error('Error fetching history:', err);
}
}
function openHistoryModal() {
document.getElementById('history-modal').classList.remove('hidden');
fetchTripHistory();
}
// ===== نظام الاتصال والرسائل - الزبون =====
let driverPhone = null;
let riderUserId = null;
let riderChatUnread = 0;
function getRiderUserId() {
try {
const token = localStorage.getItem('rider_token');
if (!token) return null;
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.sub;
} catch { return null; }
}
function callDriver() {
if (!driverPhone) { alert('رقم الكابتن غير متاح بعد'); return; }
const clean = driverPhone.replace(/\s+/g, '');
window.location.href = `tel:${clean}`;
}
function openRiderChat() {
document.getElementById('rider-chat-modal').classList.remove('hidden');
riderChatUnread = 0;
const badge = document.getElementById('rider-chat-badge');
badge.classList.add('hidden');
badge.innerText = '';
document.getElementById('rider-chat-input').focus();
}
function closeRiderChat() {
document.getElementById('rider-chat-modal').classList.add('hidden');
}
function appendRiderChatMessage(message, isMe, time) {
const container = document.getElementById('rider-chat-messages');
const timeStr = time ? new Date(time).toLocaleTimeString('ar-SY', { hour: '2-digit', minute: '2-digit' }) : '';
const html = isMe
? ``
: ``;
container.insertAdjacentHTML('beforeend', html);
container.scrollTop = container.scrollHeight;
}
function sendRiderMessage() {
const input = document.getElementById('rider-chat-input');
const msg = input.value.trim();
if (!msg || !currentTrip || !socket) return;
socket.emit('trip:message', { trip_id: currentTrip.id, message: msg });
appendRiderChatMessage(msg, true, new Date().toISOString());
input.value = '';
}
function setupRiderChatListeners() {
if (!socket) return;
riderUserId = getRiderUserId();
// Remove any previous listener to prevent duplicate messages
socket.off('trip:message');
socket.on('trip:message', (data) => {
const isMe = data.sender_id === riderUserId;
appendRiderChatMessage(data.message, isMe, data.created_at);
if (!isMe && document.getElementById('rider-chat-modal').classList.contains('hidden')) {
riderChatUnread++;
const badge = document.getElementById('rider-chat-badge');
badge.classList.remove('hidden');
badge.innerText = riderChatUnread > 9 ? '9+' : riderChatUnread;
}
});
}
// ===== COMPLAINTS =====
async function openComplaintsModal() {
document.getElementById('complaints-modal').classList.remove('hidden');
document.getElementById('complaint-success').classList.add('hidden');
document.getElementById('complaint-text').value = '';
await loadUserComplaints();
}
async function loadUserComplaints() {
const token = localStorage.getItem('rider_token');
if (!token) return;
const listEl = document.getElementById('complaints-list');
listEl.innerHTML = 'جاري التحميل...
';
try {
const res = await fetch(`${API_BASE}/complaints/my`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const complaints = await res.json();
if (!Array.isArray(complaints) || complaints.length === 0) {
listEl.innerHTML = 'لا توجد شكاوى سابقة
';
return;
}
const statusColors = { open: '#f59e0b', in_review: '#3b82f6', resolved: '#22c55e', closed: '#94a3b8' };
const statusLabels = { open: 'مفتوحة', in_review: 'قيد المراجعة', resolved: 'محلولة', closed: 'مغلقة' };
const typeLabels = { driver_behavior: 'سلوك السائق', route: 'مسار أو رحلة', payment: 'دفع', app_issue: 'مشكلة تطبيق', other: 'أخرى' };
listEl.innerHTML = complaints.map(c => `
${typeLabels[c.type] || c.type}
${statusLabels[c.status] || c.status}
${c.description}
${c.resolution ? `
↩ رد الإدارة: ${c.resolution}
` : ''}
${new Date(c.created_at).toLocaleDateString('ar-SY')}
`).join('');
} catch {
listEl.innerHTML = 'تعذّر تحميل الشكاوى
';
}
}
async function submitComplaint() {
const token = localStorage.getItem('rider_token');
if (!token) { alert('يجب تسجيل الدخول أولاً'); return; }
const type = document.getElementById('complaint-type').value;
const description = document.getElementById('complaint-text').value.trim();
if (!description) { alert('يرجى كتابة تفاصيل الشكوى'); return; }
try {
const res = await fetch(`${API_BASE}/complaints`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ type, description })
});
if (!res.ok) throw new Error();
document.getElementById('complaint-success').classList.remove('hidden');
document.getElementById('complaint-text').value = '';
setTimeout(() => {
document.getElementById('complaint-success').classList.add('hidden');
loadUserComplaints();
}, 2500);
} catch {
alert('حدث خطأ أثناء الإرسال، حاول مجدداً');
}
}