FuelMan/Gas Man/FuelLogs/FuelLogListView.swift

217 lines
9.2 KiB
Swift

import SwiftUI
import CoreData
struct FuelLogListView: View {
@Environment(\.managedObjectContext) private var viewContext
// All fuel logs (sorted by date descending)
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \FuelLog.date, ascending: false)],
animation: .default)
private var fuelLogs: FetchedResults<FuelLog>
// All vehicles (sorted by make)
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Vehicle.make, ascending: true)],
animation: .default)
private var vehicles: FetchedResults<Vehicle>
@State private var showingAddFuelLog = false
@State private var selectedVehicleID: UUID? = nil
@State private var showAddVehicleSheet = false
// For tracking the previous odometer reading (used for validation)
@State private var previousOdometer: Double? = nil
@State private var showOdometerAlert = false
@State private var isUpdatingCalculation = false
@State private var showingImporter = false
// Computed property for formatted previous odometer value
private var previousOdometerString: String {
previousOdometer.map { String(format: "%.0f", $0) } ?? "N/A"
}
// Filter fuel logs by the selected vehicle.
private var filteredFuelLogs: [FuelLog] {
if let vehicleID = selectedVehicleID {
return fuelLogs.filter { log in
if let logVehicleID = log.vehicle?.id {
return logVehicleID == vehicleID
}
return false
}
} else {
return Array(fuelLogs)
}
}
// Computed property to calculate average MPG for full-tank logs.
// MPG for an individual log is computed as:
// (current.odometer - next.odometer) / current.fuelVolume
private var averageMPG: Double? {
let logs = filteredFuelLogs
guard logs.count > 1 else { return nil }
var mpgValues: [Double] = []
// Iterate over logs except the oldest.
for i in 0..<logs.count - 1 {
let current = logs[i]
let next = logs[i + 1]
// Only include if fullTank is true, fuelVolume is positive, and odometer difference is positive.
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 total = mpgValues.reduce(0, +)
return total / Double(mpgValues.count)
}
var body: some View {
NavigationView {
if vehicles.isEmpty {
// Empty state: no vehicles exist.
VStack(spacing: 20) {
Text("No vehicles found.")
.font(.headline)
Text("Please add a vehicle before logging fuel records.")
.multilineTextAlignment(.center)
.padding(.horizontal)
Button("Add Vehicle") {
showAddVehicleSheet = true
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
.navigationTitle("Fuel Logs")
.sheet(isPresented: $showAddVehicleSheet) {
AddVehicleView().environment(\.managedObjectContext, viewContext)
}
} else {
List {
// Vehicle Picker Section
Section {
Picker("Vehicle", selection: $selectedVehicleID) {
ForEach(vehicles, id: \.id) { vehicle in
Text("\(vehicle.year ?? "") \(vehicle.make ?? "") \(vehicle.model ?? "")")
.tag(vehicle.id)
}
}
.pickerStyle(MenuPickerStyle())
}
// Average MPG Section
Section {
HStack {
Text("Average MPG:")
Spacer()
if let avg = averageMPG {
Text("\(avg, specifier: "%.1f")")
.bold()
} else {
Text("N/A")
.foregroundColor(.secondary)
}
}
}
// Fuel Logs Section (filtered by selected vehicle)
ForEach(filteredFuelLogs.indices, id: \.self) { index in
let log = filteredFuelLogs[index]
let distance: Double? = {
guard index < filteredFuelLogs.count - 1 else { return nil }
let previousLog = filteredFuelLogs[index + 1]
let milesDriven = log.odometer - previousLog.odometer
return milesDriven > 0 ? milesDriven : nil
}()
let mpg: Double? = {
// Only calculate MPG if fullTank is true.
guard log.fullTank else { return nil }
guard index < filteredFuelLogs.count - 1 else { return nil }
let previousLog = filteredFuelLogs[index + 1]
let milesDriven = log.odometer - previousLog.odometer
guard log.fuelVolume > 0, milesDriven > 0 else { return nil }
return milesDriven / log.fuelVolume
}()
NavigationLink(destination: FuelLogDetailView(fuelLog: log)) {
FuelLogSummaryView(log: log, mpg: mpg, distanceSincePrevious: distance)
}
}
.onDelete(perform: deleteFuelLogs)
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Fuel Logs")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showingAddFuelLog = true
} label: {
Label("Add Fuel Log", systemImage: "plus")
}
}
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
ToolbarItem(placement: .navigationBarLeading) {
Button("Import CSV") {
showingImporter = true
}
}
}
.sheet(isPresented: $showingAddFuelLog) {
AddFuelLogView().environment(\.managedObjectContext, viewContext)
}
.sheet(isPresented: $showingImporter) {
// Retrieve the selected vehicle from the vehicles fetched results.
if let selectedVehicle = vehicles.first(where: { $0.id == selectedVehicleID }) {
FuelLogImporterView(selectedVehicle: selectedVehicle)
.environment(\.managedObjectContext, viewContext)
} else {
// Optionally, provide a fallback, e.g. use the first vehicle.
FuelLogImporterView(selectedVehicle: vehicles.first)
.environment(\.managedObjectContext, viewContext)
}
// FuelLogImporterView()
// .environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
}
.onAppear {
print("Total fuel logs: \(fuelLogs.count)")
setDefaultVehicle()
}
}
}
.alert(isPresented: $showOdometerAlert) {
Alert(title: Text("Odometer Reading Error"),
message: Text("Odometer reading must be greater than the previous record (\(previousOdometerString))."),
dismissButton: .default(Text("OK")))
}
}
// Set default vehicle selection on appear.
private func setDefaultVehicle() {
if selectedVehicleID == nil {
if let lastFuelLog = fuelLogs.first, let vehicle = lastFuelLog.vehicle {
selectedVehicleID = vehicle.id
} else {
selectedVehicleID = vehicles.first?.id
}
}
}
private func deleteFuelLogs(offsets: IndexSet) {
withAnimation {
let logsToDelete = offsets.map { filteredFuelLogs[$0] }
logsToDelete.forEach { viewContext.delete($0) }
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}