<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Geo-Layover Log</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
.input-field { background-color: #374151; border-color: #4b5563; }
.btn-primary { background-color: #2563eb; }
.btn-primary:hover:not(:disabled) { background-color: #1d4ed8; }
.btn-secondary { background-color: #4b5563; }
.btn-secondary:hover:not(:disabled) { background-color: #374151; }
.btn-danger { background-color: #dc2626; }
.btn-danger:hover { background-color: #b91c1c; }
.btn-success { background-color: #16a34a; }
.btn-success:hover:not(:disabled) { background-color: #15803d; }
.status-card { background-color: #1f2937; }
.spinner { border-top-color: transparent; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
button:disabled { background-color: #374151; color: #9ca3af; cursor: not-allowed; }
</style>
</head>
<body class="bg-gray-900 text-gray-200 flex items-center justify-center min-h-screen p-4">
<div class="w-full max-w-md mx-auto">
<header class="text-center mb-6">
<h1 class="text-3xl font-bold text-white">Geo-Layover Log</h1>
<p class="text-gray-400">Automated layover alerts based on your destination.</p>
</header>
<main id="app-container">
<div id="setup-form" class="space-y-4">
<div>
<label for="driverCode" class="block text-sm font-medium text-gray-300 mb-1">Driver Code</label>
<input type="text" id="driverCode" class="input-field w-full px-4 py-2 rounded-lg" placeholder="e.g., JSMITH123">
</div>
<div>
<label for="tripNumber" class="block text-sm font-medium text-gray-300 mb-1">Trip Number</label>
<input type="text" id="tripNumber" class="input-field w-full px-4 py-2 rounded-lg" placeholder="e.g., 987654">
</div>
<div>
<label for="destinationAddress" class="block text-sm font-medium text-gray-300 mb-1">Destination Address</label>
<input type="text" id="destinationAddress" class="input-field w-full px-4 py-2 rounded-lg" placeholder="e.g., 1600 Amphitheatre Parkway, Mountain View, CA">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="alertRadius" class="block text-sm font-medium text-gray-300 mb-1">Alert Radius (mi)</label>
<input type="number" id="alertRadius" class="input-field w-full px-4 py-2 rounded-lg" value="50">
</div>
<div>
<label for="deliveryTime" class="block text-sm font-medium text-gray-300 mb-1">Delivery Appointment</label>
<input type="datetime-local" id="deliveryTime" class="input-field w-full px-4 py-2 rounded-lg text-gray-400">
</div>
</div>
<button id="trackButton" class="btn-primary w-full py-3 rounded-lg font-semibold text-white transition duration-200 flex items-center justify-center">
Set Destination & Track
</button>
</div>
<div id="tracking-display" class="hidden">
<div id="statusCard" class="status-card p-5 rounded-lg shadow-lg mb-4">
<div class="flex justify-between items-center mb-3">
<h2 class="text-xl font-semibold text-white">Tracking Status</h2>
<div id="spinner" class="spinner w-5 h-5 rounded-full border-2 border-blue-500 hidden"></div>
</div>
<div id="distance-status" class="text-lg text-center font-mono py-2 rounded-md bg-gray-700 text-gray-400">
Initializing...
</div>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<button id="startButton" class="btn-success w-full py-3 rounded-lg font-semibold text-white transition duration-200" disabled>Start Layover</button>
<button id="endButton" class="btn-secondary w-full py-3 rounded-lg font-semibold text-white transition duration-200" disabled>End Layover</button>
</div>
<div id="layover-details" class="text-sm space-y-2 text-gray-400 hidden">
<p><strong>Start Time:</strong> <span id="startTimeDisplay"></span></p>
<p><strong>Start Location:</strong> <span id="startLocationDisplay"></span></p>
<p><strong>End Time:</strong> <span id="endTimeDisplay">Not set</span></p>
<p><strong>Duration:</strong> <span id="durationDisplay"></span></p>
</div>
<div class="mt-6 grid grid-cols-2 gap-4">
<button id="emailButton" class="btn-secondary w-full py-3 rounded-lg font-semibold" disabled>Compose Email</button>
<button id="resetButton" class="btn-danger w-full py-3 rounded-lg font-semibold">Reset Trip</button>
</div>
</div>
<p id="error-message" class="text-red-400 text-center mt-4 h-5"></p>
</main>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const setupForm = document.getElementById('setup-form');
const trackingDisplay = document.getElementById('tracking-display');
const driverCodeInput = document.getElementById('driverCode');
const tripNumberInput = document.getElementById('tripNumber');
const destinationAddressInput = document.getElementById('destinationAddress');
const alertRadiusInput = document.getElementById('alertRadius');
const deliveryTimeInput = document.getElementById('deliveryTime');
const trackButton = document.getElementById('trackButton');
const startButton = document.getElementById('startButton');
const endButton = document.getElementById('endButton');
const emailButton = document.getElementById('emailButton');
const resetButton = document.getElementById('resetButton');
const distanceStatus = document.getElementById('distance-status');
const spinner = document.getElementById('spinner');
const errorMessage = document.getElementById('error-message');
const layoverDetails = document.getElementById('layover-details');
const startTimeDisplay = document.getElementById('startTimeDisplay');
const startLocationDisplay = document.getElementById('startLocationDisplay');
const endTimeDisplay = document.getElementById('endTimeDisplay');
const durationDisplay = document.getElementById('durationDisplay');
let state = {};
let watchId = null;
let locationTimeout = null;
const initialState = {
isTracking: false,
isInGeofence: false,
layoverStarted: false,
driverCode: '',
tripNumber: '',
destinationAddress: '',
alertRadius: 50,
deliveryTime: '',
destinationCoords: null,
startTime: null,
startLocation: null,
endTime: null,
};
function loadState() {
const savedState = localStorage.getItem('geoLayoverState');
state = savedState ? JSON.parse(savedState) : { ...initialState };
driverCodeInput.value = state.driverCode;
tripNumberInput.value = state.tripNumber;
destinationAddressInput.value = state.destinationAddress;
alertRadiusInput.value = state.alertRadius;
deliveryTimeInput.value = state.deliveryTime;
render();
if(state.isTracking) {
startLocationTracking();
}
}
function saveState() {
localStorage.setItem('geoLayoverState', JSON.stringify(state));
}
function showErrorMessage(message) {
errorMessage.textContent = message;
setTimeout(() => { errorMessage.textContent = ''; }, 4000);
}
function haversineDistance(coords1, coords2) {
function toRad(x) { return x * Math.PI / 180; }
const R = 3958.8;
const dLat = toRad(coords2.lat - coords1.lat);
const dLon = toRad(coords2.lon - coords1.lon);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(coords1.lat)) * Math.cos(toRad(coords2.lat)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function formatDate(isoString) {
if (!isoString) return 'N/A';
return new Date(isoString).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
}
function calculateDuration() {
if (!state.startTime || !state.endTime) return 'Calculating...';
let diff = Math.abs(new Date(state.endTime) - new Date(state.startTime)) / 1000;
const days = Math.floor(diff / 86400); diff -= days * 86400;
const hours = Math.floor(diff / 3600); diff -= hours * 3600;
const minutes = Math.floor(diff / 60);
return [days > 0 ? `${days}d` : '', hours > 0 ? `${hours}h` : '', `${minutes}m`].filter(Boolean).join(' ');
}
async function geocodeAddress(address) {
spinner.classList.remove('hidden');
try {
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`);
const data = await response.json();
if (data && data.length > 0) return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) };
showErrorMessage('Could not find that address. Be more specific.');
return null;
} catch (error) {
showErrorMessage('Network error during geocoding.');
return null;
} finally {
spinner.classList.add('hidden');
}
}
const locationUpdateCallback = (position) => {
clearTimeout(locationTimeout);
spinner.classList.add('hidden');
const currentCoords = { lat: position.coords.latitude, lon: position.coords.longitude };
if (state.destinationCoords) {
const distance = haversineDistance(currentCoords, state.destinationCoords);
distanceStatus.textContent = `${distance.toFixed(1)} miles to destination`;
if (distance <= state.alertRadius) {
if (!state.isInGeofence) if ('vibrate' in navigator) navigator.vibrate(500);
state.isInGeofence = true;
distanceStatus.classList.replace('bg-gray-700', 'bg-green-700');
distanceStatus.classList.replace('text-gray-400', 'text-white');
} else {
state.isInGeofence = false;
distanceStatus.classList.replace('bg-green-700', 'bg-gray-700');
distanceStatus.classList.replace('text-white', 'text-gray-400');
}
renderButtons();
saveState();
}
};
const locationErrorCallback = (error) => {
clearTimeout(locationTimeout);
spinner.classList.add('hidden');
distanceStatus.textContent = `Location Error`;
distanceStatus.classList.add('bg-red-700', 'text-white');
showErrorMessage(`Error: ${error.message}. Check permissions.`);
stopLocationTracking();
};
function startLocationTracking() {
if (watchId) navigator.geolocation.clearWatch(watchId);
spinner.classList.remove('hidden');
distanceStatus.textContent = 'Initializing...';
distanceStatus.className = 'text-lg text-center font-mono py-2 rounded-md bg-gray-700 text-gray-400';
locationTimeout = setTimeout(() => {
spinner.classList.add('hidden');
distanceStatus.textContent = 'GPS Timeout';
distanceStatus.classList.add('bg-red-700', 'text-white');
showErrorMessage('Could not get location. Please check permissions and signal.');
}, 15000);
watchId = navigator.geolocation.watchPosition(
locationUpdateCallback,
locationErrorCallback,
{ enableHighAccuracy: true, maximumAge: 10000, timeout: 20000 }
);
}
function stopLocationTracking() {
if (watchId) navigator.geolocation.clearWatch(watchId);
watchId = null;
clearTimeout(locationTimeout);
spinner.classList.add('hidden');
}
function render() {
if (state.isTracking) {
setupForm.classList.add('hidden');
trackingDisplay.classList.remove('hidden');
} else {
setupForm.classList.remove('hidden');
trackingDisplay.classList.add('hidden');
}
if (state.layoverStarted) {
layoverDetails.classList.remove('hidden');
startTimeDisplay.textContent = formatDate(state.startTime);
startLocationDisplay.textContent = state.startLocation || 'Fetching...';
} else {
layoverDetails.classList.add('hidden');
}
if(state.endTime) {
endTimeDisplay.textContent = formatDate(state.endTime);
durationDisplay.textContent = calculateDuration();
} else {
endTimeDisplay.textContent = 'Not set';
durationDisplay.textContent = 'Calculating...';
}
renderButtons();
}
function renderButtons() {
startButton.disabled = !state.isInGeofence || state.layoverStarted;
endButton.disabled = !state.layoverStarted || !!state.endTime;
endButton.classList.toggle('btn-primary', state.layoverStarted && !state.endTime);
endButton.classList.toggle('btn-secondary', !state.layoverStarted || !!state.endTime);
emailButton.disabled = !state.endTime;
emailButton.classList.toggle('btn-primary', !!state.endTime);
emailButton.classList.toggle('btn-secondary', !state.endTime);
}
trackButton.addEventListener('click', async () => {
const driverCode = driverCodeInput.value.trim();
const tripNumber = tripNumberInput.value.trim();
const destinationAddress = destinationAddressInput.value.trim();
const deliveryTime = deliveryTimeInput.value;
if (!driverCode || !tripNumber || !destinationAddress || !deliveryTime) {
showErrorMessage('Please fill out all fields.');
return;
}
const coords = await geocodeAddress(destinationAddress);
if (!coords) return;
state = {...state, driverCode, tripNumber, destinationAddress, deliveryTime, alertRadius: parseFloat(alertRadiusInput.value) || 50, destinationCoords: coords, isTracking: true };
saveState();
render();
startLocationTracking();
});
startButton.addEventListener('click', () => {
state.layoverStarted = true;
state.startTime = new Date().toISOString();
navigator.geolocation.getCurrentPosition(async (position) => {
const { latitude, longitude } = position.coords;
try {
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}`);
const data = await response.json();
state.startLocation = data.display_name || `Lat: ${latitude.toFixed(2)}, Lon: ${longitude.toFixed(2)}`;
} catch {
state.startLocation = `Lat: ${latitude.toFixed(2)}, Lon: ${longitude.toFixed(2)}`;
} finally { saveState(); render(); }
});
saveState();
render();
});
endButton.addEventListener('click', () => {
state.endTime = new Date().toISOString();
stopLocationTracking();
saveState();
render();
});
emailButton.addEventListener('click', () => {
const subject = `Layover Report - Driver: ${state.driverCode}, Trip: ${state.tripNumber}`;
const body = `Hi Dispatch,\n\nThis is my layover report for Trip #${state.tripNumber}.\n\nDriver Code: ${state.driverCode}\nTrip Number: ${state.tripNumber}\nScheduled Delivery: ${formatDate(state.deliveryTime)}\n\nLayover Start Time: ${formatDate(state.startTime)}\nLayover Start Location: ${state.startLocation}\nLayover End Time: ${formatDate(state.endTime)}\n\nTotal Layover Duration: ${calculateDuration()}\n\nThank you.`;
window.location.href = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
});
resetButton.addEventListener('click', () => {
if (confirm('Are you sure you want to reset and start a new trip?')) {
stopLocationTracking();
const driverCode = state.driverCode;
state = { ...initialState, driverCode };
saveState();
loadState();
render();
}
});
loadState();
});
</script>
</body>
</html>