LIVE MODE
Loads Analyzed
0
This session
Avg Profit/Mile
$--
After all costs
Best Route
--
No data yet
Avoid Routes
0
Below threshold
Recent Route Analyses
No routes analyzed yet. Use Route Analyzer to get started.
Active Market Alerts
⚠️️ Winter weather: I-80 corridor (NE, WY, UT) -- expect 8-15% fuel cost increase
📈 High export: TX Gulf Coast ports -- strong outbound demand, favorable rates
🔄 High import: LA/Long Beach -- return loads available, reduce deadhead
Quick Route Check
Load Details
Cost Parameters

Adjustment Factors
Analysis Result
--
0
PROFIT SCORE
Gross Revenue
--
Total Costs
--
Net Profit
--
Revenue/Mile
--
Profit/Mile
--
Margin
--
Cost Breakdown

Active Factors

🔀 Build a partial load route. Type any city or address — miles calculate automatically via Google Maps. Add stops to see how they affect total mileage and cost.
Map loads when you enter locations...
Route Cost Settings
Route Summary
Add stops below to see summary
Stops & Loads
ℹ️ Connect your Truckstop API in Settings to pull live loads. Demo data shown -- click any row to analyze.
Filter Loads
Available Loads
OriginDestinationMilesRateRate/miEquipScoreAnalyzePartial Load
Truckstop.com API Configuration
⚠️️ Truckstop API requires a Systems Integration Agreement (SIA) + Load Board Pro. Contact integration@truckstop.com to get started.
Default Cost Settings
API Endpoints Reference
Load Search (SOAP v13):
POST https://webservices.truckstop.com/v13/Searching/LoadSearch.svc
Rate Insights (REST v3):
GET https://api.truckstop.com/rateinsights/v3/rates
Auth Token:
POST https://identity.truckstop.com/connect/token
: \'\'; return '' + ''+load.origin+''+load.dest+'' + ''+load.miles.toLocaleString()+''+fmtD(load.rate)+'' + ''+fmtD(p.rpm)+''+load.equip+'' + ''+p.score+'/100' + '' + ''+msBtn+''; }).join(''); // Row click -> analyze (but not if clicking a button) el('load-tbody').querySelectorAll('tr').forEach(function(row) { row.addEventListener('click', function(e) { if (e.target.tagName === 'BUTTON') return; sendToAnalyzer(parseInt(row.getAttribute('data-lid'))); }); }); // Analyze buttons el('load-tbody').querySelectorAll('button[data-lid]').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); sendToAnalyzer(parseInt(btn.getAttribute('data-lid'))); }); }); // Multi-stop buttons // Use event delegation so dynamically re-enabled buttons still work var tbody = el('load-tbody'); if (!tbody._delegated) { tbody.addEventListener('click', function(e) { var btn = e.target.closest('button[data-msid]'); if (btn) { e.stopPropagation(); sendToMultiStop(parseInt(btn.getAttribute('data-msid'))); } }); tbody._delegated = true; } } function sendToAnalyzer(id) { var load = demoLoads.filter(function(l){return l.id===id;})[0]; if (!load) return; el('a-origin').value = load.origin; el('a-dest').value = load.dest; el('a-miles').value = load.miles; el('a-rate').value = load.rate; el('a-weight').value = load.weight; el('a-equip').value = load.equip; el('tab-analyzer').click(); setTimeout(function(){el('btn-analyze').click();}, 150); } function sendToMultiStop(id) { var load = demoLoads.filter(function(l){return l.id===id;})[0]; if (!load) return; // If multi-stop only has the 2 blank default stops, clear and set up properly var allEmpty = msStops.every(function(s){ var locEl = el('ms-loc-'+s.id); return !locEl || !locEl.value.trim(); }); msSave(); // preserve any existing stop data // Insert the load as a new stop before the final destination // If only 2 blank stops exist, use first as origin and insert before last var insertIdx = msStops.length - 1; // insert before final destination stop if (insertIdx < 0) insertIdx = 0; var newId = ++msIdSeq; var newStop = { id: newId, loc: load.origin, mi: load.miles.toString(), rate: load.rate.toString(), wt: load.weight.toString(), type: 'pickup', fromLoadId: load.id // track which load this came from }; // If adding first real load to blank planner, set origin city too if (allEmpty && msStops.length >= 1) { var firstStop = msStops[0]; firstStop.loc = load.origin; // Update last stop location to load destination var lastStop = msStops[msStops.length - 1]; lastStop.loc = load.dest; // Set the new stop as a middle stop with the load details newStop.loc = load.origin; newStop.type = 'both'; } msStops.splice(insertIdx, 0, newStop); msRender(); msSummary(); // Re-render load board to show "Added" state renderLoads(filteredLoads); // Switch to multi-stop tab and show toast renderLoads(filteredLoads); el('tab-multistop').click(); toast('✅ ' + load.origin + ' → ' + load.dest + ' added to Partial Load Planner! (' + fmtD(load.rate) + ')', 4000); } el('btn-filter').addEventListener('click', function() { var o = txt('fl-orig').toUpperCase(), d = txt('fl-dest').toUpperCase(); var eq = el('fl-equip').value, mr = val('fl-rate',0); filteredLoads = demoLoads.filter(function(l){ return (!o||l.oState===o)&&(!d||l.dState===d)&&(!eq||l.equip===eq)&&l.rate>=mr; }); renderLoads(filteredLoads); }); el('btn-reset-filter').addEventListener('click', function() { ['fl-orig','fl-dest','fl-rate'].forEach(function(id){el(id).value='';}); el('fl-equip').value=''; filteredLoads = demoLoads.slice(); renderLoads(filteredLoads); }); // Multi-stop planner // Save current DOM input values back into msStops state before any re-render function msSave() { msStops.forEach(function(stop, idx) { var locEl = el('ms-loc-'+stop.id); var miEl = el('ms-mi-'+stop.id); var rateEl= el('ms-rate-'+stop.id); var wtEl = el('ms-wt-'+stop.id); var typEl = el('ms-type-'+stop.id); if (locEl) stop.loc = locEl.value; if (miEl) stop.mi = miEl.value; if (rateEl) stop.rate = rateEl.value; if (wtEl) stop.wt = wtEl.value; if (typEl) stop.type = typEl.value; }); } function msRender() { var container = el('ms-stops'); if (msStops.length === 0) { container.innerHTML = '
No stops added. Click + Add Stop to build your route.
'; return; } container.innerHTML = msStops.map(function(stop, idx) { var isFirst = idx===0, isLast = idx===msStops.length-1; var numClass = isFirst?'snum first':isLast?'snum last':'snum'; var label = isFirst?'Origin':isLast?'Final Destination':'Stop '+(idx+1); var remBtn = msStops.length > 2 ? '' : ''; return '
'+ '
'+ '
'+ '
'+(idx+1)+'
'+ ''+label+''+ remBtn+'
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'; }).join('') + (msStops.length > 1 ? '
☰ Drag stops to reorder your route
' : ''); // Remove buttons container.querySelectorAll('button[data-sid]').forEach(function(btn) { btn.addEventListener('click', function() { msSave(); var sid = parseInt(btn.getAttribute('data-sid')); msStops = msStops.filter(function(s){return s.id!==sid;}); msRender(); msSummary(); el('ms-results').style.display='none'; // Directly update load board buttons in DOM renderLoads(filteredLoads); }); }); // Wire up Google Places autocomplete on each location input if (window.google && window.google.maps && window.google.maps.places) { msStops.forEach(function(stop) { var input = el('ms-loc-'+stop.id); if (input && !input.dataset.acBound) { var ac = new google.maps.places.Autocomplete(input, { types: ['geocode'], componentRestrictions: {country: 'us'} }); ac.addListener('place_changed', function() { var place = ac.getPlace(); if (place && place.formatted_address) { msSave(); // Update this stop's location var s = msStops.find(function(s2){return s2.id===stop.id;}); if (s) s.loc = place.formatted_address; // Auto-calculate route msCalculateRoute(); } }); input.dataset.acBound = '1'; } }); } // Drag and drop var dragSrcId = null; container.querySelectorAll('.sc[draggable]').forEach(function(card) { card.addEventListener('dragstart', function(e) { msSave(); // save all data before drag dragSrcId = parseInt(card.getAttribute('data-sid')); card.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', dragSrcId); }); card.addEventListener('dragend', function() { card.classList.remove('dragging'); container.querySelectorAll('.sc').forEach(function(c) { c.classList.remove('drag-over','drag-over-below'); }); dragSrcId = null; }); card.addEventListener('dragover', function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; var targetId = parseInt(card.getAttribute('data-sid')); if (targetId === dragSrcId) return; container.querySelectorAll('.sc').forEach(function(c) { c.classList.remove('drag-over','drag-over-below'); }); // Determine if dropping above or below midpoint var rect = card.getBoundingClientRect(); var midY = rect.top + rect.height / 2; if (e.clientY < midY) { card.classList.add('drag-over'); } else { card.classList.add('drag-over-below'); } }); card.addEventListener('dragleave', function() { card.classList.remove('drag-over','drag-over-below'); }); card.addEventListener('drop', function(e) { e.preventDefault(); var targetId = parseInt(card.getAttribute('data-sid')); if (targetId === dragSrcId) return; // Find indices var srcIdx = msStops.findIndex(function(s){return s.id===dragSrcId;}); var tgtIdx = msStops.findIndex(function(s){return s.id===targetId;}); if (srcIdx < 0 || tgtIdx < 0) return; // Determine insert position (above or below) var rect = card.getBoundingClientRect(); var midY = rect.top + rect.height / 2; var insertAfter = e.clientY >= midY; // Remove src from array var moved = msStops.splice(srcIdx, 1)[0]; // Recalculate target index after removal tgtIdx = msStops.findIndex(function(s){return s.id===targetId;}); var insertAt = insertAfter ? tgtIdx + 1 : tgtIdx; msStops.splice(insertAt, 0, moved); msRender(); msSummary(); el('ms-results').style.display='none'; toast('✅ Stop reordered!', 1500); }); }); } function msCollect() { return msStops.map(function(stop, idx) { var isLast = idx===msStops.length-1; return { id:stop.id, loc: (el('ms-loc-'+stop.id)||{}).value || '', mi: isLast?0:val('ms-mi-'+stop.id,0), rate: isLast?0:val('ms-rate-'+stop.id,0), wt: isLast?0:val('ms-wt-'+stop.id,0), type: (el('ms-type-'+stop.id)||{}).value || 'pickup' }; }); } // Sync load board buttons with current msStops state // Updates buttons directly in DOM without full table re-render function syncLoadBoardButtons() { document.querySelectorAll('button[data-msid]').forEach(function(btn) { var loadId = parseInt(btn.getAttribute('data-msid')); var isAdded = msStops.some(function(s){ return s.fromLoadId === loadId; }); if (isAdded) { btn.textContent = '✓ Added'; btn.disabled = true; btn.style.background = 'var(--surface2)'; btn.style.color = 'var(--gold)'; btn.style.border = '1px solid var(--gold)'; btn.style.cursor = 'default'; btn.removeAttribute('data-msid'); btn.setAttribute('data-msid-added', loadId); } else { btn.textContent = '+ Partial Load'; btn.disabled = false; btn.style.background = 'var(--gold)'; btn.style.color = '#0a0900'; btn.style.border = 'none'; btn.style.cursor = 'pointer'; // Re-enable click btn.setAttribute('data-msid', loadId); btn.removeAttribute('data-msid-added'); // Re-attach click handler btn.onclick = function(e) { e.stopPropagation(); sendToMultiStop(loadId); }; } }); // Also fix any "added" buttons that lost their data-msid document.querySelectorAll('button[data-msid-added]').forEach(function(btn) { var loadId = parseInt(btn.getAttribute('data-msid-added')); var isAdded = msStops.some(function(s){ return s.fromLoadId === loadId; }); if (!isAdded) { btn.textContent = '+ Partial Load'; btn.disabled = false; btn.style.background = 'var(--gold)'; btn.style.color = '#0a0900'; btn.style.border = 'none'; btn.style.cursor = 'pointer'; btn.setAttribute('data-msid', loadId); btn.removeAttribute('data-msid-added'); btn.onclick = function(e) { e.stopPropagation(); sendToMultiStop(loadId); }; } }); } // Google Maps route calculation var msDirectionsService = null; var msDirectionsRenderer = null; var msRouteMap = null; function msInitMap() { if (!window.google || !window.google.maps) return; var mapEl = el('ms-map'); if (!mapEl || msRouteMap) return; msRouteMap = new google.maps.Map(mapEl, { zoom: 5, center: {lat: 37.5, lng: -96}, styles: [ {elementType:'geometry',stylers:[{color:'#1a1800'}]}, {elementType:'labels.text.fill',stylers:[{color:'#c8a84b'}]}, {elementType:'labels.text.stroke',stylers:[{color:'#0a0900'}]}, {featureType:'road',elementType:'geometry',stylers:[{color:'#2a2400'}]}, {featureType:'road.highway',elementType:'geometry',stylers:[{color:'#3a3200'}]}, {featureType:'road.highway',elementType:'geometry.stroke',stylers:[{color:'#c8a84b'}]}, {featureType:'water',elementType:'geometry',stylers:[{color:'#0a0500'}]}, {featureType:'poi',stylers:[{visibility:'off'}]}, {featureType:'transit',stylers:[{visibility:'off'}]} ], disableDefaultUI: false, zoomControl: true, streetViewControl: false, mapTypeControl: false }); msDirectionsService = new google.maps.DirectionsService(); msDirectionsRenderer = new google.maps.DirectionsRenderer({ map: msRouteMap, polylineOptions: {strokeColor:'#c8a84b', strokeWeight:4, strokeOpacity:0.9}, suppressMarkers: false }); } function msCalculateRoute() { if (!window.google || !window.google.maps) return; msSave(); var locs = msStops.map(function(s){ return s.loc; }).filter(function(l){ return l && l.trim(); }); if (locs.length < 2) return; msInitMap(); if (!msDirectionsService) return; var origin = locs[0]; var destination = locs[locs.length-1]; var waypoints = locs.slice(1, locs.length-1).map(function(loc) { return {location: loc, stopover: true}; }); msDirectionsService.route({ origin: origin, destination: destination, waypoints: waypoints, travelMode: google.maps.TravelMode.DRIVING, unitSystem: google.maps.UnitSystem.IMPERIAL, optimizeWaypoints: false }, function(result, status) { if (status === 'OK') { msDirectionsRenderer.setDirections(result); // Extract leg distances and update stop miles automatically var legs = result.routes[0].legs; var totalMeters = 0; var totalSecs = 0; legs.forEach(function(leg, i) { totalMeters += leg.distance.value; totalSecs += leg.duration.value; // Auto-fill miles for each leg (all stops except last) if (i < msStops.length - 1) { var miles = Math.round(leg.distance.value * 0.000621371); msStops[i].mi = miles.toString(); var miEl = el('ms-mi-'+msStops[i].id); if (miEl) miEl.value = miles; } }); var totalMiles = Math.round(totalMeters * 0.000621371); var hours = Math.floor(totalSecs / 3600); var mins = Math.round((totalSecs % 3600) / 60); var timeStr = hours + 'h ' + mins + 'm'; // Show map summary bar var summaryEl = el('ms-map-summary'); if (summaryEl) summaryEl.style.display = 'flex'; if (el('ms-map-miles')) el('ms-map-miles').textContent = totalMiles.toLocaleString() + ' mi'; if (el('ms-map-time')) el('ms-map-time').textContent = timeStr; if (el('ms-map-stops')) el('ms-map-stops').textContent = locs.length + ' locations'; msSummary(); toast('🗺 Route calculated: ' + totalMiles.toLocaleString() + ' mi, ' + timeStr, 3000); } else { toast('Could not calculate route: ' + status, 3000); } }); } function msSummary() { var stops = msCollect(); var hasData = stops.some(function(s){return s.mi>0||s.rate>0;}); el('ms-empty').style.display = hasData?'none':'block'; el('ms-summary').style.display = hasData?'block':'none'; if (!hasData) return; var fuel=val('ms-fuel',3.85), mpg=val('ms-mpg',6.5), drv=val('ms-driver',0.55), ovh=val('ms-overhead',0.18); var cap=val('ms-capacity',44000); var totRev=0,totMi=0,maxWt=0,totWt=0; stops.forEach(function(s){ totRev+=s.rate; totMi+=s.mi; if(s.wt>maxWt) maxWt=s.wt; totWt+=s.wt; }); // Total route cost calculated ONCE -- shared across all partial loads var totalRouteCost = (totMi/mpg)*fuel + totMi*drv + totMi*ovh; var totCost = totalRouteCost; // same total cost regardless of how many loads share it var net=totRev-totCost, ppm=totMi>0?net/totMi:0; var capPct=Math.min(100,Math.round((maxWt/cap)*100)); el('ms-rev').textContent=fmtD(totRev); el('ms-rev').style.color='#00c46a'; el('ms-cost').textContent=fmtD(totCost); el('ms-cost').style.color='#ff4d6a'; el('ms-net').textContent=fmtD(net); el('ms-net').style.color=net>=0?'#00c46a':'#ff4d6a'; el('ms-ppm').textContent=fmtD(ppm)+'/mi'; el('ms-ppm').style.color=ppm>=0.3?'#00c46a':ppm>=0?'#f0c040':'#ff4d6a'; el('ms-mi').textContent=totMi.toLocaleString(); el('ms-sc').textContent=stops.length; el('ms-cap-pct').textContent=capPct+'%'; el('ms-cap-bar').style.width=capPct+'%'; el('ms-cap-bar').style.background=capPct>90?'var(--red)':capPct>70?'var(--yellow)':'var(--gold)'; } function msAddStop(prefill) { msSave(); // preserve existing input values before re-rendering var id = ++msIdSeq; msStops.push({id:id, loc:'', mi:'', rate:'', wt:'', type:'pickup'}); if (prefill) { var s=msStops[msStops.length-1]; s.loc=prefill.loc||''; s.type=prefill.type||'pickup'; } msRender(); msSummary(); // Scroll the new stop card into view smoothly setTimeout(function() { var newCard = el('sc-'+id); if (newCard) newCard.scrollIntoView({behavior:'smooth', block:'nearest'}); // Focus the location field of the new stop var newLoc = el('ms-loc-'+id); if (newLoc) newLoc.focus(); }, 50); } el('btn-add-stop').addEventListener('click', function(){msAddStop();}); el('btn-calc-ms').addEventListener('click', function() { msCalculateRoute(); msSave(); // save before collecting var stops = msCollect(); if (stops.length<2){toast('Add at least 2 stops');return;} if (!stops.every(function(s){return s.loc;})){toast('Fill in a location for every stop');return;} var fuel=val('ms-fuel',3.85), mpg=val('ms-mpg',6.5), drv=val('ms-driver',0.55), ovh=val('ms-overhead',0.18); var cap=val('ms-capacity',44000); var splitMethod = (el('ms-split-method')||{}).value || 'weight'; // --- PARTIAL LOAD COST MODEL --- // Step 1: Calculate total route cost ONCE for the entire route var activeStops = stops.filter(function(s,i){return i < stops.length-1;}); var totMi = activeStops.reduce(function(a,s){return a+s.mi;},0); var totWt = activeStops.reduce(function(a,s){return a+s.wt;},0); var totRev = activeStops.reduce(function(a,s){return a+s.rate;},0); var totalRouteCost = (totMi/mpg)*fuel + totMi*drv + totMi*ovh; // Step 2: Assign each load its SHARE of the total cost based on split method var rows=stops.map(function(stop,idx){ var isLast=idx===stops.length-1; if(isLast){return {stop:stop,isLast:true,nextLoc:''};} var costShare; if (splitMethod === 'weight') { // Each load pays (its weight / total weight) of the full route cost var wtShare = totWt > 0 ? stop.wt / totWt : 1 / activeStops.length; costShare = totalRouteCost * wtShare; } else if (splitMethod === 'mileage') { // Each load pays (its miles / total miles) of the full route cost var miShare = totMi > 0 ? stop.mi / totMi : 1 / activeStops.length; costShare = totalRouteCost * miShare; } else { // Equal split -- costs divided evenly costShare = totalRouteCost / activeStops.length; } var legProfit = stop.rate - costShare; var ppm = stop.mi > 0 ? legProfit / stop.mi : 0; var overCap = stop.wt > cap; return {stop:stop,isLast:false,nextLoc:stops[idx+1].loc, legCost:costShare,legProfit:legProfit,ppm:ppm,overCap:overCap, wtShare:totWt>0?Math.round(stop.wt/totWt*100):0}; }); var totCost = totalRouteCost; var tbody = el('ms-tbody'); tbody.innerHTML = rows.map(function(row,idx){ if(row.isLast){ return '
'+(idx+1)+'
'+ 'Final Destination: '+row.stop.loc+''; } var pc=row.legProfit>=0?'bg':'br'; var ppmColor=row.ppm>=0.3?'var(--green)':row.ppm>=0?'var(--yellow)':'var(--red)'; var capWarn=row.overCap?' ⚠️':''; var status=row.overCap?'Over Cap': row.ppm>=0.3?'Good': row.ppm>=0?'Marginal':'Loss'; var shareLabel = row.wtShare ? row.wtShare+'% share' : ''; return ''+ '
'+(idx+1)+'
'+ ''+row.stop.loc+''+ ''+row.nextLoc+''+ ''+row.stop.mi.toLocaleString()+''+ ''+(row.stop.wt?row.stop.wt.toLocaleString()+' lbs'+capWarn:'--')+''+ ''+fmtD(row.stop.rate)+''+ ''+fmtD(row.legCost)+'
'+shareLabel+''+ ''+fmtD(row.legProfit)+''+ ''+fmtD(row.ppm)+'/mi'+ ''+status+''; }).join(''); el('ms-results').style.display='block'; var net2=totRev-totCost, ppm2=totMi>0?net2/totMi:0, margin2=totRev>0?(net2/totRev)*100:0; var badLegs=rows.filter(function(r){return !r.isLast&&r.legProfit<0;}).length; var overCapLegs=rows.filter(function(r){return !r.isLast&&r.overCap;}).length; var recHtml='
🚚 Total Route Cost: '+fmtD(totalRouteCost)+' split across '+activeStops.length+' load(s) using '+(splitMethod==='weight'?'weight share':splitMethod==='mileage'?'mileage share':'equal split')+' method. Each additional load reduces everyone\'s cost share.
'; if(overCapLegs>0) recHtml+='
⚠️ '+overCapLegs+' leg(s) exceed truck capacity. Remove weight or split loads.
'; if(badLegs>0) recHtml+='
📉 '+badLegs+' load(s) are unprofitable after their cost share. Negotiate the rate up or drop those loads.
'; if(ppm2>=0.4&&margin2>=20) recHtml+='
✅ Strong multi-stop route at '+fmtD(ppm2)+'/mi with '+margin2.toFixed(1)+'% margin. Book it.
'; else if(ppm2>=0.25) recHtml+='
👍 Acceptable at '+fmtD(ppm2)+'/mi. Look to improve weaker legs.
'; else if(ppm2>=0) recHtml+='
⚠️ Thin margins at '+fmtD(ppm2)+'/mi. Negotiate rates up or cut unprofitable legs.
'; else recHtml+='
❌ Overall net loss of '+fmtD(Math.abs(net2))+'. Restructure the route.
'; var scoredLegs=rows.filter(function(r){return !r.isLast;}); if(scoredLegs.length>=2){ var best2=scoredLegs.reduce(function(a,b){return a.ppm>b.ppm?a:b;}); var worst2=scoredLegs.reduce(function(a,b){return a.ppm at '+fmtD(best2.ppm)+'/mi. Weakest: '+worst2.stop.loc+' at '+fmtD(worst2.ppm)+'/mi -- prioritize renegotiating this leg.'; } el('ms-rec').innerHTML=recHtml; msSummary(); el('ms-results').scrollIntoView({behavior:'smooth',block:'nearest'}); }); el('btn-clear-ms').addEventListener('click', function() { msStops=[]; msIdSeq=0; msRender(); msSummary(); el('ms-results').style.display='none'; msAddStop({type:'pickup'}); msAddStop({type:'delivery'}); renderLoads(filteredLoads); toast('Route cleared'); }); el('btn-save-ms').addEventListener('click', function() { var stops=msCollect(); var first=stops[0]&&stops[0].loc||'?'; var last=stops[stops.length-1]&&stops[stops.length-1].loc||'?'; var ppm=parseFloat((el('ms-ppm').textContent||'0').replace(/[^0-9.-]/g,''))||0; var net=parseFloat((el('ms-net').textContent||'0').replace(/[^0-9.-]/g,''))||0; var score=Math.min(100,Math.max(0,Math.round(ppm*100))); state.savedRoutes.push({name:'[Multi] '+first+' -> '+last+' ('+stops.length+' stops)',ppm:ppm,net:net,score:score.toString()}); updateDashboard(); toast('Multi-stop route saved!'); el('tab-dashboard').click(); }); // Settings el('btn-test-api').addEventListener('click',function(){toast('Demo mode -- real API requires SIA credentials from Truckstop.com');}); el('btn-save-api').addEventListener('click',function(){toast('Credentials saved (session only)');}); el('btn-save-defaults').addEventListener('click',function(){ el('c-fuel').value=el('def-fuel').value; el('c-mpg').value=el('def-mpg').value; el('c-driver').value=el('def-driver').value; el('ms-fuel').value=el('def-fuel').value; el('ms-mpg').value=el('def-mpg').value; el('ms-driver').value=el('def-driver').value; toast('Default costs applied to all tabs!'); }); // Init el('a-date').value = new Date().toISOString().split('T')[0]; renderLoads(demoLoads); msAddStop({type:'pickup'}); msAddStop({type:'delivery'}); updateDashboard(); // Expose map init for Google Maps callback window._msCalcRoute = msCalculateRoute; window._msInitMap = msInitMap; })(); // Google Maps callback function initGoogleMaps() { if (window._msInitMap) window._msInitMap(); // Re-render to bind autocomplete if planner tab is active if (window._msCalcRoute) { // Autocomplete will be bound on next msRender call var tabBtn = document.getElementById('tab-multistop'); if (tabBtn) tabBtn.addEventListener('click', function() { setTimeout(function() { var evt = new Event('msreinit'); document.dispatchEvent(evt); }, 100); }); } }