호치민 기사 포함 차량 렌트 – 공항 픽업·장거리 이동 24/7
저희의 사명입니다!”
예약 완료
—
여행을 확인할 수 있도록 연락처 정보를 입력해 주세요.
(function(){"use strict";
var API_URL="/airport/api/booking-api.php";
var sPickupEl=document.getElementById("sPickup");var sDropoffEl=document.getElementById("sDropoff");var distanceEl=document.getElementById("distanceInfo");
var taxiVehicleTypesCache=[];callApi("taxiListActiveVehicleTypes").then(function(res){if(res&&res.ok&&Array.isArray(res.data)){taxiVehicleTypesCache=res.data}}).catch(function(){});
var lastQuoteRows=[];function getEstimatedPriceForVehicle(vehicleType){if(!lastQuoteRows.length)return null;var match=lastQuoteRows.find(function(r){return r.vehicle_type===vehicleType});return match?Number(match.price):Number(lastQuoteRows[0].price)}
function hideDistanceInfo(){distanceEl.style.display="none";distanceEl.classList.remove("loading");distanceEl.innerHTML="";lastQuoteRows=[]} function showDistanceLoading(){distanceEl.style.display="flex";distanceEl.classList.add("loading");distanceEl.innerHTML="Đang tra giá tuyến..."} function showDistanceResult(rows){distanceEl.style.display="flex";distanceEl.classList.remove("loading");lastQuoteRows=rows;var best=rows[0];var icon='';var html=icon+"Giá tham khảo "+esc(best.from_location)+" → "+esc(best.to_location)+" ("+esc(best.vehicle_type)+"): "+Number(best.price).toLocaleString("vi-VN")+"đ";if(best.distance_km){html+=" · ~"+esc(String(best.distance_km))+" km"} if(rows.length>1){html+=" (và "+(rows.length-1)+" loại xe khác)"} distanceEl.innerHTML=html} function showDistanceError(msg){distanceEl.style.display="flex";distanceEl.classList.add("loading");distanceEl.innerHTML=esc(msg)} function showNoListedPrice(msg){distanceEl.style.display="flex";distanceEl.classList.remove("loading");distanceEl.innerHTML=esc(msg||"Chưa có giá niêm yết cho tuyến này, đội ngũ SUNTRAVEL247 sẽ báo giá trực tiếp sau khi anh/chị đặt xe.")} function routeAddressParts(label){var parts=String(label||"").split(",").map(function(x){return x.trim()}).filter(Boolean);return{main:parts.slice(0,2).join(", ")||label,sub:parts.slice(2).join(", ")}} function routeCardHtml(from,to,info,note){var a=routeAddressParts(from),b=routeAddressParts(to);var mins=info.duration_minutes?Math.round(info.duration_minutes):0;var h=Math.floor(mins/60),m=mins%60;var humanTime=mins?((h?h+" giờ ":"")+(m?m+" phút":"")):"—";var roundTrip=note&&/khứ hồi/i.test(note);return'
'+'
'+'
'+'
'+'
'+'
'+'
'+'
'+(isAirportPriced()?"":'
')+'
'} function showDistanceEstimated(from,to,info,note){distanceEl.style.display="block";distanceEl.classList.remove("loading");distanceEl.innerHTML=routeCardHtml(from,to,info,note);lastQuoteRows=[];
if(isAirportPriced())return;
var selectedVehicle=document.getElementById("sVehicle").value;var requestKey=from+"|"+to;callApi("taxiQuoteAllVehicles",{distanceKm:info.distance_km}).then(function(res){var slot=document.getElementById("distancePriceSlot");if(!slot)return;if(!res||!res.ok||!Array.isArray(res.data)||!res.data.length){slot.textContent="Chưa có giá niêm yết cho tuyến này, đội ngũ SUNTRAVEL247 sẽ báo giá trực tiếp.";return} var rows=res.data.map(function(r){return{vehicle_type:r.vehicle_type,vehicle_type_id:r.vehicle_type_id,price:r.price,distance_km:info.distance_km.toFixed(1),duration_minutes:info.duration_minutes,breakdown:r.breakdown}});lastQuoteRows=rows;var best=rows.find(function(r){return r.vehicle_type===selectedVehicle})||rows[0];var t="Giá tham khảo ("+esc(best.vehicle_type)+"): "+Number(best.price).toLocaleString("vi-VN")+"đ";if(rows.length>1){t+=" (và "+(rows.length-1)+" loại xe khác)"} slot.innerHTML=t}).catch(function(){var slot=document.getElementById("distancePriceSlot");if(slot)slot.textContent="Chưa có giá niêm yết cho tuyến này, đội ngũ SUNTRAVEL247 sẽ báo giá trực tiếp."})}
var geocodeCache={};var addrConfirmedPoints={};function geocodeVN(query){query=String(query||"").trim();var key=query.toLowerCase();if(addrConfirmedPoints[key])return Promise.resolve(addrConfirmedPoints[key]);if(geocodeCache[key]!==undefined)return Promise.resolve(geocodeCache[key]);return callApi("mapsGeocode",{address:query}).then(function(res){var pt=(res&&res.ok&&res.data)?{lat:res.data.lat,lon:res.data.lng}:null;geocodeCache[key]=pt;return pt}).catch(function(){geocodeCache[key]=null;return null})}
var addrSuggestCache={};var addrSuggestSeq=0;
function normalizeVN(s){return String(s||"").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/đ/g,"d").replace(/\s+/g," ").trim()}
function highlightMatch(text,query){text=String(text||"");var q=String(query||"").trim();if(!q)return esc(text);var nText=normalizeVN(text),nQ=normalizeVN(q);var pos=nText.indexOf(nQ);if(pos<0)return esc(text);return esc(text.slice(0,pos))+""+esc(text.slice(pos,pos+q.length))+""+esc(text.slice(pos+q.length))}
function searchAddressSuggestions(query){var key=normalizeVN(query);if(addrSuggestCache[key]!==undefined)return Promise.resolve(addrSuggestCache[key]);
return callApi("mapsAutocomplete",{input:query}).then(function(res){var list=(res&&res.ok&&Array.isArray(res.data))?res.data.map(function(it){return{label:it.description,main:it.main,sub:it.sub,placeId:it.placeId}}):[];addrSuggestCache[key]=list.slice(0,6);return addrSuggestCache[key]}).catch(function(){return[]})}
function setupAddressAutocomplete(inputEl,suggestEl){var debounceTimer=null;var currentItems=[];var activeIndex=-1;var suppressNextInput=!1;var requestId=0;
function closeList(){suggestEl.classList.remove("open");suggestEl.innerHTML="";currentItems=[];activeIndex=-1}
function updateActive(){Array.prototype.forEach.call(suggestEl.querySelectorAll(".as-item"),function(el,i){el.classList.toggle("active",i===activeIndex);if(i===activeIndex)el.scrollIntoView({block:"nearest"})})}
function renderLoading(){suggestEl.innerHTML='
';suggestEl.classList.add("open")}
function renderList(items,query){currentItems=items;activeIndex=-1;if(!items.length){suggestEl.innerHTML='
'+'
';suggestEl.classList.add("open");return}
var pin='';suggestEl.innerHTML=items.map(function(it,i){return'
'+'
'}).join("")+'
';suggestEl.classList.add("open")}
function chooseItem(item){inputEl.value=item.label;inputEl.dataset.selectedLabel=item.label;closeList();suppressNextInput=!0;inputEl.dispatchEvent(new Event("input",{bubbles:!0}));inputEl.dispatchEvent(new Event("change",{bubbles:!0}));
if(item.placeId){callApi("mapsPlaceDetail",{placeId:item.placeId}).then(function(res){if(res&&res.ok&&res.data){var key=item.label.trim().toLowerCase();addrConfirmedPoints[key]={lat:res.data.lat,lon:res.data.lng};if(inputEl.dataset.selectedLabel===item.label){inputEl.dataset.selectedLat=String(res.data.lat);inputEl.dataset.selectedLon=String(res.data.lng)}}}).catch(function(){})}}
suggestEl.addEventListener("mousedown",function(e){e.preventDefault();var row=e.target.closest(".as-item[data-idx]");if(!row)return;var idx=parseInt(row.dataset.idx,10);if(currentItems[idx])chooseItem(currentItems[idx]);});
inputEl.addEventListener("focus",function(){var q=inputEl.value.trim();if(q.length>=2&¤tItems.length)renderList(currentItems,q);});
inputEl.addEventListener("input",function(){if(suppressNextInput){suppressNextInput=!1;return}
if(inputEl.dataset.selectedLabel&&inputEl.value!==inputEl.dataset.selectedLabel){delete inputEl.dataset.selectedLat;delete inputEl.dataset.selectedLon;delete inputEl.dataset.selectedLabel}
clearTimeout(debounceTimer);var q=inputEl.value.trim();if(q.length<2){closeList();return} renderLoading();var myId=++requestId;debounceTimer=setTimeout(function(){searchAddressSuggestions(q).then(function(items){if(myId!==requestId||inputEl.value.trim()!==q)return;renderList(items,q)})},280)});inputEl.addEventListener("keydown",function(e){if(!suggestEl.classList.contains("open"))return;if(e.key==="ArrowDown"){e.preventDefault();activeIndex=Math.min(activeIndex+1,currentItems.length-1);updateActive()}else if(e.key==="ArrowUp"){e.preventDefault();activeIndex=Math.max(activeIndex-1,0);updateActive()}else if(e.key==="Enter"){if(activeIndex>=0&¤tItems[activeIndex]){e.preventDefault();chooseItem(currentItems[activeIndex])}else if(currentItems.length===1){e.preventDefault();chooseItem(currentItems[0])}}else if(e.key==="Escape"){closeList()}});
inputEl.addEventListener("blur",function(){setTimeout(closeList,150)})}
setupAddressAutocomplete(sPickupEl,document.getElementById("sPickupSuggest"));setupAddressAutocomplete(sDropoffEl,document.getElementById("sDropoffSuggest"));function goongDistanceKm(a,b){return callApi("mapsDistance",{fromLat:a.lat,fromLon:a.lon,toLat:b.lat,toLon:b.lon}).then(function(res){if(!res||!res.ok||!res.data)return null;return{distance_km:res.data.distance_km,duration_minutes:res.data.duration_minutes}}).catch(function(){return null})}
function todayStr(){var d=new Date();return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0")} var pickupDateEl=document.getElementById("sDate");pickupDateEl.min=todayStr();pickupDateEl.value=todayStr();
var payosEnabledCache=null;async function setupPayButton(code,phone,price){var box=document.getElementById("successPayBox");if(!box)return;box.style.display="none";var er=document.getElementById("successPayErr");if(er)er.style.display="none";if(!(Number(price)>=2000))return;try{if(payosEnabledCache===null){var c=await callApi("payosConfig",{});payosEnabledCache=!!c.enabled}}catch(e){payosEnabledCache=!1} if(!payosEnabledCache)return;var btn=document.getElementById("successPayBtn");btn.dataset.code=code;btn.dataset.phone=phone;box.style.display=""} document.addEventListener("click",async function(ev){var btn=ev.target&&ev.target.closest?ev.target.closest("#successPayBtn"):null;if(!btn||btn.disabled)return;var err=document.getElementById("successPayErr");err.style.display="none";var label=btn.textContent;btn.disabled=!0;btn.textContent="⏳ ...";try{var r=await callApi("payosCreatePayment",{trackingCode:btn.dataset.code,phone:btn.dataset.phone});window.location.href=r.checkoutUrl}catch(e){err.textContent=e.message;err.style.display="";btn.disabled=!1;btn.textContent=label}});
async function callApi(action,payload){payload=payload||{};payload.action=action;var res;try{res=await fetch(API_URL,{method:"POST",headers:{"Content-Type":"text/plain;charset=utf-8"},body:JSON.stringify(payload)})}catch(networkErr){throw new Error("Không thể kết nối máy chủ. Vui lòng kiểm tra kết nối mạng và thử lại.")} var json;try{json=await res.json()}catch(e){throw new Error("Máy chủ đang bận, vui lòng thử lại sau ít phút.")} if(!json.ok)throw new Error(json.message||"Có lỗi xảy ra, vui lòng thử lại.");return json}
function formatVnd(n){n=Number(n);if(!n)return"";return new Intl.NumberFormat("vi-VN").format(n)+"đ"}
var currentService="mot_chieu";var serviceTabs=document.getElementById("serviceTabs");var returnDateWrap=document.getElementById("sReturnDateWrap");var returnTimeWrap=document.getElementById("sReturnTimeWrap");var airportDirWrap=document.getElementById("sAirportDirWrap");var flightWrap=document.getElementById("sFlightWrap");var airportDirSelect=document.getElementById("sAirportDirection");var sPickupEl=document.getElementById("sPickup");var sDropoffEl=document.getElementById("sDropoff");var sDropoffWrap=document.getElementById("sDropoffWrap");var sVehicleWrap=document.getElementById("sVehicleWrap");var rentalRegionWrap=document.getElementById("sRentalRegionWrap");var sRentalRegionEl=document.getElementById("sRentalRegion");
function updateAirportPlaceholders(){} airportDirSelect.addEventListener("change",updateAirportPlaceholders);
function isRentalService(){return currentService==="thue_gio"||currentService==="thue_theo_thang"} function rentalTypeCode(){return currentService==="thue_gio"?"gio":"thang"}
var rentalRegionsCache={};function loadRentalRegions(){var type=rentalTypeCode();if(rentalRegionsCache[type]){fillRentalRegionSelect(rentalRegionsCache[type]);return} sRentalRegionEl.innerHTML='';callApi("rentalListRegions",{rentalType:type}).then(function(res){var regions=(res&&res.data)||[];rentalRegionsCache[type]=regions;if(isRentalService()&&rentalTypeCode()===type){fillRentalRegionSelect(regions)}}).catch(function(){sRentalRegionEl.innerHTML=''})} function fillRentalRegionSelect(regions){if(!regions.length){sRentalRegionEl.innerHTML='';return} var current=sRentalRegionEl.value;sRentalRegionEl.innerHTML=''+regions.map(function(r){return''}).join("");if(regions.indexOf(current)!==-1)sRentalRegionEl.value=current}
function updateServiceUI(){returnDateWrap.style.display=currentService==="khu_hoi"?"":"none";returnTimeWrap.style.display=currentService==="khu_hoi"?"":"none";airportDirWrap.style.display=currentService==="dua_don_san_bay"?"":"none";flightWrap.style.display=currentService==="dua_don_san_bay"?"":"none";sDropoffWrap.style.display=isRentalService()?"none":"";sVehicleWrap.style.display=isRentalService()?"none":"";rentalRegionWrap.style.display=isRentalService()?"":"none";sPickupEl.placeholder="Nhập địa chỉ chính xác";sDropoffEl.placeholder="Nhập địa chỉ chính xác";rtSyncFields();if(isRentalService())loadRentalRegions();} serviceTabs.addEventListener("click",function(e){var btn=e.target.closest("button[data-value]");if(!btn)return;serviceTabs.querySelectorAll("button").forEach(function(b){b.classList.remove("active")});btn.classList.add("active");currentService=btn.dataset.value;updateServiceUI()});updateServiceUI();
function esc(s){return String(s==null?"":s).replace(/[&<>"']/g,function(c){return{"&":"&","<":"<",">":">",'"':""","'":"'"}[c]})} function escAttr(s){return esc(s)}
var mainBtn=document.getElementById("openBookingBtn");var pricePanelEl=document.getElementById("pricePanel");var sVehicleEl=document.getElementById("sVehicle");
var flowState="idle";var tripState={distanceKm:null,durationMinutes:null,fixedRows:null,priceRows:null,rentalPackageRows:null,selectedRentalRow:null};
var MAIN_BTN_LABELS={idle:"🔍 KIỂM TRA LỘ TRÌNH",distance_done:"💰 XEM GIÁ",price_done:"🚘 ĐẶT XE",submitting:"⏳ ĐANG GỬI ĐƠN...",success:"✓ ĐẶT XE THÀNH CÔNG"};
function updateMainButton(){mainBtn.disabled=!1;mainBtn.textContent=MAIN_BTN_LABELS[flowState]||MAIN_BTN_LABELS.idle} function setMainButtonLoading(text){mainBtn.disabled=!0;mainBtn.textContent=text}
function hidePricePanel(){pricePanelEl.style.display="none";pricePanelEl.innerHTML=""}
function invalidateFlow(){if(flowState==="submitting"||flowState==="success")return;if(flowState==="idle")return;flowState="idle";tripState.distanceKm=null;tripState.durationMinutes=null;tripState.fixedRows=null;tripState.priceRows=null;tripState.rentalPackageRows=null;tripState.selectedRentalRow=null;lastQuoteRows=[];hidePricePanel();hideDistanceInfo();updateMainButton()}
function friendlyLookupError(reason){if(reason==="no_price"){return"Chưa có giá niêm yết cho loại xe này. Đội ngũ SUNTRAVEL247 sẽ liên hệ báo giá trực tiếp sau khi anh/chị đặt xe."} if(reason==="network"){return"Không thể kết nối máy chủ để tính giá. Vui lòng thử lại sau ít phút hoặc gọi hotline 0916 43 28 29."} return"Không thể xác định khoảng cách cho tuyến này. Vui lòng kiểm tra lại điểm đón/điểm đến hoặc gọi hotline 0916 43 28 29 để được báo giá trực tiếp."}
function runDistanceStep(){var from=sPickupEl.value.trim();var to=sDropoffEl.value.trim();var isRoundTrip=currentService==="khu_hoi";var stillCurrent=function(){return sPickupEl.value.trim()===from&&sDropoffEl.value.trim()===to};showDistanceLoading();return callApi("quoteRoute",{from:from,to:to}).then(function(res){if(!stillCurrent())return{stale:!0};if(res.data&&res.data.length){var oneWayKm=Number(res.data[0].distance_km)||0;var oneWayMinutes=res.data[0].duration_minutes;if(isRoundTrip){showDistanceEstimated(from,to,{distance_km:oneWayKm*2,duration_minutes:oneWayMinutes?oneWayMinutes*2:null},"khứ hồi - đã tính x2 quãng đường");tripState.distanceKm=oneWayKm*2;tripState.durationMinutes=oneWayMinutes?oneWayMinutes*2:null;tripState.fixedRows=null}else{showDistanceResult(res.data);tripState.distanceKm=oneWayKm||null;tripState.durationMinutes=oneWayMinutes;tripState.fixedRows=res.data} return{ok:!0}} showNoListedPrice(res.message);return Promise.all([geocodeVN(from),geocodeVN(to)]).then(function(pts){if(!stillCurrent())return{stale:!0};if(!pts[0]||!pts[1]){showDistanceError(friendlyLookupError("geocode"));return{ok:!1,reason:"geocode"}} return goongDistanceKm(pts[0],pts[1]).then(function(info){if(!stillCurrent())return{stale:!0};if(!info){showDistanceError(friendlyLookupError("route"));return{ok:!1,reason:"route"}} var rtKm=isRoundTrip?info.distance_km*2:info.distance_km;var rtMinutes=isRoundTrip&&info.duration_minutes?info.duration_minutes*2:info.duration_minutes;showDistanceEstimated(from,to,{distance_km:rtKm,duration_minutes:rtMinutes},isRoundTrip?"khứ hồi - đã tính x2 quãng đường":null);tripState.distanceKm=rtKm;tripState.durationMinutes=rtMinutes;tripState.fixedRows=null;return{ok:!0}})})}).catch(function(){if(!stillCurrent())return{stale:!0};showDistanceError(friendlyLookupError("network"));return{ok:!1,reason:"network"}})}
function isAirportPriced(){if(currentService==="dua_don_san_bay")return!0;if(currentService!=="mot_chieu")return!1;var pu=(document.getElementById("sPickup")?document.getElementById("sPickup").value:"");var dr=(document.getElementById("sDropoff")?document.getElementById("sDropoff").value:"");var txt=(pu+" "+dr).toLowerCase();return/sân bay|san bay|tân sơn nhất|tan son nhat|\btsn\b|airport|nội bài|noi bai/.test(txt)}
function rtSyncFields(){var sd=document.getElementById("sDate"),rdEl=document.getElementById("sReturnDate");var wrap=document.getElementById("sRtDaysWrap"),daysEl=document.getElementById("sRtDays");if(!sd||!rdEl)return;var isRt=currentService==="khu_hoi";if(sd.value){rdEl.min=sd.value;if(isRt&&(!rdEl.value||rdEl.value
")+'
'} function rtNoteText(vehicle){var lines=rtLines(vehicle,RT_VI);if(!lines.length)return"";var rd=document.getElementById("sReturnDate"),rt=document.getElementById("sReturnTime");return"\n[Khứ hồi: về "+(rd?rd.value:"")+" "+(rt?rt.value:"")+" | "+lines.join(" | ")+"]"}
function runPriceStep(){if(currentService==="khu_hoi"){var rtOneWayKm=tripState.distanceKm?tripState.distanceKm/2:0;if(!rtOneWayKm)return Promise.resolve({ok:!1,reason:"no_distance"});return callApi("taxiRoundTripQuote",{oneWayKm:rtOneWayKm,durationMinutes:tripState.durationMinutes?tripState.durationMinutes/2:null,pickupDate:document.getElementById("sDate").value,pickupTime:document.getElementById("sTime").value,returnDate:document.getElementById("sReturnDate").value,returnTime:document.getElementById("sReturnTime").value,pickupAddress:sPickupEl.value.trim(),dropoffAddress:sDropoffEl.value.trim()}).then(function(res){if(!res.data||!res.data.length)return{ok:!1,reason:"no_price"};tripState.priceRows=res.data;tripState.rtMeta=res.meta||null;lastQuoteRows=res.data;return{ok:!0}}).catch(function(){return{ok:!1,reason:"network"}})} if(tripState.fixedRows&&tripState.fixedRows.length){tripState.priceRows=tripState.fixedRows;lastQuoteRows=tripState.fixedRows;return Promise.resolve({ok:!0})} if(!tripState.distanceKm){return Promise.resolve({ok:!1,reason:"no_distance"})} if(isAirportPriced()){var sDateValForBracket=document.getElementById("sDate").value;var sTimeValForBracket=document.getElementById("sTime").value;var bracketDatetime=(sDateValForBracket&&sTimeValForBracket)?(sDateValForBracket+" "+sTimeValForBracket+":00"):null;return callApi("taxiBracketQuoteAllVehicles",{serviceType:"san_bay",distanceKm:tripState.distanceKm,datetime:bracketDatetime}).then(function(res){if(res.data&&res.data.length){tripState.priceRows=res.data;lastQuoteRows=res.data;return{ok:!0}} return runPriceStepProgressive()}).catch(function(){return runPriceStepProgressive()})} return runPriceStepProgressive()}
function runPriceStepProgressive(){return callApi("taxiQuoteAllVehicles",{distanceKm:tripState.distanceKm}).then(function(res){if(!res.data||!res.data.length){return{ok:!1,reason:"no_price"}} tripState.priceRows=res.data;lastQuoteRows=res.data;return{ok:!0}}).catch(function(){return{ok:!1,reason:"network"}})}
function priceForVehicle(vehicleName){if(!tripState.priceRows)return null;var row=tripState.priceRows.find(function(r){return r.vehicle_type===vehicleName});return row?Number(row.price):null}
function nightSurchargeForVehicle(vehicleName){if(!tripState.priceRows)return 0;var row=tripState.priceRows.find(function(r){return r.vehicle_type===vehicleName});return row&&row.night_surcharge?Number(row.night_surcharge):0}
var VEHICLE_PHOTOS={"4 chỗ":"/airport/assets/4cho.jpg","7 chỗ":"/airport/assets/7cho.jpg","Limousine 9 chỗ":"/airport/assets/limousine.jpg","Sedona":"/airport/assets/sedona.jpg","Limousine 11 chỗ":"/airport/assets/limousine11.jpg","Carnival":"/airport/assets/carnival.jpg","16 chỗ":"/airport/assets/16cho.jpg","29 chỗ":"/airport/assets/29cho.jpg","45 chỗ":"/airport/assets/45cho.jpg"};var VEHICLE_PHOTO_KEYS_BY_LEN=Object.keys(VEHICLE_PHOTOS).sort(function(a,b){return b.length-a.length});function vehiclePhotoFor(name){if(!name)return null;if(VEHICLE_PHOTOS[name])return VEHICLE_PHOTOS[name];var found=VEHICLE_PHOTO_KEYS_BY_LEN.find(function(k){return name.indexOf(k)!==-1});return found?VEHICLE_PHOTOS[found]:null}
function renderPricePanel(){if(!tripState.priceRows||!tripState.priceRows.length){pricePanelEl.style.display="";pricePanelEl.innerHTML='
';return} var selected=sVehicleEl.value;var html='
'+'
'+'
'});html+='
';if(priceForVehicle(selected)===null){html+='
'} if(isAirportPriced()&&tripState.priceRows.some(function(r){return r.pricing_mode==="km"})){html+='
'} html+=rtNoteHtml(selected);pricePanelEl.style.display="";pricePanelEl.innerHTML=html} function renderRentalPackagePanel(){var rows=tripState.rentalPackageRows||[];if(!rows.length){pricePanelEl.style.display="";pricePanelEl.innerHTML='
';return} if(!tripState.selectedRentalRow||rows.indexOf(tripState.selectedRentalRow)===-1){tripState.selectedRentalRow=rows[0]} var html='
'+'
'+'
'});html+='
';pricePanelEl.style.display="";pricePanelEl.innerHTML=html}
function runRentalPackageStep(){var region=sRentalRegionEl.value;if(!region){return Promise.resolve({ok:!1,reason:"no_region"})} return callApi("rentalListPackages",{rentalType:rentalTypeCode(),region:region}).then(function(res){var rows=(res&&res.data)||[];tripState.rentalPackageRows=rows;tripState.selectedRentalRow=rows.length?rows[0]:null;lastQuoteRows=rows;return{ok:rows.length>0,reason:rows.length?"":"no_price"}}).catch(function(){return{ok:!1,reason:"network"}})}
pricePanelEl.addEventListener("click",function(e){var rentalRow=e.target.closest(".pp-row[data-rental-idx]");if(rentalRow){var idx=parseInt(rentalRow.dataset.rentalIdx,10);if(tripState.rentalPackageRows&&tripState.rentalPackageRows[idx]){tripState.selectedRentalRow=tripState.rentalPackageRows[idx];renderRentalPackagePanel()} return} var row=e.target.closest(".pp-row[data-vehicle]");if(!row)return;var vehicleName=row.dataset.vehicle;if([...sVehicleEl.options].some(function(o){return o.value===vehicleName})){sVehicleEl.value=vehicleName} renderPricePanel()});sVehicleEl.addEventListener("change",function(){if(flowState==="price_done"&&tripState.priceRows){renderPricePanel()}});
mainBtn.addEventListener("click",function(){if(isRentalService()){if(!sPickupEl.value.trim()||!sRentalRegionEl.value){(sPickupEl.value.trim()?sRentalRegionEl:sPickupEl).focus();return}}else if(!sPickupEl.value.trim()||!sDropoffEl.value.trim()){sPickupEl.focus();return} if(isRentalService()&&flowState==="idle"){setMainButtonLoading("Đang tải bảng giá...");runRentalPackageStep().then(function(r){if(r&&r.ok){flowState="price_done";renderRentalPackagePanel()}else{flowState="idle";pricePanelEl.style.display="";pricePanelEl.innerHTML='
'} updateMainButton()});return} if(flowState==="idle"){setMainButtonLoading("Đang tính quãng đường...");runDistanceStep().then(function(r){if(r&&r.stale)return;if(r&&r.ok){flowState="distance_done"}else{flowState="idle"} updateMainButton()});return} if(flowState==="distance_done"){setMainButtonLoading("Đang tính giá...");runPriceStep().then(function(r){if(r&&r.ok){flowState="price_done";renderPricePanel()}else{flowState="distance_done";pricePanelEl.style.display="";pricePanelEl.innerHTML='
'} updateMainButton()});return} if(flowState==="price_done"){openBookingModal();return}});
[sPickupEl,sDropoffEl].forEach(function(el){el.addEventListener("input",invalidateFlow)});document.getElementById("sDate").addEventListener("change",invalidateFlow);document.getElementById("sTime").addEventListener("change",invalidateFlow);document.getElementById("sReturnDate").addEventListener("change",invalidateFlow);document.getElementById("sReturnTime").addEventListener("change",invalidateFlow);document.getElementById("sDate").addEventListener("change",rtSyncFields);document.getElementById("sReturnDate").addEventListener("change",rtSyncFields);document.getElementById("sAirportDirection").addEventListener("change",invalidateFlow);sRentalRegionEl.addEventListener("change",invalidateFlow);serviceTabs.addEventListener("click",function(e){if(e.target.closest("button[data-value]"))invalidateFlow();});
var backdrop=document.getElementById("bookingBackdrop");var formWrap=document.getElementById("bookingFormWrap");var successWrap=document.getElementById("bookingSuccessWrap");var bookingForm=document.getElementById("bookingForm");var bookingStatus=document.getElementById("bookingStatus");var bookingSubmitBtn=document.getElementById("bookingSubmitBtn");
var serviceLabels={mot_chieu:"Một chiều",khu_hoi:"Khứ hồi",thue_gio:"Thuê giờ",thue_theo_thang:"Thuê theo tháng",dua_don_san_bay:"Đưa đón sân bay"};
function setPriceDisplayWithVat(priceEl,basePrice,nightSurcharge){var vatHint=document.getElementById("tsVatHint");var nightHint=document.getElementById("tsNightHint");if(basePrice===null||basePrice===undefined){priceEl.textContent="Sẽ báo giá trực tiếp";priceEl.classList.add("small");if(vatHint)vatHint.style.display="none";if(nightHint)nightHint.style.display="none";return} var vatChecked=document.getElementById("bVatCompany")?document.getElementById("bVatCompany").checked:!1;var finalPrice=vatChecked?Math.round(Number(basePrice)*1.08):Number(basePrice);priceEl.textContent=finalPrice.toLocaleString("vi-VN")+"đ";priceEl.classList.remove("small");if(vatHint)vatHint.style.display=vatChecked?"":"none";if(nightHint){if(nightSurcharge&&Number(nightSurcharge)>0){nightHint.textContent="✓ Giá trên đã gồm phụ thu giờ đêm (22:00-05:59): "+Number(nightSurcharge).toLocaleString("vi-VN")+"đ";nightHint.style.display=""}else{nightHint.style.display="none"}}}
function fillTripSummary(){var dateVal=document.getElementById("sDate").value;var timeVal=document.getElementById("sTime").value;document.getElementById("tsDateTime").textContent=(dateVal?dateVal.split("-").reverse().join("/"):"")+(timeVal?" lúc "+timeVal:"");document.getElementById("tsPassengers").textContent=document.getElementById("sPassengers").value||"1";var priceEl=document.getElementById("tsPrice");
if(isRentalService()){var pkg=tripState.selectedRentalRow;document.getElementById("tsRoute").textContent=sPickupEl.value.trim()+(pkg?" · Khu vực: "+pkg.region:"");document.getElementById("tsVehicle").textContent=(pkg?pkg.vehicle_type+" · "+pkg.package_name:"—")+" · "+(serviceLabels[currentService]||"");setPriceDisplayWithVat(priceEl,pkg?Number(pkg.price):null,0);return}
var vehicleName=sVehicleEl.value;document.getElementById("tsRoute").textContent=sPickupEl.value.trim()+" → "+sDropoffEl.value.trim();document.getElementById("tsVehicle").textContent=vehicleName+(currentService?" · "+(serviceLabels[currentService]||""):"");var price=priceForVehicle(vehicleName);var nightSurcharge=nightSurchargeForVehicle(vehicleName);setPriceDisplayWithVat(priceEl,price,nightSurcharge)}
var bVatCompanyEl=document.getElementById("bVatCompany");var bVatFieldsEl=document.getElementById("bVatFields");if(bVatCompanyEl){bVatCompanyEl.addEventListener("change",function(){if(bVatFieldsEl)bVatFieldsEl.style.display=bVatCompanyEl.checked?"":"none";fillTripSummary()})}
function openBookingModal(){formWrap.style.display="";successWrap.style.display="none";bookingStatus.textContent="";bookingStatus.className="modal-status";fillTripSummary();backdrop.classList.add("open");document.body.style.overflow="hidden"} function closeBookingModal(){backdrop.classList.remove("open");document.body.style.overflow=""} document.getElementById("bookingClose").addEventListener("click",closeBookingModal);document.getElementById("bookingDoneBtn").addEventListener("click",function(){closeBookingModal();if(flowState==="success"){flowState="idle";hidePricePanel();hideDistanceInfo();updateMainButton();window.scrollTo({top:0,behavior:"smooth"})}});backdrop.addEventListener("click",function(e){if(e.target===backdrop)closeBookingModal();});document.addEventListener("keydown",function(e){if(e.key==="Escape")closeBookingModal();});
bookingForm.addEventListener("submit",async function(e){e.preventDefault();bookingStatus.textContent="";bookingStatus.className="modal-status";
var pickupDateVal=document.getElementById("sDate").value;var pickupTimeVal=document.getElementById("sTime").value;var isRental=isRentalService();var rentalPkg=isRental?tripState.selectedRentalRow:null;
var chosenVehicle,estimatedPrice,pricingSnapshot,distanceForFinalQuote;
if(isRental){chosenVehicle=rentalPkg?rentalPkg.vehicle_type:"";estimatedPrice=rentalPkg?Number(rentalPkg.price):null;pricingSnapshot=rentalPkg?{rental_type:rentalTypeCode(),region:rentalPkg.region,package_name:rentalPkg.package_name,price:rentalPkg.price,overage_note:rentalPkg.overage_note}:null;distanceForFinalQuote=null}else{chosenVehicle=document.getElementById("sVehicle").value;estimatedPrice=getEstimatedPriceForVehicle(chosenVehicle);pricingSnapshot=null;var lastRow=lastQuoteRows.find(function(r){return r.vehicle_type===chosenVehicle});distanceForFinalQuote=lastRow?Number(lastRow.distance_km):null;if(!isAirportPriced()&&distanceForFinalQuote&&pickupDateVal&&pickupTimeVal){try{var finalQuoteRes=await callApi("taxiQuote",{vehicleType:chosenVehicle,distanceKm:distanceForFinalQuote,datetime:pickupDateVal+" "+pickupTimeVal+":00"});if(finalQuoteRes&&finalQuoteRes.ok&&finalQuoteRes.data){estimatedPrice=finalQuoteRes.data.final_total;pricingSnapshot=finalQuoteRes.data}}catch(errQuote){}}}
var vatCompanyChecked=document.getElementById("bVatCompany")?document.getElementById("bVatCompany").checked:!1;var vatCompanyName=vatCompanyChecked?document.getElementById("bVatCompanyName").value.trim():"";var vatTaxCode=vatCompanyChecked?document.getElementById("bVatTaxCode").value.trim():"";var vatCompanyAddress=vatCompanyChecked?document.getElementById("bVatCompanyAddress").value.trim():"";var vatSurchargeAmount=0;if(vatCompanyChecked&&estimatedPrice){var priceBeforeVat=Number(estimatedPrice);estimatedPrice=Math.round(priceBeforeVat*1.08);vatSurchargeAmount=estimatedPrice-priceBeforeVat}
var payload={customerName:document.getElementById("bName").value.trim(),phone:document.getElementById("bPhone").value.trim(),email:document.getElementById("bEmail").value.trim(),pickupSignName:document.getElementById("bSignName").value.trim(),serviceType:currentService,pickupAddress:document.getElementById("sPickup").value.trim(),dropoffAddress:isRental?("Khu vực: "+(rentalPkg?rentalPkg.region:sRentalRegionEl.value)):document.getElementById("sDropoff").value.trim(),vehicleType:chosenVehicle,passengers:parseInt(document.getElementById("sPassengers").value,10)||1,pickupDate:pickupDateVal,pickupTime:pickupTimeVal,returnDate:currentService==="khu_hoi"?document.getElementById("sReturnDate").value:"",returnTime:currentService==="khu_hoi"?document.getElementById("sReturnTime").value:"",flightCode:currentService==="dua_don_san_bay"?document.getElementById("sFlightCode").value.trim():"",airportDirection:currentService==="dua_don_san_bay"?document.getElementById("sAirportDirection").value:"",notes:(document.getElementById("bNotes").value.trim()+rtNoteText(chosenVehicle)).trim(),consentData:document.getElementById("bConsent").checked,estimatedPrice:estimatedPrice,pricingSnapshot:pricingSnapshot,vatCompany:vatCompanyChecked,vatCompanyName:vatCompanyName,vatTaxCode:vatTaxCode,vatCompanyAddress:vatCompanyAddress,vatSurchargeAmount:vatSurchargeAmount};
if(!payload.customerName||!payload.phone||!payload.pickupAddress||!payload.dropoffAddress||!payload.pickupDate||!payload.pickupTime){bookingStatus.textContent="Vui lòng điền đầy đủ các trường bắt buộc (*)";bookingStatus.className="modal-status err";return} if(payload.vatCompany&&(!payload.vatCompanyName||!payload.vatTaxCode||!payload.vatCompanyAddress)){bookingStatus.textContent="Vui lòng điền đầy đủ Tên công ty / Mã số thuế / Địa chỉ công ty để xuất hóa đơn VAT.";bookingStatus.className="modal-status err";return} if(!/^[0-9+ ]{9,12}$/.test(payload.phone)){bookingStatus.textContent="Số điện thoại không hợp lệ, vui lòng kiểm tra lại";bookingStatus.className="modal-status err";return} if(payload.email&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(payload.email)){bookingStatus.textContent="Email không hợp lệ, vui lòng kiểm tra lại";bookingStatus.className="modal-status err";return} if(!payload.consentData){bookingStatus.textContent="Vui lòng đồng ý cho phép sử dụng thông tin để tiếp tục";bookingStatus.className="modal-status err";return}
flowState="submitting";bookingSubmitBtn.disabled=!0;bookingSubmitBtn.textContent="⏳ ĐANG GỬI ĐƠN...";var summaryForSuccess={route:payload.pickupAddress+" → "+payload.dropoffAddress,dateTime:payload.pickupDate.split("-").reverse().join("/")+" lúc "+payload.pickupTime,vehicle:chosenVehicle,km:isRental?(rentalPkg?rentalPkg.package_name:"—"):(distanceForFinalQuote?distanceForFinalQuote.toFixed(1)+" km":"—"),price:estimatedPrice?(Number(estimatedPrice).toLocaleString("vi-VN")+"đ"+(payload.vatCompany?" (đã gồm VAT 8%)":"")):"Sẽ báo giá trực tiếp"};try{var res=await callApi("createBooking",payload);flowState="success";formWrap.style.display="none";successWrap.style.display="";document.getElementById("successCode").textContent=res.trackingCode;document.getElementById("successRoute").textContent=summaryForSuccess.route;document.getElementById("successDateTime").textContent=summaryForSuccess.dateTime;document.getElementById("successVehicle").textContent=summaryForSuccess.vehicle;document.getElementById("successKm").textContent=summaryForSuccess.km;document.getElementById("successPrice").textContent=summaryForSuccess.price;var successViewBtn=document.getElementById("successViewBtn");successViewBtn.dataset.code=res.trackingCode;successViewBtn.dataset.phone=payload.phone;setupPayButton(res.trackingCode,payload.phone,estimatedPrice);bookingForm.reset();if(bVatFieldsEl)bVatFieldsEl.style.display="none"}catch(err){flowState="price_done";bookingStatus.textContent=err.message;bookingStatus.className="modal-status err"}finally{bookingSubmitBtn.disabled=!1;bookingSubmitBtn.textContent="🚘 Xác nhận đặt xe";updateMainButton()}});
})()
🧳 여행 유틸리티
환전, 항공권, 호텔 및 여행 액세서리 예약 – 모든 여행에 편리하게
외화
신속하게
사용



























