Master Taxi
...
Alloh sizni o'z panohida asrasin!
📱 M TAXIni telefoningizga o'rnating
🚗

Shahar va sana kiriting

Ҳайдовчи
Telefon raqam
📋

Telefon raqamingizni kiriting

Admin kirish
${hasMail?``:''}" >★` ).join(''); if (!isDriver) { return `
${fromD}${toD}
${fmtPrice(r.price)} 🪑 ${seats} joy ${starsHtml} ${rat.toFixed(1)}
📞 ${fmtPhone(r.phone)} ${hasMail?``:''}
${seats>0 ?`` :`To'lgan`}
`; } // Driver's own ride card const statusDot = r.status==='completed' ? '🏁 Yakunlangan' : '🟢 Faol'; return `
${r.from}${r.to}
${statusDot}
🪑 ${seats}/${r.seats} ${fmtPrice(r.price)}
`; } // ═══════════════════════════════════════ // BOOKING MODAL // ═══════════════════════════════════════ function openBookModal(rideId) { const r = S.rides.find(x=>x.id===rideId); if (!r) return; const seats = parseInt(r.availableSeats)||0; const saved = localStorage.getItem('mtaxi_lastPhone')||''; openModal('Joy bron qilish', `
${cdisplay(r.from)} → ${cdisplay(r.to)} · ${fmtTime(r.time)}
📞 Qo'ng'iroq
`); } async function submitBook(rideId, maxS) { const phone = normalizePhone(document.getElementById('bkPhone').value); const seats = parseInt(document.getElementById('bkSeats').value)||1; if (!phone) { toast('Telefon kiriting','error'); return; } if (seats>maxS) { toast(`Max ${maxS} joy`,'error'); return; } localStorage.setItem('mtaxi_lastPhone', phone); try { const res = await apiPost({ action:'bookRide', rideId, phone, seats }); S.pending[res.bookingId] = { rideId, phone, seats }; localStorage.setItem('mtaxi_pending', JSON.stringify(S.pending)); closeModal(); toast("Bron so'rovi yuborildi! Natija 15 soniyada ko'rinadi",'success'); } catch(e) { toast(e.message,'error'); } } // ═══════════════════════════════════════ // POLLING // ═══════════════════════════════════════ function startPoll() { if (S.pollTimer) clearInterval(S.pollTimer); S.pollTimer = setInterval(poll, CFG.POLL_MS); } async function poll() { if (!S.online) return; // Check pending bookings for (const [bkId, bk] of Object.entries(S.pending)) { try { const res = await apiGet({ action:'getBookingStatus', bookingId:bkId }); if (res.status==='accepted') { openModal('✅ Bron qabul qilindi!', `
🎉

Sizning broningiz qabul qilindi!

${bk.seats} ta joy band qilindi

`); delete S.pending[bkId]; localStorage.setItem('mtaxi_pending', JSON.stringify(S.pending)); } else if (res.status==='rejected') { openModal('❌ Bron rad etildi', `
😔

Afsuski, bron rad etildi

Boshqa reysni sinab ko'ring

