Files
tankstopp-app/internal/views/pages/fuelstops.templ
T
2025-07-07 01:44:12 +02:00

815 lines
28 KiB
Plaintext

package pages
import (
"tankstopp/internal/currency"
"tankstopp/internal/models"
"tankstopp/internal/views/components"
)
templ AddFuelStopPage(user *models.User, username string, vehicles []models.Vehicle, currencies []currency.Currency) {
@components.BaseLayout("Add Fuel Stop", user, username) {
@components.PageHeader("Add Fuel Stop", "Record a new fuel stop")
<div class="page-body">
<div class="container-xl">
<div class="row justify-content-center">
<div class="col-12 col-lg-8">
<form method="POST" class="card">
<div class="card-header">
<h3 class="card-title">Fuel Stop Details</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
@components.FormGroup("Date", "") {
@components.DateInput("date", "", true)
}
</div>
<div class="col-md-6">
@components.FormGroup("Vehicle", "") {
@components.VehicleSelect("vehicle_id", 0, vehicles, true)
}
</div>
</div>
<div class="row">
<div class="col-md-6">
@components.FormGroup("Station Name", "") {
<div class="input-group">
@components.Input("station_name", "text", "Enter station name", "", false)
<button class="btn btn-outline-secondary" type="button" onclick="findNearbyStations()">
@components.Icon("search", 24)
Find Nearby
</button>
</div>
}
</div>
<div class="col-md-6">
@components.FormGroup("Location", "") {
@components.Input("location", "text", "Enter location", "", false)
}
</div>
</div>
<!-- Station Search Results Modal -->
<div class="modal fade" id="stationSearchModal" tabindex="-1" aria-labelledby="stationSearchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="stationSearchModalLabel">Nearby Fuel Stations</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="stationSearchResults">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Searching...</span>
</div>
<p class="mt-2">Finding nearby fuel stations...</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-outline-primary" onclick="showManualEntry()">Enter Manually</button>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
@components.FormGroup("Fuel Type", "") {
@components.FuelTypeSelect("fuel_type", "", true)
}
</div>
<div class="col-md-6">
@components.FormGroup("Amount (Liters)", "") {
@components.NumberInput("amount", "0.00", 0.0, "0.01", 0.0, true)
}
</div>
</div>
<div class="row">
<div class="col-md-4">
@components.FormGroup("Price per Liter", "") {
<div class="input-group">
@components.NumberInput("price_per_liter", "0.000", 0.0, "0.001", 0.0, true)
<span class="input-group-text" id="price-currency">{ user.BaseCurrency }</span>
</div>
}
</div>
<div class="col-md-4">
@components.FormGroup("Total Cost", "") {
<div class="input-group">
@components.NumberInput("total_cost", "0.00", 0.0, "0.01", 0.0, true)
<span class="input-group-text" id="total-currency">{ user.BaseCurrency }</span>
</div>
}
</div>
<div class="col-md-4">
@components.FormGroup("Currency", "") {
@components.CurrencySelect("currency", user.BaseCurrency, currencies)
}
</div>
</div>
<div class="row">
<div class="col-md-6">
@components.FormGroup("Odometer Reading (km)", "") {
@components.NumberInput("odometer", "0", 0.0, "1", 0.0, false)
}
</div>
<div class="col-md-6">
@components.FormGroup("Trip Length (km)", "") {
@components.NumberInput("trip_length", "0.0", 0.0, "0.1", 0.0, false)
}
</div>
</div>
<div class="row">
<div class="col-12">
@components.FormGroup("Notes", "") {
@components.TextArea("notes", "Optional notes about this fuel stop", "", 3)
}
</div>
</div>
</div>
<div class="card-footer text-end">
<div class="d-flex">
<a href="/dashboard" class="btn btn-link">Cancel</a>
<button type="submit" class="btn btn-primary ms-auto">
@components.Icon("plus", 24)
Add Fuel Stop
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@FuelStopScript(vehicles)
}
}
templ EditFuelStopPage(user *models.User, username string, stop *models.FuelStop, vehicles []models.Vehicle, currencies []currency.Currency) {
@components.BaseLayout("Edit Fuel Stop", user, username) {
@components.PageHeader("Edit Fuel Stop", "Update fuel stop details")
<div class="page-body">
<div class="container-xl">
<div class="row justify-content-center">
<div class="col-12 col-lg-8">
<form method="POST" class="card">
<div class="card-header">
<h3 class="card-title">Fuel Stop Details</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
@components.FormGroup("Date", "") {
@components.DateInput("date", stop.Date.Format("2006-01-02"), true)
}
</div>
<div class="col-md-6">
@components.FormGroup("Vehicle", "") {
@components.VehicleSelect("vehicle_id", stop.VehicleID, vehicles, true)
}
</div>
</div>
<div class="row">
<div class="col-md-6">
@components.FormGroup("Station Name", "") {
<div class="input-group">
@components.Input("station_name", "text", "Enter station name", stop.StationName, false)
<button class="btn btn-outline-secondary" type="button" onclick="findNearbyStations()">
@components.Icon("search", 24)
Find Nearby
</button>
</div>
}
</div>
<div class="col-md-6">
@components.FormGroup("Location", "") {
@components.Input("location", "text", "Enter location", stop.Location, false)
}
</div>
</div>
<!-- Station Search Results Modal -->
<div class="modal fade" id="stationSearchModal" tabindex="-1" aria-labelledby="stationSearchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="stationSearchModalLabel">Nearby Fuel Stations</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="stationSearchResults">
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Searching...</span>
</div>
<p class="mt-2">Finding nearby fuel stations...</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-outline-primary" onclick="showManualEntry()">Enter Manually</button>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
@components.FormGroup("Fuel Type", "") {
@components.FuelTypeSelect("fuel_type", stop.FuelType, true)
}
</div>
<div class="col-md-6">
@components.FormGroup("Amount (Liters)", "") {
@components.NumberInput("amount", "0.00", stop.Liters, "0.01", 0.0, true)
}
</div>
</div>
<div class="row">
<div class="col-md-4">
@components.FormGroup("Price per Liter", "") {
<div class="input-group">
@components.NumberInput("price_per_liter", "0.000", stop.PricePerL, "0.001", 0.0, true)
<span class="input-group-text" id="price-currency">{ stop.Currency }</span>
</div>
}
</div>
<div class="col-md-4">
@components.FormGroup("Total Cost", "") {
<div class="input-group">
@components.NumberInput("total_cost", "0.00", stop.TotalPrice, "0.01", 0.0, true)
<span class="input-group-text" id="total-currency">{ stop.Currency }</span>
</div>
}
</div>
<div class="col-md-4">
@components.FormGroup("Currency", "") {
@components.CurrencySelect("currency", stop.Currency, currencies)
}
</div>
</div>
<div class="row">
<div class="col-md-6">
@components.FormGroup("Odometer Reading (km)", "") {
@components.NumberInput("odometer", "0", float64(stop.Odometer), "1", 0.0, false)
}
</div>
<div class="col-md-6">
@components.FormGroup("Trip Length (km)", "") {
@components.NumberInput("trip_length", "0.0", stop.TripLength, "0.1", 0.0, false)
}
</div>
</div>
<div class="row">
<div class="col-12">
@components.FormGroup("Notes", "") {
@components.TextArea("notes", "Optional notes about this fuel stop", stop.Notes, 3)
}
</div>
</div>
</div>
<div class="card-footer text-end">
<div class="d-flex">
<a href="/dashboard" class="btn btn-link">Cancel</a>
<button type="submit" class="btn btn-primary ms-auto">
@components.Icon("check", 24)
Update Fuel Stop
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@FuelStopScript(vehicles)
}
}
script FuelStopScript(vehicles []models.Vehicle) {
// Update currency display when currency dropdown changes
document.addEventListener('DOMContentLoaded', function() {
const currencySelect = document.querySelector('select[name="currency"]');
const priceCurrency = document.getElementById('price-currency');
const totalCurrency = document.getElementById('total-currency');
if (currencySelect) {
currencySelect.addEventListener('change', function() {
const selectedCurrency = this.value;
if (priceCurrency) priceCurrency.textContent = selectedCurrency;
if (totalCurrency) totalCurrency.textContent = selectedCurrency;
});
}
// Update fuel type when vehicle is selected
const vehicleSelect = document.querySelector('select[name="vehicle_id"]');
const fuelTypeSelect = document.querySelector('select[name="fuel_type"]');
if (vehicleSelect && fuelTypeSelect) {
vehicleSelect.addEventListener('change', async function() {
const selectedVehicleId = this.value;
if (selectedVehicleId) {
try {
// Fetch vehicle information from API
const response = await fetch(`/api/vehicles/${selectedVehicleId}`);
if (response.ok) {
const vehicle = await response.json();
if (vehicle.fuel_type) {
fuelTypeSelect.value = vehicle.fuel_type;
}
} else {
console.warn('Failed to fetch vehicle information');
}
} catch (error) {
console.error('Error fetching vehicle information:', error);
}
} else {
// Reset fuel type when no vehicle is selected
fuelTypeSelect.value = '';
}
});
}
// Auto-calculate total cost when amount or price per liter changes
const amountInput = document.querySelector('input[name="amount"]');
const priceInput = document.querySelector('input[name="price_per_liter"]');
const totalInput = document.querySelector('input[name="total_cost"]');
function calculateTotal() {
if (amountInput && priceInput && totalInput) {
const amount = parseFloat(amountInput.value) || 0;
const price = parseFloat(priceInput.value) || 0;
const total = amount * price;
totalInput.value = total.toFixed(2);
}
}
if (amountInput) {
amountInput.addEventListener('input', calculateTotal);
}
if (priceInput) {
priceInput.addEventListener('input', calculateTotal);
}
// Also calculate total when total is changed (reverse calculation)
if (totalInput && amountInput) {
totalInput.addEventListener('input', function() {
const total = parseFloat(this.value) || 0;
const amount = parseFloat(amountInput.value) || 0;
if (amount > 0) {
const pricePerLiter = total / amount;
if (priceInput) {
priceInput.value = pricePerLiter.toFixed(3);
}
}
});
}
});
// Check if page is served over HTTPS
function isSecureContext() {
return location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1';
}
// Debug function for location issues
function debugLocationInfo() {
console.log('=== Location Debug Info ===');
console.log('Protocol:', location.protocol);
console.log('Hostname:', location.hostname);
console.log('Is secure context:', isSecureContext());
console.log('Geolocation supported:', !!navigator.geolocation);
console.log('Permissions API supported:', !!navigator.permissions);
if (navigator.permissions) {
navigator.permissions.query({name: 'geolocation'}).then(function(result) {
console.log('Geolocation permission:', result.state);
}).catch(function(error) {
console.log('Permission query error:', error);
});
}
}
// Fuel Station Search Functions
window.findNearbyStations = function() {
// Debug location setup
debugLocationInfo();
// Check if we're in a secure context
if (!isSecureContext()) {
showStationSearchError('Geolocation requires HTTPS. Please access this page via HTTPS or use manual entry.');
return;
}
const modal = new bootstrap.Modal(document.getElementById('stationSearchModal'));
modal.show();
// Reset modal content
document.getElementById('stationSearchResults').innerHTML = `
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Searching...</span>
</div>
<p class="mt-2">Finding nearby fuel stations...</p>
</div>
`;
// Get user's location with improved error handling
if (navigator.geolocation) {
console.log('Starting geolocation request...');
// Update status to show we're requesting location
document.getElementById('stationSearchResults').innerHTML = `
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Getting location...</span>
</div>
<p class="mt-2">Requesting your location...</p>
<small class="text-muted">Please allow location access when prompted</small>
</div>
`;
navigator.geolocation.getCurrentPosition(
function(position) {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
console.log('Location obtained:', lat, lon);
console.log('Accuracy:', position.coords.accuracy + 'm');
console.log('Timestamp:', new Date(position.timestamp));
searchNearbyStations(lat, lon);
},
function(error) {
console.error('Geolocation error:', error);
console.error('Error code:', error.code);
console.error('Error message:', error.message);
let errorMessage = 'Unable to get your location. ';
let showRetryOption = false;
switch(error.code) {
case error.PERMISSION_DENIED:
errorMessage += 'Location access was denied. Please enable location services, refresh the page, and try again.';
if (!isSecureContext()) {
errorMessage += ' Note: This page requires HTTPS for location access.';
}
break;
case error.POSITION_UNAVAILABLE:
errorMessage += 'Location information is unavailable. Please check your GPS settings or try again.';
showRetryOption = true;
break;
case error.TIMEOUT:
errorMessage += 'Location request timed out. Trying with lower accuracy...';
// Try again with lower accuracy
tryLowAccuracyLocation();
return;
default:
errorMessage += 'An unknown error occurred while retrieving location.';
showRetryOption = true;
break;
}
showStationSearchError(errorMessage, showRetryOption);
},
{
enableHighAccuracy: true,
timeout: 15000, // Increased timeout to 15 seconds
maximumAge: 300000 // 5 minutes
}
);
} else {
showStationSearchError('Geolocation is not supported by this browser. Please enter station details manually.');
}
};
// Fallback function for low accuracy location
function tryLowAccuracyLocation() {
document.getElementById('stationSearchResults').innerHTML = `
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Trying low accuracy...</span>
</div>
<p class="mt-2">Trying with lower accuracy...</p>
</div>
`;
navigator.geolocation.getCurrentPosition(
function(position) {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
console.log('Low accuracy location obtained:', lat, lon);
searchNearbyStations(lat, lon);
},
function(error) {
console.error('Low accuracy geolocation error:', error);
showStationSearchError('Unable to get your location even with low accuracy. Please enter station details manually or try again later.');
},
{
enableHighAccuracy: false, // Use less accurate but faster location
timeout: 30000, // Longer timeout for low accuracy
maximumAge: 600000 // 10 minutes cache for low accuracy
}
);
};
function searchNearbyStations(lat, lon) {
console.log('Searching for stations near:', lat, lon);
// Update status to show we're searching
document.getElementById('stationSearchResults').innerHTML = `
<div class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Searching...</span>
</div>
<p class="mt-2">Searching for nearby fuel stations...</p>
<small class="text-muted">This may take a few seconds</small>
</div>
`;
const overpassUrl = 'https://overpass-api.de/api/interpreter';
const query = `
[out:json][timeout:30];
(
node["amenity"="fuel"](around:5000,${lat},${lon});
way["amenity"="fuel"](around:5000,${lat},${lon});
relation["amenity"="fuel"](around:5000,${lat},${lon});
);
out center meta;
`;
console.log('Overpass query:', query);
// Add timeout to the fetch request
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
fetch(overpassUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'data=' + encodeURIComponent(query),
signal: controller.signal
})
.then(response => {
clearTimeout(timeoutId);
console.log('API response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('API response data:', data);
if (data.remark && data.remark.includes('timeout')) {
throw new Error('API request timed out');
}
if (data.elements && data.elements.length > 0) {
console.log(`Found ${data.elements.length} stations`);
displayStationResults(data.elements, lat, lon);
} else {
console.log('No stations found in API response');
showStationSearchError('No fuel stations found within 5km of your location. Try searching manually or check a different area.');
}
})
.catch(error => {
clearTimeout(timeoutId);
console.error('Error searching for stations:', error);
let errorMessage = 'Error searching for fuel stations. ';
if (error.name === 'AbortError') {
errorMessage += 'The search timed out. Please try again or enter station details manually.';
} else if (error.message.includes('HTTP error')) {
errorMessage += 'The map service is temporarily unavailable. Please try again later.';
} else if (error.message.includes('Failed to fetch')) {
errorMessage += 'Network error. Please check your internet connection and try again.';
} else {
errorMessage += 'Please try again or enter station details manually.';
}
showStationSearchError(errorMessage);
});
}
function displayStationResults(stations, userLat, userLon) {
// Calculate distances and sort by distance
const stationsWithDistance = stations.map(station => {
const stationLat = station.lat || (station.center && station.center.lat);
const stationLon = station.lon || (station.center && station.center.lon);
if (stationLat && stationLon) {
const distance = calculateDistance(userLat, userLon, stationLat, stationLon);
return {
...station,
distance: distance,
displayLat: stationLat,
displayLon: stationLon
};
}
return null;
}).filter(station => station !== null);
stationsWithDistance.sort((a, b) => a.distance - b.distance);
const resultsHTML = stationsWithDistance.map(station => {
const name = station.tags.name || station.tags.brand || 'Unknown Station';
const address = [
station.tags['addr:street'],
station.tags['addr:housenumber'],
station.tags['addr:city'],
station.tags['addr:postcode']
].filter(Boolean).join(' ');
const brand = station.tags.brand || '';
const operator = station.tags.operator || '';
const displayName = brand || operator || name;
return `
<div class="card mb-2 station-result" style="cursor: pointer;"
onclick="selectStation('${displayName}', '${address}', ${station.displayLat}, ${station.displayLon})">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="card-title mb-1">${displayName}</h6>
<p class="card-text text-muted mb-0">${address}</p>
${brand && brand !== displayName ? `<small class="text-muted">${brand}</small>` : ''}
</div>
<div class="text-end">
<span class="badge bg-primary">${station.distance.toFixed(1)} km</span>
</div>
</div>
</div>
</div>
`;
}).join('');
document.getElementById('stationSearchResults').innerHTML = resultsHTML ||
'<div class="text-center text-muted">No fuel stations found nearby.</div>';
}
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Earth's radius in km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function showStationSearchError(message, showRetryOption = false) {
const retryButton = showRetryOption ? `
<button type="button" class="btn btn-outline-secondary me-2" onclick="findNearbyStations()">
Try Again
</button>
` : '';
document.getElementById('stationSearchResults').innerHTML = `
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle"></i> ${message}
</div>
<div class="mt-3">
${retryButton}
<button type="button" class="btn btn-outline-primary" onclick="showManualEntry()">
Enter Station Details Manually
</button>
</div>
`;
}
window.showManualEntry = function() {
document.getElementById('stationSearchResults').innerHTML = `
<div class="alert alert-info" role="alert">
<h6>Manual Entry</h6>
<p>Enter the station details manually:</p>
</div>
<form onsubmit="return selectManualStation(event)">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Station Name</label>
<input type="text" class="form-control" id="manual-station-name" placeholder="e.g., Shell, TOTAL, Aral" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Brand (optional)</label>
<select class="form-select" id="manual-station-brand">
<option value="">Select brand...</option>
<option value="Shell">Shell</option>
<option value="TOTAL">TOTAL</option>
<option value="Aral">Aral</option>
<option value="Esso">Esso</option>
<option value="BP">BP</option>
<option value="AGIP">AGIP</option>
<option value="OMV">OMV</option>
<option value="JET">JET</option>
<option value="Other">Other</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Address</label>
<input type="text" class="form-control" id="manual-station-address" placeholder="Street, City, Country" required>
</div>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-secondary" onclick="findNearbyStations()">
Back to Search
</button>
<button type="submit" class="btn btn-primary">
Use This Station
</button>
</div>
</form>
`;
};
window.selectManualStation = function(event) {
event.preventDefault();
const stationName = document.getElementById('manual-station-name').value;
const stationBrand = document.getElementById('manual-station-brand').value;
const stationAddress = document.getElementById('manual-station-address').value;
// Use brand name if provided, otherwise use the entered name
const finalName = stationBrand && stationBrand !== 'Other' ? stationBrand : stationName;
// Fill form fields
document.querySelector('input[name="station_name"]').value = finalName;
document.querySelector('input[name="location"]').value = stationAddress;
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('stationSearchModal'));
if (modal) {
modal.hide();
}
// Show success message
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-white bg-success border-0';
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
Station entered manually: ${finalName}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
document.body.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
// Remove toast after it hides
toast.addEventListener('hidden.bs.toast', function() {
document.body.removeChild(toast);
});
return false;
};
window.selectStation = function(name, address, lat, lon) {
// Fill form fields
document.querySelector('input[name="station_name"]').value = name;
document.querySelector('input[name="location"]').value = address;
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('stationSearchModal'));
if (modal) {
modal.hide();
}
// Show success message
const toast = document.createElement('div');
toast.className = 'toast align-items-center text-white bg-success border-0';
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
Station selected: ${name}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
`;
document.body.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
// Remove toast after it hides
toast.addEventListener('hidden.bs.toast', function() {
document.body.removeChild(toast);
});
};
}