diff --git a/Gas Man/MainTabView.swift b/Gas Man/MainTabView.swift index b80e869..2d166ea 100644 --- a/Gas Man/MainTabView.swift +++ b/Gas Man/MainTabView.swift @@ -22,6 +22,10 @@ struct MainTabView: View { .tabItem { Label("Maintenance", systemImage: "wrench.fill") } + StatsView() + .tabItem { + Label("Stats", systemImage: "chart.bar.fill") + } } } } diff --git a/Gas Man/Stats/StatsView.swift b/Gas Man/Stats/StatsView.swift new file mode 100644 index 0000000..1631d9e --- /dev/null +++ b/Gas Man/Stats/StatsView.swift @@ -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 + + // Fetch all vehicles sorted by make (or any preferred order) + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Vehicle.make, ascending: true)], + animation: .default) + private var vehicles: FetchedResults + + @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.. 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.. 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 + } + } + } +} +