`); delete S.pending[bkId]; localStorage.setItem('mtaxi_pending', JSON.stringify(S.pending)); } } catch {} } // Refresh rides list if searching const from = document.getElementById('sfrom').value; const to = document.getElementById('sto').value; if (document.getElementById('tabPassenger').classList.contains('active') && (from||to)) { try { const rides = await apiGet({ action:'getRides', from, to, date:S.sDate }); S.rides = rides; renderRides(rides, document.getElementById('ridesList')); } catch {} } // Refresh driver bookings if on driver tab if (document.getElementById('tabDriver').classList.contains('active') && S.driver) { for (const r of S.myRides) { if (document.getElementById('bklist-'+r.id)) loadRideBookings(r.id); } } } // ═══════════════════════════════════════ // DRIVER // ═══════════════════════════════════════ function loginDriver() { const phone = normalizePhone(document.getElementById('drvPhone').value); const name = document.getElementById('drvName').value.trim(); if (!phone || !name) { toast('Telefon va ism kiriting','error'); return; } S.driver = { phone, name }; localStorage.setItem('mtaxi_driver', JSON.stringify(S.driver)); updateDriverUI(); loadDriverPanel(); } function logoutDriver() { S.driver = null; localStorage.removeItem('mtaxi_driver'); updateDriverUI(); } function updateDriverUI() { const af = document.getElementById('driverAuthForm'); const lb = document.getElementById('driverLoggedBadge'); const dp = document.getElementById('driverPanel'); if (S.driver) { af.style.display='none'; lb.style.display='block'; dp.style.display='block'; document.getElementById('drvBadgeName').textContent = S.driver.name; document.getElementById('drvBadgePhone').textContent = S.driver.phone; } else { af.style.display='block'; lb.style.display='none'; dp.style.display='none'; } } function toggleAddRide() { const f = document.getElementById('addRideForm'); if (f.classList.contains('closed')) { f.style.maxHeight = f.scrollHeight + 400 + 'px'; // extra for suggestions f.classList.remove('closed'); if (!document.getElementById('rdate').value) document.getElementById('rdate').value = today(); } else { f.style.maxHeight = '0'; f.classList.add('closed'); } } async function submitRide() { if (!S.driver) { toast('Avval kiring','error'); return; } const data = { action:'addRide', driverName: S.driver.name, phone: S.driver.phone, from: document.getElementById('rfrom').value.trim(), to: document.getElementById('rto').value.trim(), date: document.getElementById('rdate').value, time: document.getElementById('rtime').value, seats: document.getElementById('rseats').value, price: document.getElementById('rprice').value, plateNumber: document.getElementById('rplate').value.trim(), acceptMail: document.getElementById('rmail').checked, }; if (!data.from||!data.to||!data.date||!data.seats||!data.price) { toast("Barcha maydonlarni to'ldiring",'error'); return; } try { await apiPost(data); const _pl = document.getElementById('rplate').value.trim(); if (_pl) localStorage.setItem('mtaxi_plate', _pl); toast("Reys muvaffaqiyatli qo'shildi!",'success'); toggleAddRide(); ['rfrom','rto','rseats','rprice','rplate'].forEach(id => document.getElementById(id).value=''); document.getElementById('rmail').checked=false; loadDriverPanel(); } catch(e) { toast(e.message,'error'); } } async function loadDriverPanel() { if (!S.driver) return; const el = document.getElementById('myRidesList'); el.innerHTML = '
'; try { const _dk = `drv:${S.driver.phone}`; const rides = cacheGet(_dk, 30000) || await apiGet({ action:'getDriverRides', phone:S.driver.phone }); cacheSet(_dk, rides); S.myRides = rides; if (!rides||!rides.length) { el.innerHTML='
🚗

Reyslar yo\'q

'; return; } el.innerHTML = rides.map(r => rideCardHtml(r, true)).join(''); rides.forEach(r => loadRideBookings(r.id)); } catch(e) { el.innerHTML=`
⚠️

${e.message}

`; } } async function handleBk(bkId, rideId, status) { try { await apiPost({ action:'updateBooking', bookingId:bkId, status }); toast(status==='accepted'?'Qabul qilindi!':'Rad etildi', status==='accepted'?'success':'error'); loadRideBookings(rideId); loadDriverPanel(); } catch(e) { toast(e.message,'error'); } } // ═══════════════════════════════════════ // RATING // ═══════════════════════════════════════ function hvrStars(n, id) { document.getElementById(id)?.querySelectorAll('.star').forEach((s,i) => s.classList.toggle('hover',i s.classList.remove('hover')); } // ═══════════════════════════════════════ // HISTORY // ═══════════════════════════════════════ async function loadHistory() { const phone = normalizePhone(document.getElementById('histPhone').value); if (!phone) return; localStorage.setItem('mtaxi_lastPhone', phone); const el = document.getElementById('historyList'); el.innerHTML = '
'; try { const _hk = `hist:${phone}`; const bks = cacheGet(_hk, 60000) || await apiGet({ action:'getPassengerHistory', phone }); cacheSet(_hk, bks); if (!bks||!bks.length) { el.innerHTML='
📋

Tarix bo\'sh

'; return; } el.innerHTML = bks.map(b => `
${cdisplay(b.from)}${cdisplay(b.to)}
${ b.status==='pending'?'Kutilmoqda':b.status==='accepted'?'✅ Qabul':'❌ Rad' } ${b.seats} joy
${b.status==='accepted' && !b.rated ? `
Haydovchini baholang:
${interactiveStars(b.rideId)} ` : b.status==='accepted' && b.rated ? `
Bahoyingiz: ${ Array.from({length:5},(_,i)=>``).join('') }
` : ''}
`).join(''); } catch(e) { el.innerHTML=`
⚠️

${e.message}

