FuelMan/Gas Man/Stats/StatsView.swift

275 lines
10 KiB
Swift

//
// StatsView.swift
// Gas Man
//
// Created by Kameron Kenny on 3/19/25.
//
import SwiftUI
import CoreData
struct StatsView: View {
@Environment(\.managedObjectContext) private var viewContext
// Fetch all fuel logs sorted by date (descending)
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \FuelLog.date, ascending: false)],
animation: .default)
private var fuelLogs: FetchedResults<FuelLog>
// Fetch all vehicles sorted by make
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Vehicle.make, ascending: true)],
animation: .default)
private var vehicles: FetchedResults<Vehicle>
@State private var selectedVehicleID: UUID? = nil
// Filter fuel logs by the selected vehicle.
private var filteredFuelLogs: [FuelLog] {
if let vehicleID = selectedVehicleID {
return fuelLogs.filter { log in
log.vehicle?.id == vehicleID
}
} else {
return Array(fuelLogs)
}
}
// Total number of fill-ups.
private var fillUpCount: Int {
filteredFuelLogs.count
}
// Average MPG computed only for full-tank logs.
private var averageMPG: Double? {
let logs = filteredFuelLogs
guard logs.count > 1 else { return nil }
var mpgValues: [Double] = []
for i in 0..<logs.count - 1 {
let current = logs[i]
let next = logs[i + 1]
if current.fullTank, current.fuelVolume > 0, current.odometer > next.odometer {
let mpg = (current.odometer - next.odometer) / current.fuelVolume
mpgValues.append(mpg)
}
}
guard !mpgValues.isEmpty else { return nil }
let sum = mpgValues.reduce(0, +)
return sum / Double(mpgValues.count)
}
// Price per gallon stats.
private var pricePerGallonStats: (min: Double?, max: Double?, avg: Double?) {
let prices = filteredFuelLogs.map { $0.pricePerGalon }
guard !prices.isEmpty else { return (nil, nil, nil) }
let minPrice = prices.min()
let maxPrice = prices.max()
let avgPrice = prices.reduce(0, +) / Double(prices.count)
return (minPrice, maxPrice, avgPrice)
}
// Distance between fill-ups stats.
private var distanceStats: (min: Double?, max: Double?, avg: Double?) {
let logs = filteredFuelLogs
guard logs.count > 1 else { return (nil, nil, nil) }
var distances: [Double] = []
for i in 0..<logs.count - 1 {
let current = logs[i]
let next = logs[i + 1]
let dist = current.odometer - next.odometer
if dist > 0 {
distances.append(dist)
}
}
guard !distances.isEmpty else { return (nil, nil, nil) }
let minDistance = distances.min()!
let maxDistance = distances.max()!
let avgDistance = distances.reduce(0, +) / Double(distances.count)
return (minDistance, maxDistance, avgDistance)
}
// Gallons per fill-up stats.
private var gallonsStats: (min: Double?, max: Double?, avg: Double?) {
let gallons = filteredFuelLogs.map { $0.fuelVolume }
guard !gallons.isEmpty else { return (nil, nil, nil) }
let minGallons = gallons.min()
let maxGallons = gallons.max()
let avgGallons = gallons.reduce(0, +) / Double(gallons.count)
return (minGallons, maxGallons, avgGallons)
}
var body: some View {
NavigationView {
Form {
// Vehicle Selector Section
Section(header: Text("Select Vehicle")) {
Picker("Vehicle", selection: $selectedVehicleID) {
ForEach(vehicles, id: \.id) { vehicle in
Text("\(vehicle.year ?? "") \(vehicle.make ?? "") \(vehicle.model ?? "")")
.tag(vehicle.id)
}
}
.pickerStyle(MenuPickerStyle())
}
// Fill-Up Count Section
Section {
HStack {
Text("Fill-Up Count:")
Spacer()
Text("\(fillUpCount)")
.bold()
}
}
// Average MPG Section with NavigationLink to chart.
Section {
NavigationLink(destination: MPGTrendChartView(fuelLogs: filteredFuelLogs)) {
HStack {
Text("Average MPG:")
Spacer()
if let avgMPG = averageMPG {
Text("\(avgMPG, specifier: "%.1f") MPG")
.bold()
} else {
Text("N/A")
.foregroundColor(.secondary)
}
Image(systemName: "chart.bar.fill")
.foregroundColor(.blue)
}
}
}
// Price per Gallon Stats
Section(header: Text("Price per Gallon")) {
let stats = pricePerGallonStats
HStack {
Text("Lowest:")
Spacer()
if let minPrice = stats.min {
Text("$\(minPrice, specifier: "%.3f")")
} else {
Text("N/A")
}
}
HStack {
Text("Highest:")
Spacer()
if let maxPrice = stats.max {
Text("$\(maxPrice, specifier: "%.3f")")
} else {
Text("N/A")
}
}
HStack {
Text("Average:")
Spacer()
if let avgPrice = stats.avg {
Text("$\(avgPrice, specifier: "%.3f")")
} else {
Text("N/A")
}
}
NavigationLink(destination: PricePerGallonTrendChartView(fuelLogs: filteredFuelLogs)) {
HStack {
Text("Trends:")
Spacer()
Image(systemName: "chart.bar.fill")
.foregroundColor(.blue)
}
}
}
// Distance Between Fill-Ups Stats
Section(header: Text("Distance Between Fill-Ups")) {
let dStats = distanceStats
HStack {
Text("Lowest:")
Spacer()
if let minDistance = dStats.min {
Text("\(minDistance, specifier: "%.0f") miles")
} else {
Text("N/A")
}
}
HStack {
Text("Highest:")
Spacer()
if let maxDistance = dStats.max {
Text("\(maxDistance, specifier: "%.0f") miles")
} else {
Text("N/A")
}
}
HStack {
Text("Average:")
Spacer()
if let avgDistance = dStats.avg {
Text("\(avgDistance, specifier: "%.0f") miles")
} else {
Text("N/A")
}
}
NavigationLink(destination: DistanceBetweenFillupsTrendChartView(fuelLogs: filteredFuelLogs)) {
HStack {
Text("Trends:")
Spacer()
Image(systemName: "chart.bar.fill")
.foregroundColor(.blue)
}
}
}
// Gallons per Fill-Up Stats
Section(header: Text("Gallons per Fill-Up")) {
let gStats = gallonsStats
HStack {
Text("Lowest:")
Spacer()
if let minGallons = gStats.min {
Text("\(minGallons, specifier: "%.3f") gallons")
} else {
Text("N/A")
}
}
HStack {
Text("Highest:")
Spacer()
if let maxGallons = gStats.max {
Text("\(maxGallons, specifier: "%.3f") gallons")
} else {
Text("N/A")
}
}
HStack {
Text("Average:")
Spacer()
if let avgGallons = gStats.avg {
Text("\(avgGallons, specifier: "%.3f") gallons")
} else {
Text("N/A")
}
}
NavigationLink(destination: GalPerFuelUpTrendChartView(fuelLogs: filteredFuelLogs)) {
HStack {
Text("Trends:")
Spacer()
Image(systemName: "chart.bar.fill")
.foregroundColor(.blue)
}
}
}
}
.navigationTitle("Stats")
}
.onAppear {
if selectedVehicleID == nil {
selectedVehicleID = vehicles.first?.id
}
}
}
}