يوبر سوريا

رحلتك الآمنة تبدأ من هنا

تسجيل دخول سريع وآمن بنقرة واحدة

أو عبر الهاتف
// --- الإعدادات الأساسية الديناميكية --- 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 = `
${date}
${statusHtml}
${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 ? `
${message}
${timeStr}
` : `
${message}
${timeStr}
`; 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('حدث خطأ أثناء الإرسال، حاول مجدداً'); } }