`; } } // ═══════════════════════════════════════ // ADMIN // ═══════════════════════════════════════ function adminLogin() { if (document.getElementById('adminPass').value === CFG.ADMIN_PW) { S.adminOk = true; document.getElementById('adminLoginCard').style.display='none'; document.getElementById('adminPanel').style.display='block'; loadAdminStats(); } else { toast("Noto'g'ri parol",'error'); } } async function getDailyReport() { const btn = document.getElementById('btnReport'); btn.disabled=true; try { const r = await apiGet({ action:'getDailyReport' }); const pre = document.getElementById('reportPre'); pre.style.display='block'; pre.textContent = `📊 Kunlik hisobot 📅 ${new Date().toLocaleDateString('uz-UZ')} 🚗 Jami reyslar: ${r.totalRides||0} ✅ Faol: ${r.activeRides||0} 🏁 Yakunlangan: ${r.completedRides||0} 📬 Bronlar jami: ${r.totalBookings||0} ✅ Qabul: ${r.acceptedBookings||0} ❌ Rad: ${r.rejectedBookings||0} ⏳ Kutilmoqda: ${r.pendingBookings||0} ⭐ O'rtacha reyting: ${(r.avgRating||0).toFixed(2)}`; } catch(e) { toast(e.message,'error'); } btn.disabled=false; } async function sendTelegram() { try { await apiPost({ action:'sendDailyReport' }); toast("Telegram'ga yuborildi! 📨",'success'); } catch(e) { toast(e.message,'error'); } } async function cleanRides(days) { const msg = days===0 ? 'Barcha reyslarni' : `${days} kundan eski reyslarni`; if (!confirm(`${msg} o'chirishni tasdiqlaysizmi?`)) return; try { const r = await apiPost({ action:'cleanOldRides', days }); toast(`${r.deleted||0} ta reys o'chirildi`,'success'); loadAdminStats(); } catch(e) { toast(e.message,'error'); } } function clearCache() { const keys = Object.keys(localStorage).filter(k => k.startsWith('mtx_')); keys.forEach(k => localStorage.removeItem(k)); toast(`${keys.length} ta kesh o'chirildi`,'success'); } async function exportLogs() { try { const logs = await apiGet({ action:'getLogs' }); const csv = 'Vaqt,Harakat,Ma\'lumot\n'+(logs||[]).map(l=>`${l.timestamp},${l.action},"${(l.data||'').replace(/"/g,'""')}"`).join('\n'); const a = document.createElement('a'); a.href = 'data:text/csv;charset=utf-8,'+encodeURIComponent(csv); a.download = `mtaxi_logs_${today()}.csv`; a.click(); } catch(e) { toast(e.message,'error'); } } async function loadAdminStats() { const el = document.getElementById('adminStats'); if (!el) return; try { const r = await apiGet({ action:'getDailyReport' }); el.innerHTML = [ ['🚗','Jami reyslar', r.totalRides||0], ['✅','Faol reyslar', r.activeRides||0], ['📬','Bronlar', r.totalBookings||0], ['⭐','O\'rt. reyting',(r.avgRating||0).toFixed(1)], ].map(([ic,lb,v])=>`
${ic}
${v}
${lb}
`).join(''); } catch(e) { el.innerHTML=`
${e.message}
`; } } // ═══════════════════════════════════════ // MODAL // ═══════════════════════════════════════ function openModal(title, body) { document.getElementById('modalTitle').textContent = title; document.getElementById('modalBody').innerHTML = body; document.getElementById('modalBg').classList.add('show'); } function closeModal() { document.getElementById('modalBg').classList.remove('show'); } // ═══════════════════════════════════════ // TOAST // ═══════════════════════════════════════ function toast(msg, type='info') { const wrap = document.getElementById('toastWrap'); const t = document.createElement('div'); t.className = `toast ${type}`; t.textContent = msg; wrap.appendChild(t); setTimeout(() => t.remove(), 4000); } // ═══════════════════════════════════════ // PWA // ═══════════════════════════════════════ window.addEventListener('beforeinstallprompt', e => { e.preventDefault(); S.pwaPrompt = e; document.getElementById('installBanner').classList.add('show'); }); function installPWA() { if (S.pwaPrompt) { S.pwaPrompt.prompt(); S.pwaPrompt.userChoice.then(() => { document.getElementById('installBanner').classList.remove('show'); S.pwaPrompt = null; }); } } if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch(()=>{}); } // ═══════════════════════════════════════ // INIT // ═══════════════════════════════════════ (function init() { setConn(navigator.onLine); applyFontSize(); const inp = document.getElementById('searchDateInp'); inp.value = today(); inp.min = today(); if (S.driver) updateDriverUI(); const lp = localStorage.getItem('mtaxi_lastPhone'); if (lp) document.getElementById('histPhone').value = lp; startPoll(); })();