first commit

This commit is contained in:
2025-07-07 01:44:12 +02:00
commit bf68bde4ce
72 changed files with 29002 additions and 0 deletions
+403
View File
@@ -0,0 +1,403 @@
package handlers
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"time"
"tankstopp/internal/models"
"github.com/gorilla/mux"
)
// APIGetFuelStopsHandler returns fuel stops as JSON
func (h *Handler) APIGetFuelStopsHandler(w http.ResponseWriter, r *http.Request) {
userID, _ := h.getCurrentUser(r)
if userID == 0 {
h.writeJSONError(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Parse query parameters for filtering (not used in simplified implementation)
_ = r.URL.Query().Get("vehicle_id")
_ = r.URL.Query().Get("fuel_type")
_ = r.URL.Query().Get("date_from")
_ = r.URL.Query().Get("date_to")
limitStr := r.URL.Query().Get("limit")
offsetStr := r.URL.Query().Get("offset")
// Parse limit and offset
limit := 50 // default limit
offset := 0 // default offset
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 {
limit = l
}
}
if offsetStr != "" {
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
offset = o
}
}
// Get fuel stops (simplified - using existing method)
stops, err := h.db.GetFuelStops(userID)
if err != nil {
log.Printf("Error getting fuel stops: %v", err)
h.writeJSONError(w, "Failed to retrieve fuel stops", http.StatusInternalServerError)
return
}
// Apply basic pagination
totalCount := len(stops)
end := offset + limit
if end > len(stops) {
end = len(stops)
}
if offset < len(stops) {
stops = stops[offset:end]
} else {
stops = []models.FuelStop{}
}
// Prepare response
response := struct {
Data []models.FuelStop `json:"data"`
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"has_more"`
Pagination struct {
CurrentPage int `json:"current_page"`
TotalPages int `json:"total_pages"`
PerPage int `json:"per_page"`
} `json:"pagination"`
}{
Data: stops,
Total: totalCount,
Limit: limit,
Offset: offset,
HasMore: offset+len(stops) < totalCount,
}
// Calculate pagination
response.Pagination.PerPage = limit
response.Pagination.CurrentPage = (offset / limit) + 1
response.Pagination.TotalPages = (totalCount + limit - 1) / limit
h.writeJSONResponse(w, response, http.StatusOK)
}
// APICreateFuelStopHandler creates a new fuel stop via JSON API
func (h *Handler) APICreateFuelStopHandler(w http.ResponseWriter, r *http.Request) {
userID, _ := h.getCurrentUser(r)
if userID == 0 {
h.writeJSONError(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Parse JSON request body
var request struct {
VehicleID uint `json:"vehicle_id"`
Date string `json:"date"`
StationName string `json:"station_name"`
Location string `json:"location"`
FuelType string `json:"fuel_type"`
Liters float64 `json:"liters"`
PricePerL float64 `json:"price_per_l"`
TotalPrice float64 `json:"total_price"`
Currency string `json:"currency"`
Odometer int `json:"odometer"`
TripLength float64 `json:"trip_length"`
Notes string `json:"notes"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
h.writeJSONError(w, "Invalid JSON format", http.StatusBadRequest)
return
}
// Validate required fields
if err := h.validateAPIFuelStopRequest(&request); err != nil {
h.writeJSONError(w, err.Error(), http.StatusBadRequest)
return
}
// Parse date
date, err := time.Parse("2006-01-02", request.Date)
if err != nil {
h.writeJSONError(w, "Invalid date format. Use YYYY-MM-DD", http.StatusBadRequest)
return
}
// Get user's default currency if not provided
if request.Currency == "" {
user, err := h.db.GetUserByID(userID)
if err != nil {
log.Printf("Error getting user: %v", err)
h.writeJSONError(w, "Failed to retrieve user information", http.StatusInternalServerError)
return
}
request.Currency = user.BaseCurrency
}
// Create fuel stop model
fuelStop := &models.FuelStop{
UserID: userID,
VehicleID: request.VehicleID,
Date: date,
StationName: request.StationName,
Location: request.Location,
FuelType: request.FuelType,
Liters: request.Liters,
PricePerL: request.PricePerL,
TotalPrice: request.TotalPrice,
Currency: request.Currency,
Odometer: request.Odometer,
TripLength: request.TripLength,
Notes: request.Notes,
}
// Use station name as location if location is empty
if fuelStop.Location == "" {
fuelStop.Location = fuelStop.StationName
}
// Save to database
err = h.db.CreateFuelStopWithValidation(fuelStop)
if err != nil {
log.Printf("Error creating fuel stop: %v", err)
h.writeJSONError(w, "Failed to create fuel stop", http.StatusInternalServerError)
return
}
// Return created fuel stop
h.writeJSONResponse(w, map[string]interface{}{
"message": "Fuel stop created successfully",
"data": fuelStop,
}, http.StatusCreated)
}
// APIGetFuelStopStatsHandler returns fuel stop statistics as JSON
func (h *Handler) APIGetFuelStopStatsHandler(w http.ResponseWriter, r *http.Request) {
userID, _ := h.getCurrentUser(r)
if userID == 0 {
h.writeJSONError(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Parse query parameters (not used in simplified implementation)
_ = r.URL.Query().Get("date_from")
_ = r.URL.Query().Get("date_to")
groupBy := r.URL.Query().Get("group_by") // month, year, vehicle
// Get basic statistics
stats, err := h.db.GetFuelStopStats(userID)
if err != nil {
log.Printf("Error getting fuel stop stats: %v", err)
h.writeJSONError(w, "Failed to retrieve statistics", http.StatusInternalServerError)
return
}
// Prepare response structure
response := struct {
Basic *models.FuelStopStats `json:"basic"`
Daily []DailyStats `json:"daily,omitempty"`
Monthly []MonthlyStats `json:"monthly,omitempty"`
ByVehicle []VehicleStats `json:"by_vehicle,omitempty"`
Summary StatsSummary `json:"summary"`
}{
Basic: stats,
}
// Additional statistics would require more complex database queries
// For now, we'll just return basic stats
_ = groupBy // Acknowledge the parameter
// Calculate summary statistics
response.Summary = h.calculateStatsSummary(stats)
h.writeJSONResponse(w, response, http.StatusOK)
}
// Helper structs for statistics
type DailyStats struct {
Date string `json:"date"`
TotalStops int `json:"total_stops"`
TotalCost float64 `json:"total_cost"`
TotalLiters float64 `json:"total_liters"`
}
type MonthlyStats struct {
Month string `json:"month"`
Year int `json:"year"`
TotalStops int `json:"total_stops"`
TotalCost float64 `json:"total_cost"`
TotalLiters float64 `json:"total_liters"`
AvgPrice float64 `json:"avg_price"`
}
type VehicleStats struct {
VehicleID uint `json:"vehicle_id"`
VehicleName string `json:"vehicle_name"`
TotalStops int `json:"total_stops"`
TotalCost float64 `json:"total_cost"`
TotalLiters float64 `json:"total_liters"`
AvgPrice float64 `json:"avg_price"`
LastFillUp string `json:"last_fillup"`
}
type StatsSummary struct {
CostPerKm float64 `json:"cost_per_km"`
FuelEfficiency float64 `json:"fuel_efficiency"`
MonthlyAverage float64 `json:"monthly_average"`
WeeklyAverage float64 `json:"weekly_average"`
MostUsedStation string `json:"most_used_station"`
PreferredFuel string `json:"preferred_fuel"`
}
// validateAPIFuelStopRequest validates the JSON request for creating fuel stops
func (h *Handler) validateAPIFuelStopRequest(req *struct {
VehicleID uint `json:"vehicle_id"`
Date string `json:"date"`
StationName string `json:"station_name"`
Location string `json:"location"`
FuelType string `json:"fuel_type"`
Liters float64 `json:"liters"`
PricePerL float64 `json:"price_per_l"`
TotalPrice float64 `json:"total_price"`
Currency string `json:"currency"`
Odometer int `json:"odometer"`
TripLength float64 `json:"trip_length"`
Notes string `json:"notes"`
}) error {
if req.VehicleID == 0 {
return fmt.Errorf("vehicle_id is required")
}
if req.Date == "" {
return fmt.Errorf("date is required")
}
if req.StationName == "" && req.Location == "" {
return fmt.Errorf("station_name or location is required")
}
if req.FuelType == "" {
return fmt.Errorf("fuel_type is required")
}
if req.Liters <= 0 {
return fmt.Errorf("liters must be greater than 0")
}
if req.PricePerL <= 0 {
return fmt.Errorf("price_per_l must be greater than 0")
}
if req.TotalPrice <= 0 {
return fmt.Errorf("total_price must be greater than 0")
}
if req.Odometer < 0 {
return fmt.Errorf("odometer cannot be negative")
}
if req.TripLength < 0 {
return fmt.Errorf("trip_length cannot be negative")
}
if len(req.Notes) > 500 {
return fmt.Errorf("notes cannot be longer than 500 characters")
}
return nil
}
// APIGetVehicleHandler returns vehicle information as JSON
func (h *Handler) APIGetVehicleHandler(w http.ResponseWriter, r *http.Request) {
userID, _ := h.getCurrentUser(r)
if userID == 0 {
h.writeJSONError(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Get vehicle ID from URL path
vars := mux.Vars(r)
vehicleIDStr := vars["id"]
vehicleID, err := strconv.ParseUint(vehicleIDStr, 10, 32)
if err != nil {
h.writeJSONError(w, "Invalid vehicle ID", http.StatusBadRequest)
return
}
// Get vehicle from database
vehicle, err := h.db.GetVehicleByID(uint(vehicleID), userID)
if err != nil {
log.Printf("Error getting vehicle: %v", err)
h.writeJSONError(w, "Failed to retrieve vehicle", http.StatusInternalServerError)
return
}
if vehicle == nil {
h.writeJSONError(w, "Vehicle not found", http.StatusNotFound)
return
}
// Return vehicle information
h.writeJSONResponse(w, vehicle, http.StatusOK)
}
// calculateStatsSummary calculates additional summary statistics
func (h *Handler) calculateStatsSummary(stats *models.FuelStopStats) StatsSummary {
summary := StatsSummary{}
if stats.TotalStops > 0 {
// Calculate monthly average (assuming data spans multiple months)
monthlyAvg := stats.TotalSpent / 12 // This is a simplified calculation
summary.MonthlyAverage = monthlyAvg
// Calculate weekly average
summary.WeeklyAverage = monthlyAvg / 4.33 // Average weeks per month
// Calculate fuel efficiency (simplified)
summary.FuelEfficiency = stats.AverageConsumption
// These would require additional database queries in a real implementation
summary.MostUsedStation = "N/A"
summary.PreferredFuel = "N/A"
}
return summary
}
// writeJSONResponse writes a JSON response with the given status code
func (h *Handler) writeJSONResponse(w http.ResponseWriter, data interface{}, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("Error encoding JSON response: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// writeJSONError writes a JSON error response
func (h *Handler) writeJSONError(w http.ResponseWriter, message string, statusCode int) {
errorResponse := struct {
Error string `json:"error"`
Message string `json:"message"`
Code int `json:"code"`
}{
Error: http.StatusText(statusCode),
Message: message,
Code: statusCode,
}
h.writeJSONResponse(w, errorResponse, statusCode)
}
+271
View File
@@ -0,0 +1,271 @@
package handlers
import (
"context"
"fmt"
"log"
"net/http"
"strings"
"time"
"tankstopp/internal/auth"
"tankstopp/internal/currency"
"tankstopp/internal/models"
"tankstopp/internal/views/pages"
)
// RootHandler redirects to appropriate page based on authentication status
func (h *Handler) RootHandler(w http.ResponseWriter, r *http.Request) {
// Check if user is authenticated
sessionID, err := auth.GetSessionCookie(r)
if err != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
_, exists := h.sessionManager.GetSession(sessionID)
if !exists {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// User is authenticated, redirect to dashboard
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}
// LoginHandler handles user authentication
func (h *Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
component := pages.LoginPage("")
w.Header().Set("Content-Type", "text/html")
err := component.Render(r.Context(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
case "POST":
h.handleLogin(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleLogin processes login form submission
func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
log.Printf("Error parsing form: %v", err)
h.renderLoginWithError(w, "Invalid form data")
return
}
username := strings.TrimSpace(r.FormValue("username"))
password := r.FormValue("password")
if username == "" || password == "" {
h.renderLoginWithError(w, "Username and password are required")
return
}
// Get user from database
user, err := h.db.GetUserByUsername(username)
if err != nil {
log.Printf("Error getting user: %v", err)
h.renderLoginWithError(w, "Invalid username or password")
return
}
// Check if user exists
if user == nil {
h.renderLoginWithError(w, "Invalid username or password")
return
}
// Check password
if !user.CheckPassword(password) {
h.renderLoginWithError(w, "Invalid username or password")
return
}
// Create session
session := h.sessionManager.CreateSession(int(user.ID), user.Username)
// Set session cookie
cookie := &http.Cookie{
Name: "session_id",
Value: session.ID,
Path: "/",
HttpOnly: true,
Secure: false, // Set to true in production with HTTPS
SameSite: http.SameSiteStrictMode,
Expires: session.ExpiresAt,
}
http.SetCookie(w, cookie)
// Redirect to dashboard
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}
// renderLoginWithError renders the login page with an error message
func (h *Handler) renderLoginWithError(w http.ResponseWriter, errorMsg string) {
component := pages.LoginPage(errorMsg)
w.Header().Set("Content-Type", "text/html")
err := component.Render(context.Background(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// RegisterHandler handles user registration
func (h *Handler) RegisterHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
component := pages.RegisterPage("")
w.Header().Set("Content-Type", "text/html")
err := component.Render(r.Context(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
case "POST":
h.handleRegister(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleRegister processes registration form submission
func (h *Handler) handleRegister(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
log.Printf("Error parsing form: %v", err)
h.renderRegisterWithError(w, "Invalid form data")
return
}
// Parse form data
form := models.UserRegistrationForm{
Username: strings.TrimSpace(r.FormValue("username")),
Email: strings.TrimSpace(r.FormValue("email")),
Password: r.FormValue("password"),
ConfirmPassword: r.FormValue("confirm_password"),
BaseCurrency: r.FormValue("base_currency"),
}
// Validate form
if err := h.validateRegistrationForm(&form); err != nil {
h.renderRegisterWithError(w, err.Error())
return
}
// Convert form to user
user, err := form.ToUser()
if err != nil {
log.Printf("Error converting form to user: %v", err)
h.renderRegisterWithError(w, "Failed to create user")
return
}
// Create user in database
err = h.db.CreateUser(user)
if err != nil {
log.Printf("Error creating user: %v", err)
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
h.renderRegisterWithError(w, "Username or email already exists")
} else {
h.renderRegisterWithError(w, "Failed to create account")
}
return
}
// Redirect to login with success message
http.Redirect(w, r, "/login?success=Account+created+successfully", http.StatusSeeOther)
}
// validateRegistrationForm validates the registration form data
func (h *Handler) validateRegistrationForm(form *models.UserRegistrationForm) error {
if form.Username == "" {
return fmt.Errorf("Username is required")
}
if len(form.Username) < 3 {
return fmt.Errorf("Username must be at least 3 characters long")
}
if form.Email == "" {
return fmt.Errorf("Email is required")
}
if !strings.Contains(form.Email, "@") {
return fmt.Errorf("Invalid email address")
}
if form.Password == "" {
return fmt.Errorf("Password is required")
}
if len(form.Password) < 8 {
return fmt.Errorf("Password must be at least 8 characters long")
}
if form.Password != form.ConfirmPassword {
return fmt.Errorf("Passwords do not match")
}
if form.BaseCurrency == "" {
return fmt.Errorf("Base currency is required")
}
if !currency.IsValidCurrency(form.BaseCurrency) {
return fmt.Errorf("Invalid currency")
}
return nil
}
// renderRegisterWithError renders the registration page with an error message
func (h *Handler) renderRegisterWithError(w http.ResponseWriter, errorMsg string) {
component := pages.RegisterPage(errorMsg)
w.Header().Set("Content-Type", "text/html")
err := component.Render(context.Background(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// LogoutHandler handles user logout
func (h *Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Get session cookie
sessionID, err := auth.GetSessionCookie(r)
if err == nil {
// Remove session from session manager
h.sessionManager.DeleteSession(sessionID)
}
// Clear session cookie
cookie := &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
HttpOnly: true,
Secure: false, // Set to true in production with HTTPS
SameSite: http.SameSiteStrictMode,
Expires: time.Unix(0, 0), // Expire immediately
}
http.SetCookie(w, cookie)
// Redirect to login page
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
+151
View File
@@ -0,0 +1,151 @@
package handlers
import (
"log"
"net/http"
"tankstopp/internal/models"
"tankstopp/internal/views/pages"
)
// HomeHandler serves the main dashboard page
func (h *Handler) HomeHandler(w http.ResponseWriter, r *http.Request) {
userID, username := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Get the user object
user, err := h.db.GetUserByID(userID)
if err != nil {
log.Printf("Error getting user: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Get fuel stops for the user
stops, err := h.db.GetFuelStops(userID)
if err != nil {
log.Printf("Error getting fuel stops: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Get fuel stop statistics
stats, err := h.db.GetFuelStopStats(userID)
if err != nil {
log.Printf("Error getting fuel stop stats: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Get vehicles for the user
vehicles, err := h.db.GetVehicles(userID)
if err != nil {
log.Printf("Error getting vehicles: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Calculate dashboard statistics
totalStops := len(stops)
totalCost := stats.TotalSpent
// Calculate detailed consumption statistics
avgConsumption, _, _ := h.calculateConsumptionStats(stops)
if avgConsumption == 0 {
avgConsumption = stats.AverageConsumption // Fallback to basic stats
}
var lastFillUp *models.FuelStop
if len(stops) > 0 {
lastFillUp = &stops[0]
}
// Render dashboard using templ
component := pages.DashboardPage(user, username, stops, vehicles, totalStops, totalCost, avgConsumption, lastFillUp)
w.Header().Set("Content-Type", "text/html")
err = component.Render(r.Context(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// calculateConsumptionStats calculates consumption-related statistics
func (h *Handler) calculateConsumptionStats(stops []models.FuelStop) (float64, float64, float64) {
if len(stops) == 0 {
return 0, 0, 0
}
var totalLiters, totalKm float64
var consumptionReadings []float64
for _, stop := range stops {
totalLiters += stop.Liters
if stop.TripLength > 0 {
totalKm += stop.TripLength
// Calculate consumption for this stop (L/100km)
consumption := (stop.Liters / stop.TripLength) * 100
if consumption > 0 && consumption < 50 { // Filter out unrealistic values
consumptionReadings = append(consumptionReadings, consumption)
}
}
}
// Average consumption from individual readings
var avgConsumption float64
if len(consumptionReadings) > 0 {
var sum float64
for _, consumption := range consumptionReadings {
sum += consumption
}
avgConsumption = sum / float64(len(consumptionReadings))
}
// Overall consumption from totals
var overallConsumption float64
if totalKm > 0 {
overallConsumption = (totalLiters / totalKm) * 100
}
return avgConsumption, overallConsumption, totalKm
}
// calculateEfficiencyTrend calculates fuel efficiency trend over time
func (h *Handler) calculateEfficiencyTrend(stops []models.FuelStop) string {
if len(stops) < 2 {
return "insufficient_data"
}
// Get consumption for recent stops (last 5) vs older stops
recentStops := stops[:min(5, len(stops))]
olderStops := stops[min(5, len(stops)):]
recentAvg, _, _ := h.calculateConsumptionStats(recentStops)
olderAvg, _, _ := h.calculateConsumptionStats(olderStops)
if recentAvg == 0 || olderAvg == 0 {
return "insufficient_data"
}
diff := recentAvg - olderAvg
if diff < -0.5 {
return "improving" // Lower consumption is better
} else if diff > 0.5 {
return "worsening"
}
return "stable"
}
// min helper function
func min(a, b int) int {
if a < b {
return a
}
return b
}
+395
View File
@@ -0,0 +1,395 @@
package handlers
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"tankstopp/internal/currency"
"tankstopp/internal/models"
"tankstopp/internal/views/pages"
"github.com/gorilla/mux"
)
// AddFuelStopHandler handles adding new fuel stops
func (h *Handler) AddFuelStopHandler(w http.ResponseWriter, r *http.Request) {
userID, _ := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
switch r.Method {
case "GET":
// Get user for default currency
user, err := h.db.GetUserByID(userID)
if err != nil {
log.Printf("Error getting user: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Get vehicles for the user
vehicles, err := h.db.GetVehicles(userID)
if err != nil {
log.Printf("Error getting vehicles: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Render add fuel stop form using templ
currencies := currency.SupportedCurrencies()
component := pages.AddFuelStopPage(user, user.Username, vehicles, currencies)
w.Header().Set("Content-Type", "text/html")
err = component.Render(r.Context(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
case "POST":
h.handleAddFuelStop(w, r, userID)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleAddFuelStop processes the form submission for adding fuel stops
func (h *Handler) handleAddFuelStop(w http.ResponseWriter, r *http.Request, userID uint) {
err := r.ParseForm()
if err != nil {
log.Printf("Error parsing form: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Parse form data
form := models.FuelStopForm{
Date: strings.TrimSpace(r.FormValue("date")),
VehicleID: parseUint(r.FormValue("vehicle_id")),
StationName: strings.TrimSpace(r.FormValue("station_name")),
Location: strings.TrimSpace(r.FormValue("location")),
FuelType: r.FormValue("fuel_type"),
Liters: parseFloat(r.FormValue("amount")),
PricePerL: parseFloat(r.FormValue("price_per_liter")),
TotalPrice: parseFloat(r.FormValue("total_cost")),
Currency: r.FormValue("currency"),
Odometer: parseInt(r.FormValue("odometer")),
TripLength: parseFloat(r.FormValue("trip_length")),
Notes: r.FormValue("notes"),
}
// Validate form
if err := h.validateFuelStopForm(&form); err != nil {
log.Printf("Validation error: %v", err)
http.Redirect(w, r, "/add?error="+err.Error(), http.StatusSeeOther)
return
}
// Convert form to fuel stop
fuelStop, err := form.ToFuelStop(userID)
if err != nil {
log.Printf("Error converting form to fuel stop: %v", err)
http.Redirect(w, r, "/add?error=Invalid+date+format", http.StatusSeeOther)
return
}
// Use station name as location if location is empty
if fuelStop.Location == "" {
fuelStop.Location = fuelStop.StationName
}
// Save to database
err = h.db.CreateFuelStop(fuelStop)
if err != nil {
log.Printf("Error creating fuel stop: %v", err)
http.Redirect(w, r, "/add?error=Failed+to+save+fuel+stop", http.StatusSeeOther)
return
}
// Redirect to dashboard with success message
http.Redirect(w, r, "/dashboard?success=Fuel+stop+added+successfully", http.StatusSeeOther)
}
// EditFuelStopHandler handles editing existing fuel stops
func (h *Handler) EditFuelStopHandler(w http.ResponseWriter, r *http.Request) {
userID, _ := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Get the user object
user, err := h.db.GetUserByID(userID)
if err != nil {
log.Printf("Error getting user: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
vars := mux.Vars(r)
idInt, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
id := uint(idInt)
switch r.Method {
case "GET":
stop, err := h.db.GetFuelStopByID(id, userID)
if err != nil {
log.Printf("Error getting fuel stop: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if stop == nil {
http.Error(w, "Fuel stop not found", http.StatusNotFound)
return
}
// Get vehicles for the user
vehicles, err := h.db.GetVehicles(userID)
if err != nil {
log.Printf("Error getting vehicles: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Render edit fuel stop form using templ
currencies := currency.SupportedCurrencies()
component := pages.EditFuelStopPage(user, user.Username, stop, vehicles, currencies)
w.Header().Set("Content-Type", "text/html")
err = component.Render(r.Context(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
case "POST":
h.handleEditFuelStop(w, r, id, userID)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleEditFuelStop processes the form submission for editing fuel stops
func (h *Handler) handleEditFuelStop(w http.ResponseWriter, r *http.Request, id, userID uint) {
err := r.ParseForm()
if err != nil {
log.Printf("Error parsing form: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Get existing fuel stop
existingStop, err := h.db.GetFuelStopByID(id, userID)
if err != nil {
log.Printf("Error getting fuel stop: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if existingStop == nil {
http.Error(w, "Fuel stop not found", http.StatusNotFound)
return
}
// Parse form data
form := models.FuelStopForm{
Date: strings.TrimSpace(r.FormValue("date")),
VehicleID: parseUint(r.FormValue("vehicle_id")),
StationName: strings.TrimSpace(r.FormValue("station_name")),
Location: strings.TrimSpace(r.FormValue("location")),
FuelType: r.FormValue("fuel_type"),
Liters: parseFloat(r.FormValue("amount")),
PricePerL: parseFloat(r.FormValue("price_per_liter")),
TotalPrice: parseFloat(r.FormValue("total_cost")),
Currency: r.FormValue("currency"),
Odometer: parseInt(r.FormValue("odometer")),
TripLength: parseFloat(r.FormValue("trip_length")),
Notes: r.FormValue("notes"),
}
// Validate form
if err := h.validateFuelStopForm(&form); err != nil {
log.Printf("Validation error: %v", err)
http.Redirect(w, r, fmt.Sprintf("/edit/%d?error=%s", id, err.Error()), http.StatusSeeOther)
return
}
// Convert form to fuel stop
updatedStop, err := form.ToFuelStop(userID)
if err != nil {
log.Printf("Error converting form to fuel stop: %v", err)
http.Redirect(w, r, fmt.Sprintf("/edit/%d?error=Invalid+date+format", id), http.StatusSeeOther)
return
}
// Set the ID to update existing record
updatedStop.ID = id
// Use station name as location if location is empty
if updatedStop.Location == "" {
updatedStop.Location = updatedStop.StationName
}
// Update in database
err = h.db.UpdateFuelStop(updatedStop)
if err != nil {
log.Printf("Error updating fuel stop: %v", err)
http.Redirect(w, r, fmt.Sprintf("/edit/%d?error=Failed+to+update+fuel+stop", id), http.StatusSeeOther)
return
}
// Redirect to dashboard with success message
http.Redirect(w, r, "/dashboard?success=Fuel+stop+updated+successfully", http.StatusSeeOther)
}
// DeleteFuelStopHandler handles deleting fuel stops
func (h *Handler) DeleteFuelStopHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
userID, _ := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
idInt, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
id := uint(idInt)
// Verify fuel stop exists and belongs to user
fuelStop, err := h.db.GetFuelStopByID(id, userID)
if err != nil {
log.Printf("Error getting fuel stop: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if fuelStop == nil {
http.Error(w, "Fuel stop not found", http.StatusNotFound)
return
}
// Delete fuel stop
err = h.db.DeleteFuelStop(id, userID)
if err != nil {
log.Printf("Error deleting fuel stop: %v", err)
http.Redirect(w, r, "/dashboard?error=Failed+to+delete+fuel+stop", http.StatusSeeOther)
return
}
// Redirect to dashboard with success message
http.Redirect(w, r, "/dashboard?success=Fuel+stop+deleted+successfully", http.StatusSeeOther)
}
// validateFuelStopForm validates the fuel stop form data
func (h *Handler) validateFuelStopForm(form *models.FuelStopForm) error {
if form.Date == "" {
return fmt.Errorf("Date is required")
}
// Validate date format
_, err := time.Parse("2006-01-02", form.Date)
if err != nil {
return fmt.Errorf("Invalid date format")
}
if form.VehicleID == 0 {
return fmt.Errorf("Vehicle is required")
}
if form.StationName == "" && form.Location == "" {
return fmt.Errorf("Station name or location is required")
}
if form.FuelType == "" {
return fmt.Errorf("Fuel type is required")
}
if form.Liters <= 0 {
return fmt.Errorf("Amount must be greater than 0")
}
if form.PricePerL <= 0 {
return fmt.Errorf("Price per liter must be greater than 0")
}
if form.TotalPrice <= 0 {
return fmt.Errorf("Total price must be greater than 0")
}
if form.Currency != "" && !currency.IsValidCurrency(form.Currency) {
return fmt.Errorf("Invalid currency")
}
if form.Odometer < 0 {
return fmt.Errorf("Odometer reading cannot be negative")
}
if form.TripLength < 0 {
return fmt.Errorf("Trip length cannot be negative")
}
if form.TripLength > 2000 {
return fmt.Errorf("Trip length cannot exceed 2000 km")
}
// Validate consumption if both trip length and amount are provided
if form.TripLength > 0 && form.Liters > 0 {
consumption := (form.Liters / form.TripLength) * 100
if consumption > 50 {
return fmt.Errorf("Fuel consumption %.1f L/100km seems unrealistic. Please check trip length and amount", consumption)
}
if consumption < 1 {
return fmt.Errorf("Fuel consumption %.1f L/100km seems too low. Please check trip length and amount", consumption)
}
}
return nil
}
// Helper functions for parsing form values
func parseFloat(s string) float64 {
if s == "" {
return 0
}
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
return f
}
func parseInt(s string) int {
if s == "" {
return 0
}
i, err := strconv.Atoi(s)
if err != nil {
return 0
}
return i
}
func parseUint(s string) uint {
if s == "" {
return 0
}
i, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return 0
}
return uint(i)
}
+101
View File
@@ -0,0 +1,101 @@
package handlers
import (
"log"
"net/http"
"strconv"
"tankstopp/internal/auth"
"tankstopp/internal/database"
"github.com/gorilla/mux"
)
// Handler contains dependencies for all HTTP handlers
type Handler struct {
db *database.DB
sessionManager *auth.SessionManager
}
// NewHandler creates a new handler with database connection and session manager
func NewHandler(db *database.DB) *Handler {
return &Handler{
db: db,
sessionManager: auth.NewSessionManager(),
}
}
// AuthMiddleware checks if user is authenticated
func (h *Handler) AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sessionID, err := auth.GetSessionCookie(r)
if err != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
session, exists := h.sessionManager.GetSession(sessionID)
if !exists {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Add user info to request context
r.Header.Set("X-User-ID", strconv.Itoa(int(session.UserID)))
r.Header.Set("X-Username", session.Username)
next.ServeHTTP(w, r)
}
}
// getCurrentUser extracts user information from request headers
func (h *Handler) getCurrentUser(r *http.Request) (uint, string) {
userIDStr := r.Header.Get("X-User-ID")
username := r.Header.Get("X-Username")
if userIDStr == "" {
return 0, ""
}
userIDInt, err := strconv.Atoi(userIDStr)
if err != nil {
log.Printf("Error parsing user ID: %v", err)
return 0, ""
}
return uint(userIDInt), username
}
// RegisterRoutes registers all application routes
func (h *Handler) RegisterRoutes(r *mux.Router) {
// Static files
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
// Public routes (no authentication required)
r.HandleFunc("/", h.RootHandler).Methods("GET")
r.HandleFunc("/login", h.LoginHandler).Methods("GET", "POST")
r.HandleFunc("/register", h.RegisterHandler).Methods("GET", "POST")
r.HandleFunc("/logout", h.LogoutHandler).Methods("POST")
// Protected routes (authentication required)
r.HandleFunc("/dashboard", h.AuthMiddleware(h.HomeHandler)).Methods("GET")
r.HandleFunc("/add", h.AuthMiddleware(h.AddFuelStopHandler)).Methods("GET", "POST")
r.HandleFunc("/edit/{id:[0-9]+}", h.AuthMiddleware(h.EditFuelStopHandler)).Methods("GET", "POST")
r.HandleFunc("/delete/{id:[0-9]+}", h.AuthMiddleware(h.DeleteFuelStopHandler)).Methods("POST")
r.HandleFunc("/settings", h.AuthMiddleware(h.SettingsHandler)).Methods("GET")
r.HandleFunc("/settings/profile", h.AuthMiddleware(h.UpdateProfileHandler)).Methods("POST")
r.HandleFunc("/settings/password", h.AuthMiddleware(h.UpdatePasswordHandler)).Methods("POST")
r.HandleFunc("/settings/delete-account", h.AuthMiddleware(h.DeleteAccountHandler)).Methods("POST")
// Vehicle management routes
r.HandleFunc("/vehicles", h.AuthMiddleware(h.VehiclesHandler)).Methods("GET")
r.HandleFunc("/vehicles/add", h.AuthMiddleware(h.AddVehicleHandler)).Methods("GET", "POST")
r.HandleFunc("/vehicles/edit/{id:[0-9]+}", h.AuthMiddleware(h.EditVehicleHandler)).Methods("GET", "POST")
r.HandleFunc("/vehicles/delete/{id:[0-9]+}", h.AuthMiddleware(h.DeleteVehicleHandler)).Methods("POST")
// API routes
r.HandleFunc("/api/fuel-stops", h.AuthMiddleware(h.APIGetFuelStopsHandler)).Methods("GET")
r.HandleFunc("/api/fuel-stops", h.AuthMiddleware(h.APICreateFuelStopHandler)).Methods("POST")
r.HandleFunc("/api/stats", h.AuthMiddleware(h.APIGetFuelStopStatsHandler)).Methods("GET")
r.HandleFunc("/api/vehicles/{id:[0-9]+}", h.AuthMiddleware(h.APIGetVehicleHandler)).Methods("GET")
}
+287
View File
@@ -0,0 +1,287 @@
package handlers
import (
"fmt"
"log"
"net/http"
"strings"
"tankstopp/internal/currency"
"tankstopp/internal/views/pages"
"golang.org/x/crypto/bcrypt"
)
// SettingsHandler handles user settings page
func (h *Handler) SettingsHandler(w http.ResponseWriter, r *http.Request) {
userID, _ := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Get user details
user, err := h.db.GetUserByID(userID)
if err != nil {
log.Printf("Error getting user: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Render settings page using templ
currencies := currency.SupportedCurrencies()
successMessage := r.URL.Query().Get("success")
errorMessage := r.URL.Query().Get("error")
component := pages.SettingsPage(user, user.Username, currencies, successMessage, errorMessage)
w.Header().Set("Content-Type", "text/html")
err = component.Render(r.Context(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// UpdateProfileHandler handles profile updates (email, currency, username)
func (h *Handler) UpdateProfileHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
userID, _ := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
err := r.ParseForm()
if err != nil {
log.Printf("Error parsing form: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
username := strings.TrimSpace(r.FormValue("username"))
email := strings.TrimSpace(r.FormValue("email"))
baseCurrency := r.FormValue("base_currency")
// Validate form data
if err := h.validateProfileForm(username, email, baseCurrency); err != nil {
http.Redirect(w, r, "/settings?error="+err.Error(), http.StatusSeeOther)
return
}
// Get current user
user, err := h.db.GetUserByID(userID)
if err != nil {
log.Printf("Error getting user: %v", err)
http.Redirect(w, r, "/settings?error=Failed+to+update+profile", http.StatusSeeOther)
return
}
// Update user fields
user.Username = username
user.Email = email
user.BaseCurrency = baseCurrency
// Save to database
err = h.db.UpdateUser(user)
if err != nil {
log.Printf("Error updating user: %v", err)
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
if strings.Contains(err.Error(), "username") {
http.Redirect(w, r, "/settings?error=Username+already+taken", http.StatusSeeOther)
} else {
http.Redirect(w, r, "/settings?error=Email+already+in+use", http.StatusSeeOther)
}
} else {
http.Redirect(w, r, "/settings?error=Failed+to+update+profile", http.StatusSeeOther)
}
return
}
// Note: Session username update would require session manager enhancement
// For now, user will see updated username on next login
http.Redirect(w, r, "/settings?success=Profile+updated+successfully", http.StatusSeeOther)
}
// UpdatePasswordHandler handles password changes
func (h *Handler) UpdatePasswordHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
userID, _ := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
err := r.ParseForm()
if err != nil {
log.Printf("Error parsing form: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
currentPassword := r.FormValue("current_password")
newPassword := r.FormValue("new_password")
confirmPassword := r.FormValue("confirm_password")
// Validate passwords
if err := h.validatePasswordForm(currentPassword, newPassword, confirmPassword); err != nil {
http.Redirect(w, r, "/settings?error="+err.Error(), http.StatusSeeOther)
return
}
// Get current user
user, err := h.db.GetUserByID(userID)
if err != nil {
log.Printf("Error getting user: %v", err)
http.Redirect(w, r, "/settings?error=Failed+to+change+password", http.StatusSeeOther)
return
}
// Verify current password
if !user.CheckPassword(currentPassword) {
http.Redirect(w, r, "/settings?error=Current+password+is+incorrect", http.StatusSeeOther)
return
}
// Hash new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
log.Printf("Error hashing password: %v", err)
http.Redirect(w, r, "/settings?error=Failed+to+change+password", http.StatusSeeOther)
return
}
// Update password
user.PasswordHash = string(hashedPassword)
err = h.db.UpdateUser(user)
if err != nil {
log.Printf("Error updating user password: %v", err)
http.Redirect(w, r, "/settings?error=Failed+to+change+password", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/settings?success=Password+changed+successfully", http.StatusSeeOther)
}
// DeleteAccountHandler handles account deletion
func (h *Handler) DeleteAccountHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
userID, _ := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Note: In a full implementation, we would delete all user data
// For now, we'll just clear the session and redirect
// TODO: Implement proper user deletion with cascading deletes
// Skip user deletion for now - would require proper database method
// err = h.db.DeleteUser(userID)
var err error // placeholder
if err != nil {
log.Printf("Error deleting user: %v", err)
http.Redirect(w, r, "/settings?error=Failed+to+delete+account", http.StatusSeeOther)
return
}
// Note: Removing all user sessions would require session manager enhancement
// Current session will be cleared by cookie deletion below
// Clear session cookie
cookie := &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
HttpOnly: true,
Secure: false, // Set to true in production with HTTPS
MaxAge: -1, // Delete immediately
}
http.SetCookie(w, cookie)
// Redirect to login with message
http.Redirect(w, r, "/login?success=Account+deleted+successfully", http.StatusSeeOther)
}
// validateProfileForm validates profile update form data
func (h *Handler) validateProfileForm(username, email, baseCurrency string) error {
if username == "" {
return fmt.Errorf("Username is required")
}
if len(username) < 3 {
return fmt.Errorf("Username must be at least 3 characters long")
}
if len(username) > 50 {
return fmt.Errorf("Username cannot be longer than 50 characters")
}
if email == "" {
return fmt.Errorf("Email is required")
}
if !strings.Contains(email, "@") || !strings.Contains(email, ".") {
return fmt.Errorf("Invalid email address")
}
if len(email) > 255 {
return fmt.Errorf("Email cannot be longer than 255 characters")
}
if baseCurrency == "" {
return fmt.Errorf("Base currency is required")
}
if !currency.IsValidCurrency(baseCurrency) {
return fmt.Errorf("Invalid currency")
}
return nil
}
// validatePasswordForm validates password change form data
func (h *Handler) validatePasswordForm(currentPassword, newPassword, confirmPassword string) error {
if currentPassword == "" {
return fmt.Errorf("Current password is required")
}
if newPassword == "" {
return fmt.Errorf("New password is required")
}
if len(newPassword) < 8 {
return fmt.Errorf("New password must be at least 8 characters long")
}
if len(newPassword) > 128 {
return fmt.Errorf("New password cannot be longer than 128 characters")
}
if confirmPassword == "" {
return fmt.Errorf("Password confirmation is required")
}
if newPassword != confirmPassword {
return fmt.Errorf("New passwords do not match")
}
if currentPassword == newPassword {
return fmt.Errorf("New password must be different from current password")
}
return nil
}
+342
View File
@@ -0,0 +1,342 @@
package handlers
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"tankstopp/internal/models"
"tankstopp/internal/views/pages"
"github.com/gorilla/mux"
)
// VehiclesHandler handles vehicle management page
func (h *Handler) VehiclesHandler(w http.ResponseWriter, r *http.Request) {
userID, username := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Get user object
user, err := h.db.GetUserByID(userID)
if err != nil {
log.Printf("Error getting user: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Get vehicles for the user
vehicles, err := h.db.GetVehicles(userID)
if err != nil {
log.Printf("Error getting vehicles: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Render vehicles page using templ
component := pages.VehiclesPage(user, username, vehicles)
w.Header().Set("Content-Type", "text/html")
err = component.Render(r.Context(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// AddVehicleHandler handles adding new vehicles
func (h *Handler) AddVehicleHandler(w http.ResponseWriter, r *http.Request) {
userID, _ := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
switch r.Method {
case "GET":
// Get user object
user, err := h.db.GetUserByID(userID)
if err != nil {
log.Printf("Error getting user: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Render add vehicle form using templ
component := pages.AddVehiclePage(user, user.Username)
w.Header().Set("Content-Type", "text/html")
err = component.Render(r.Context(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
case "POST":
h.handleAddVehicle(w, r, userID)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleAddVehicle processes the form submission for adding vehicles
func (h *Handler) handleAddVehicle(w http.ResponseWriter, r *http.Request, userID uint) {
err := r.ParseForm()
if err != nil {
log.Printf("Error parsing form: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Parse form data
form := models.VehicleForm{
Name: strings.TrimSpace(r.FormValue("name")),
Make: strings.TrimSpace(r.FormValue("make")),
Model: strings.TrimSpace(r.FormValue("model")),
LicensePlate: strings.TrimSpace(r.FormValue("license_plate")),
FuelType: r.FormValue("fuel_type"),
Notes: r.FormValue("notes"),
IsActive: r.FormValue("is_active") == "on",
}
// Parse year
if yearStr := r.FormValue("year"); yearStr != "" {
if year, err := strconv.Atoi(yearStr); err == nil {
form.Year = year
}
}
// Validate form
if err := h.validateVehicleForm(&form); err != nil {
log.Printf("Validation error: %v", err)
http.Redirect(w, r, "/vehicles/add?error="+err.Error(), http.StatusSeeOther)
return
}
// Convert form to vehicle
vehicle := form.ToVehicle(userID)
// Save to database
err = h.db.CreateVehicle(vehicle)
if err != nil {
log.Printf("Error creating vehicle: %v", err)
http.Redirect(w, r, "/vehicles/add?error=Failed+to+create+vehicle", http.StatusSeeOther)
return
}
// Redirect to vehicles page with success message
http.Redirect(w, r, "/vehicles?success=Vehicle+added+successfully", http.StatusSeeOther)
}
// EditVehicleHandler handles editing existing vehicles
func (h *Handler) EditVehicleHandler(w http.ResponseWriter, r *http.Request) {
userID, _ := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
idInt, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
id := uint(idInt)
switch r.Method {
case "GET":
// Get vehicle
vehicle, err := h.db.GetVehicleByID(id, userID)
if err != nil {
log.Printf("Error getting vehicle: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if vehicle == nil {
http.Error(w, "Vehicle not found", http.StatusNotFound)
return
}
// Get user object
user, err := h.db.GetUserByID(userID)
if err != nil {
log.Printf("Error getting user: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Render edit vehicle form using templ
component := pages.EditVehiclePage(user, user.Username, vehicle)
w.Header().Set("Content-Type", "text/html")
err = component.Render(r.Context(), w)
if err != nil {
log.Printf("Error rendering template: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
case "POST":
h.handleEditVehicle(w, r, id, userID)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleEditVehicle processes the form submission for editing vehicles
func (h *Handler) handleEditVehicle(w http.ResponseWriter, r *http.Request, id, userID uint) {
err := r.ParseForm()
if err != nil {
log.Printf("Error parsing form: %v", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Get existing vehicle
existingVehicle, err := h.db.GetVehicleByID(id, userID)
if err != nil {
log.Printf("Error getting vehicle: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if existingVehicle == nil {
http.Error(w, "Vehicle not found", http.StatusNotFound)
return
}
// Parse form data
form := models.VehicleForm{
Name: strings.TrimSpace(r.FormValue("name")),
Make: strings.TrimSpace(r.FormValue("make")),
Model: strings.TrimSpace(r.FormValue("model")),
LicensePlate: strings.TrimSpace(r.FormValue("license_plate")),
FuelType: r.FormValue("fuel_type"),
Notes: r.FormValue("notes"),
IsActive: r.FormValue("is_active") == "on",
}
// Parse year
if yearStr := r.FormValue("year"); yearStr != "" {
if year, err := strconv.Atoi(yearStr); err == nil {
form.Year = year
}
}
// Validate form
if err := h.validateVehicleForm(&form); err != nil {
log.Printf("Validation error: %v", err)
http.Redirect(w, r, fmt.Sprintf("/vehicles/edit/%d?error=%s", id, err.Error()), http.StatusSeeOther)
return
}
// Convert form to vehicle
updatedVehicle := form.ToVehicle(userID)
updatedVehicle.ID = id
updatedVehicle.CreatedAt = existingVehicle.CreatedAt
// Update in database
err = h.db.UpdateVehicle(updatedVehicle)
if err != nil {
log.Printf("Error updating vehicle: %v", err)
http.Redirect(w, r, fmt.Sprintf("/vehicles/edit/%d?error=Failed+to+update+vehicle", id), http.StatusSeeOther)
return
}
// Redirect to vehicles page with success message
http.Redirect(w, r, "/vehicles?success=Vehicle+updated+successfully", http.StatusSeeOther)
}
// DeleteVehicleHandler handles deleting vehicles
func (h *Handler) DeleteVehicleHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
userID, _ := h.getCurrentUser(r)
if userID == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
vars := mux.Vars(r)
idInt, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
id := uint(idInt)
// Verify vehicle exists and belongs to user
vehicle, err := h.db.GetVehicleByID(id, userID)
if err != nil {
log.Printf("Error getting vehicle: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if vehicle == nil {
http.Error(w, "Vehicle not found", http.StatusNotFound)
return
}
// Note: In a full implementation, we would check for associated fuel stops
// For now, we'll allow deletion and rely on database constraints
// Delete vehicle
err = h.db.DeleteVehicle(id, userID)
if err != nil {
log.Printf("Error deleting vehicle: %v", err)
http.Redirect(w, r, "/vehicles?error=Failed+to+delete+vehicle", http.StatusSeeOther)
return
}
// Redirect to vehicles page with success message
http.Redirect(w, r, "/vehicles?success=Vehicle+deleted+successfully", http.StatusSeeOther)
}
// validateVehicleForm validates the vehicle form data
func (h *Handler) validateVehicleForm(form *models.VehicleForm) error {
if form.Name == "" {
return fmt.Errorf("Vehicle name is required")
}
if len(form.Name) < 2 {
return fmt.Errorf("Vehicle name must be at least 2 characters long")
}
if form.Make == "" {
return fmt.Errorf("Vehicle make is required")
}
if form.Model == "" {
return fmt.Errorf("Vehicle model is required")
}
if form.FuelType == "" {
return fmt.Errorf("Fuel type is required")
}
// Validate year if provided
if form.Year != 0 {
currentYear := 2024 // You might want to use time.Now().Year()
if form.Year < 1900 || form.Year > currentYear+1 {
return fmt.Errorf("Year must be between 1900 and %d", currentYear+1)
}
}
// Validate license plate format if provided
if form.LicensePlate != "" {
if len(form.LicensePlate) > 20 {
return fmt.Errorf("License plate cannot be longer than 20 characters")
}
}
// Validate notes length if provided
if len(form.Notes) > 500 {
return fmt.Errorf("Notes cannot be longer than 500 characters")
}
return nil
}