stats view
This commit is contained in:
parent
16d65536d0
commit
292c9770ef
|
@ -22,6 +22,10 @@ struct MainTabView: View {
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Maintenance", systemImage: "wrench.fill")
|
Label("Maintenance", systemImage: "wrench.fill")
|
||||||
}
|
}
|
||||||
|
StatsView()
|
||||||
|
.tabItem {
|
||||||
|
Label("Stats", systemImage: "chart.bar.fill")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,234 @@
|
||||||
|
//
|
||||||
|
// 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 (or any preferred order)
|
||||||
|
@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: lowest, highest, average.
|
||||||
|
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: lowest, highest, average.
|
||||||
|
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(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())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats for Fuel Logs
|
||||||
|
Section(header: Text("Fill-Up Count")) {
|
||||||
|
Text("\(fillUpCount)")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Average MPG")) {
|
||||||
|
if let avgMPG = averageMPG {
|
||||||
|
Text("\(avgMPG, specifier: "%.1f") MPG")
|
||||||
|
} else {
|
||||||
|
Text("N/A")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Stats")
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if selectedVehicleID == nil {
|
||||||
|
selectedVehicleID = vehicles.first?.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue