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
+478
View File
@@ -0,0 +1,478 @@
package pages
import (
"fmt"
"tankstopp/internal/models"
"tankstopp/internal/views/components"
)
templ DashboardPage(user *models.User, username string, stops []models.FuelStop, vehicles []models.Vehicle, totalStops int, totalCost float64, avgConsumption float64, lastFillUp *models.FuelStop) {
@components.BaseLayout("Dashboard", user, username) {
@components.PageHeader("Overview", "Dashboard")
<div class="page-body">
<div class="container-xl">
<!-- Statistics Cards -->
<div class="row row-deck row-cards mb-4">
<div class="col-sm-6 col-lg-3">
@components.Card("Total Stops", "gas-station") {
<div class="d-flex align-items-center">
<div class="subheader">Fuel stops recorded</div>
<div class="ms-auto lh-1">
<div class="dropdown">
<a class="dropdown-toggle text-muted" href="#" data-bs-toggle="dropdown">Last 30 days</a>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item active" href="#">Last 30 days</a>
<a class="dropdown-item" href="#">Last 90 days</a>
<a class="dropdown-item" href="#">All time</a>
</div>
</div>
</div>
</div>
<div class="h1 mb-3">{ fmt.Sprintf("%d", totalStops) }</div>
<div class="d-flex mb-2">
<div class="flex-fill">
<div class="progress progress-sm">
<div class="progress-bar bg-primary" style="width: 75%" role="progressbar"></div>
</div>
</div>
<div class="text-muted ms-2">75%</div>
</div>
}
</div>
<div class="col-sm-6 col-lg-3">
@components.Card("Total Spent", "currency") {
<div class="d-flex align-items-center">
<div class="subheader">Total fuel costs</div>
</div>
<div class="h1 mb-3">{ fmt.Sprintf("%.2f %s", totalCost, user.BaseCurrency) }</div>
<div class="d-flex mb-2">
<div class="flex-fill">
<div class="progress progress-sm">
<div class="progress-bar bg-success" style="width: 60%" role="progressbar"></div>
</div>
</div>
<div class="text-muted ms-2">60%</div>
</div>
}
</div>
<div class="col-sm-6 col-lg-3">
@components.Card("Avg Consumption", "gauge") {
<div class="d-flex align-items-center">
<div class="subheader">Liters per 100km</div>
</div>
<div class="h1 mb-3">
if avgConsumption > 0 {
{ fmt.Sprintf("%.1f L/100km", avgConsumption) }
} else {
{ "N/A" }
}
</div>
<div class="d-flex mb-2">
<div class="flex-fill">
<div class="progress progress-sm">
<div class="progress-bar bg-warning" style="width: 45%" role="progressbar"></div>
</div>
</div>
<div class="text-muted ms-2">Efficiency</div>
</div>
}
</div>
<div class="col-sm-6 col-lg-3">
@components.Card("Vehicles", "car") {
<div class="d-flex align-items-center">
<div class="subheader">Active vehicles</div>
</div>
<div class="h1 mb-3">{ fmt.Sprintf("%d", len(vehicles)) }</div>
<div class="d-flex mb-2">
<div class="flex-fill">
<div class="progress progress-sm">
<div class="progress-bar bg-info" style="width: 100%" role="progressbar"></div>
</div>
</div>
<div class="text-muted ms-2">100%</div>
</div>
}
</div>
</div>
<!-- Trip Length and Efficiency Statistics -->
<div class="row row-deck row-cards mb-4">
<div class="col-sm-6 col-lg-4">
@components.Card("Total Distance", "trip") {
<div class="d-flex align-items-center">
<div class="subheader">Kilometers driven</div>
</div>
<div class="h1 mb-3">
{ fmt.Sprintf("%.0f km", calculateTotalDistance(stops)) }
</div>
<div class="d-flex mb-2">
<div class="flex-fill">
<div class="progress progress-sm">
<div class="progress-bar bg-info" style="width: 80%" role="progressbar"></div>
</div>
</div>
<div class="text-muted ms-2">Tracked</div>
</div>
}
</div>
<div class="col-sm-6 col-lg-4">
@components.Card("Efficiency Trend", "chart-bar") {
<div class="d-flex align-items-center">
<div class="subheader">Recent performance</div>
</div>
<div class="h1 mb-3">
if len(stops) >= 3 {
if calculateEfficiencyTrend(stops) == "improving" {
<span class="text-success">Improving</span>
} else if calculateEfficiencyTrend(stops) == "worsening" {
<span class="text-danger">Declining</span>
} else {
<span class="text-info">Stable</span>
}
} else {
<span class="text-muted">N/A</span>
}
</div>
<div class="d-flex mb-2">
<div class="flex-fill">
<div class="progress progress-sm">
if calculateEfficiencyTrend(stops) == "improving" {
<div class="progress-bar bg-success" style="width: 75%" role="progressbar"></div>
} else if calculateEfficiencyTrend(stops) == "worsening" {
<div class="progress-bar bg-danger" style="width: 30%" role="progressbar"></div>
} else {
<div class="progress-bar bg-info" style="width: 50%" role="progressbar"></div>
}
</div>
</div>
<div class="text-muted ms-2">Trend</div>
</div>
}
</div>
<div class="col-sm-6 col-lg-4">
@components.Card("Best Efficiency", "award") {
<div class="d-flex align-items-center">
<div class="subheader">Lowest consumption</div>
</div>
<div class="h1 mb-3">
{ fmt.Sprintf("%.1f L/100km", getBestEfficiency(stops)) }
</div>
<div class="d-flex mb-2">
<div class="flex-fill">
<div class="progress progress-sm">
<div class="progress-bar bg-success" style="width: 90%" role="progressbar"></div>
</div>
</div>
<div class="text-muted ms-2">Personal best</div>
</div>
}
</div>
</div>
<!-- Last Fill-up Info -->
if lastFillUp != nil {
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">
@components.Icon("clock", 24)
Last Fill-up
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-sm-6 col-lg-3">
<div class="mb-3">
<div class="text-muted">Date</div>
<div class="h4">{ lastFillUp.Date.Format("Jan 2, 2006") }</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="mb-3">
<div class="text-muted">Amount</div>
<div class="h4">{ fmt.Sprintf("%.2f L", lastFillUp.Liters) }</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="mb-3">
<div class="text-muted">Cost</div>
<div class="h4">{ fmt.Sprintf("%.2f %s", lastFillUp.TotalPrice, user.BaseCurrency) }</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="mb-3">
<div class="text-muted">Location</div>
<div class="h4">{ lastFillUp.Location }</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
<!-- Filters and Search -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Filters</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-sm-6 col-lg-3">
@components.FormGroup("Vehicle", "") {
@components.VehicleSelect("vehicle_id", 0, vehicles, false)
}
</div>
<div class="col-sm-6 col-lg-3">
@components.FormGroup("Fuel Type", "") {
@components.FuelTypeSelect("fuel_type", "", false)
}
</div>
<div class="col-sm-6 col-lg-3">
@components.FormGroup("Date From", "") {
@components.DateInput("date_from", "", false)
}
</div>
<div class="col-sm-6 col-lg-3">
@components.FormGroup("Date To", "") {
@components.DateInput("date_to", "", false)
}
</div>
</div>
<div class="row">
<div class="col-12">
@components.ButtonGroup() {
<button type="button" class="btn btn-primary" onclick="applyFilters()">
@components.Icon("search", 24)
Apply Filters
</button>
<button type="button" class="btn btn-secondary" onclick="clearFilters()">
@components.Icon("refresh", 24)
Clear
</button>
}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fuel Stops Table -->
<div class="row">
<div class="col-12">
if len(stops) > 0 {
@FuelStopsTable(stops, user.BaseCurrency)
} else {
@components.EmptyState("gas-station", "No fuel stops found", "Start tracking your fuel expenses by adding your first fuel stop.", "Add your first stop", "/add")
}
</div>
</div>
</div>
</div>
@DashboardScript()
}
}
templ FuelStopsTable(stops []models.FuelStop, currency string) {
<div class="card">
<div class="card-header">
<h3 class="card-title">Recent Fuel Stops</h3>
<div class="card-actions">
<a href="/add" class="btn btn-primary">
@components.Icon("plus", 24)
Add Stop
</a>
</div>
</div>
<div class="table-responsive">
<table class="table table-vcenter table-mobile-md card-table">
<thead>
<tr>
<th>Date</th>
<th>Vehicle</th>
<th>Location</th>
<th>Fuel Type</th>
<th>Amount</th>
<th>Price/L</th>
<th>Total</th>
<th>Trip Length</th>
<th>Consumption</th>
<th>Odometer</th>
<th class="w-1">Actions</th>
</tr>
</thead>
<tbody>
for _, stop := range stops {
<tr>
<td data-label="Date">
<div class="d-flex py-1 align-items-center">
<div class="flex-fill">
<div class="font-weight-medium">{ stop.Date.Format("Jan 2, 2006") }</div>
<div class="text-muted">{ stop.Date.Format("15:04") }</div>
</div>
</div>
</td>
<td data-label="Vehicle">
<div class="d-flex py-1 align-items-center">
<div class="flex-fill">
<div class="font-weight-medium">{ stop.Vehicle.Name }</div>
if stop.Vehicle.LicensePlate != "" {
<div class="text-muted">{ stop.Vehicle.LicensePlate }</div>
}
</div>
</div>
</td>
<td data-label="Location">
<div class="d-flex py-1 align-items-center">
@components.Icon("location", 24)
<div class="ms-2">{ stop.Location }</div>
</div>
</td>
<td data-label="Fuel Type">
<span class="badge bg-secondary">{ stop.FuelType }</span>
</td>
<td data-label="Amount">
<div class="font-weight-medium">{ fmt.Sprintf("%.2f L", stop.Liters) }</div>
</td>
<td data-label="Price/L">
<div class="font-weight-medium">{ fmt.Sprintf("%.3f %s", stop.PricePerL, currency) }</div>
</td>
<td data-label="Total">
<div class="font-weight-medium text-success">{ fmt.Sprintf("%.2f %s", stop.TotalPrice, currency) }</div>
</td>
<td data-label="Trip Length">
if stop.TripLength > 0 {
<div class="font-weight-medium">{ fmt.Sprintf("%.1f km", stop.TripLength) }</div>
} else {
<div class="text-muted">Not recorded</div>
}
</td>
<td data-label="Consumption">
if stop.TripLength > 0 {
<div class="font-weight-medium">
{ fmt.Sprintf("%.1f L/100km", (stop.Liters/stop.TripLength)*100) }
</div>
} else {
<div class="text-muted">N/A</div>
}
</td>
<td data-label="Odometer">
if stop.Odometer > 0 {
<div class="font-weight-medium">{ fmt.Sprintf("%d km", stop.Odometer) }</div>
} else {
<div class="text-muted">Not recorded</div>
}
</td>
<td>
@components.ButtonGroup() {
@components.EditButton(fmt.Sprintf("/edit/%d", stop.ID))
@components.DeleteButton(fmt.Sprintf("/delete/%d", stop.ID), fmt.Sprintf("fuel stop from %s", stop.Date.Format("Jan 2, 2006")))
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
// Helper functions for statistics calculations
func calculateTotalDistance(stops []models.FuelStop) float64 {
var total float64
for _, stop := range stops {
if stop.TripLength > 0 {
total += stop.TripLength
}
}
return total
}
func calculateEfficiencyTrend(stops []models.FuelStop) string {
if len(stops) < 3 {
return "insufficient_data"
}
// Calculate average consumption for recent vs older stops
recent := stops[:len(stops)/2]
older := stops[len(stops)/2:]
recentAvg := calculateAverageConsumption(recent)
olderAvg := calculateAverageConsumption(older)
if recentAvg == 0 || olderAvg == 0 {
return "insufficient_data"
}
diff := recentAvg - olderAvg
if diff < -0.5 {
return "improving"
} else if diff > 0.5 {
return "worsening"
}
return "stable"
}
func calculateAverageConsumption(stops []models.FuelStop) float64 {
var totalConsumption float64
var count int
for _, stop := range stops {
if stop.TripLength > 0 {
consumption := (stop.Liters / stop.TripLength) * 100
if consumption > 0 && consumption < 50 {
totalConsumption += consumption
count++
}
}
}
if count == 0 {
return 0
}
return totalConsumption / float64(count)
}
func getBestEfficiency(stops []models.FuelStop) float64 {
bestEfficiency := float64(999) // Start with high value
for _, stop := range stops {
if stop.TripLength > 0 {
consumption := (stop.Liters / stop.TripLength) * 100
if consumption > 0 && consumption < bestEfficiency && consumption < 50 {
bestEfficiency = consumption
}
}
}
if bestEfficiency == 999 {
return 0
}
return bestEfficiency
}
script DashboardScript() {
function applyFilters() {
const vehicleId = document.querySelector('select[name="vehicle_id"]').value;
const fuelType = document.querySelector('select[name="fuel_type"]').value;
const dateFrom = document.querySelector('input[name="date_from"]').value;
const dateTo = document.querySelector('input[name="date_to"]').value;
const params = new URLSearchParams();
if (vehicleId) params.append('vehicle_id', vehicleId);
if (fuelType) params.append('fuel_type', fuelType);
if (dateFrom) params.append('date_from', dateFrom);
if (dateTo) params.append('date_to', dateTo);
window.location.href = '/dashboard?' + params.toString();
}
function clearFilters() {
window.location.href = '/dashboard';
}
function confirmDelete(itemName) {
return confirm(`Are you sure you want to delete this ${itemName}? This action cannot be undone.`);
}
}