diff --git a/Gas Man/AddFuelLogView.swift b/Gas Man/AddFuelLogView.swift index e5bbcf6..1c39a59 100644 --- a/Gas Man/AddFuelLogView.swift +++ b/Gas Man/AddFuelLogView.swift @@ -12,10 +12,10 @@ struct AddFuelLogView: View { @Environment(\.managedObjectContext) private var viewContext @Environment(\.dismiss) var dismiss - // Form fields + // Form fields for fuel log @State private var date = Date() @State private var odometer = "" - @State private var fuelVolume = "" + @State private var fuelVolume: String = "" @State private var cost = "" @State private var locationCoordinates = "" @State private var locationName = "" @@ -31,29 +31,83 @@ struct AddFuelLogView: View { // For tracking the previous odometer reading @State private var previousOdometer: Double? = nil @State private var showOdometerAlert = false - - // Flag to avoid update loops in our calculation logic @State private var isUpdatingCalculation = false + // Vehicle selection: + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Vehicle.make, ascending: true)], + animation: .default) + private var vehicles: FetchedResults + + // Use the new UUID id for vehicle selection + @State private var selectedVehicleID: UUID? = nil + // Computed property for formatted previous odometer value private var previousOdometerString: String { previousOdometer.map { String(format: "%.0f", $0) } ?? "N/A" } - // Computed property for the odometer alert - var odometerAlert: Alert { - let messageString = "Odometer reading must be greater than the previous record (\(previousOdometerString))." - return Alert( - title: Text("Odometer Reading Error"), - message: Text(messageString), - dismissButton: .default(Text("OK")) - ) - } - var body: some View { NavigationView { Form { - fuelLogDetailsSection + Section(header: Text("Fuel Log Details")) { + DatePicker("Date", selection: $date, displayedComponents: [.date, .hourAndMinute]) + + HStack { + Text("Odometer:") + TextField("", text: $odometer) + .keyboardType(.decimalPad) + } + + HStack { + Text("Gallons:") + TextField("", text: $fuelVolume) + .keyboardType(.decimalPad) + .onChange(of: fuelVolume) { _ in updateCalculatedValues() } + } + + HStack { + Text("Price/Gal:") + TextField("", text: $pricePerGalon) + .keyboardType(.decimalPad) + .onChange(of: pricePerGalon) { _ in updateCalculatedValues() } + } + + HStack { + Text("Cost:") + TextField("", text: $cost) + .keyboardType(.decimalPad) + .onChange(of: cost) { _ in updateCalculatedValues() } + } + + Button("Get Current Location") { + locationManager.requestLocation() + } + + if !locationCoordinates.isEmpty { + Text("Coordinates: \(locationCoordinates)") + } + + TextField("Location Coordinates", text: $locationCoordinates) + TextField("Location Name", text: $locationName) + + Picker("Octane", selection: $selectedOctane) { + ForEach(octaneOptions, id: \.self) { option in + Text("\(option)").tag(option) + } + } + .pickerStyle(MenuPickerStyle()) + } + + Section(header: Text("Vehicle")) { + Picker("Select Vehicle", selection: $selectedVehicleID) { + ForEach(vehicles, id: \.id) { vehicle in + Text("\(vehicle.year) \(vehicle.make ?? "") \(vehicle.model ?? "")") + .tag(vehicle.id) + } + } + .pickerStyle(MenuPickerStyle()) + } } .navigationTitle("Add Fuel Log") .toolbar { @@ -74,101 +128,32 @@ struct AddFuelLogView: View { locationName = placemark.name ?? "" } } - .onAppear(perform: loadPreviousData) - .alert(isPresented: $showOdometerAlert) { odometerAlert } - } - } - - // MARK: - Subviews - - private var fuelLogDetailsSection: some View { - Section(header: Text("Fuel Log Details")) { - DatePicker("Date", selection: $date, displayedComponents: [.date, .hourAndMinute]) - - // Odometer field - HStack(spacing: 4) { - Text("Odometer: ") - .font(.caption) - .foregroundColor(.secondary) - TextField("", text: $odometer) - .keyboardType(.decimalPad) - .textFieldStyle(RoundedBorderTextFieldStyle()) - } - - // Fuel Volume field - HStack(spacing: 4) { - Text("Gallons: ") - .font(.caption) - .foregroundColor(.secondary) - TextField("", text: $fuelVolume) - .keyboardType(.decimalPad) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .onChange(of: fuelVolume) { _ in updateCalculatedValues() } - } - - // Price per Gallon field - HStack(spacing: 4) { - Text("Price/Gal: $") - .font(.caption) - .foregroundColor(.secondary) - TextField("", text: $pricePerGalon) - .keyboardType(.decimalPad) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .onChange(of: pricePerGalon) { _ in updateCalculatedValues() } - } - - // Cost field - HStack(spacing: 4) { - Text("Cost: $") - .font(.caption) - .foregroundColor(.secondary) - TextField("", text: $cost) - .keyboardType(.decimalPad) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .onChange(of: cost) { _ in updateCalculatedValues() } - } - - Button("Get Current Location") { - locationManager.requestLocation() - } - - if !locationCoordinates.isEmpty { - Text("Coordinates: \(locationCoordinates)") - .font(.caption) - .foregroundColor(.secondary) - } - - TextField("Location Coordinates", text: $locationCoordinates) - - HStack(spacing: 4) { - Text("Location Name:") - .font(.caption) - .foregroundColor(.secondary) - TextField("", text: $locationName) - } - - Picker("Octane", selection: $selectedOctane) { - ForEach(octaneOptions, id: \.self) { option in - Text("\(option)").tag(option) + .onAppear { + // Fetch the last FuelLog record to set defaults: + let fetchRequest: NSFetchRequest = FuelLog.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)] + fetchRequest.fetchLimit = 1 + if let lastFuelLog = try? viewContext.fetch(fetchRequest).first { + selectedOctane = Int(lastFuelLog.octane) + previousOdometer = lastFuelLog.odometer + odometer = String(format: "%.0f", lastFuelLog.odometer) + + // Set default vehicle from previous record if available: + if let lastVehicle = lastFuelLog.vehicle { + selectedVehicleID = lastVehicle.id + } + } else { + selectedOctane = 87 + odometer = "" + // If no previous fuel log, default to the first available vehicle. + selectedVehicleID = vehicles.first?.id } } - .pickerStyle(MenuPickerStyle()) - } - } - - // MARK: - Helper Methods - - private func loadPreviousData() { - let fetchRequest: NSFetchRequest = FuelLog.fetchRequest() - fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)] - fetchRequest.fetchLimit = 1 - if let lastFuelLog = try? viewContext.fetch(fetchRequest).first { - selectedOctane = Int(lastFuelLog.octane) - previousOdometer = lastFuelLog.odometer - odometer = String(format: "%.0f", lastFuelLog.odometer) - } else { - selectedOctane = 87 - odometer = "" + .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"))) + } } } @@ -181,7 +166,6 @@ struct AddFuelLogView: View { } private func updateCalculatedValues() { - // Prevent recursive updates. guard !isUpdatingCalculation else { return } isUpdatingCalculation = true @@ -189,24 +173,19 @@ struct AddFuelLogView: View { let costVal = Double(cost) let price = Double(pricePerGalon) - // 1. If fuelVolume and pricePerGalon are provided, calculate cost. if let f = fuel, let p = price, f > 0 { let computedCost = roundToTwo(f * p) let computedCostStr = String(format: "%.2f", computedCost) if cost != computedCostStr { cost = computedCostStr } - } - // 2. If fuelVolume and cost are provided, and pricePerGalon is empty, calculate pricePerGalon. - else if let f = fuel, let c = costVal, f > 0, pricePerGalon.trimmingCharacters(in: .whitespaces).isEmpty { + } else if let f = fuel, let c = costVal, f > 0, pricePerGalon.trimmingCharacters(in: .whitespaces).isEmpty { let computedPrice = roundToThree(c / f) let computedPriceStr = String(format: "%.3f", computedPrice) if pricePerGalon != computedPriceStr { pricePerGalon = computedPriceStr } - } - // 3. If pricePerGalon and cost are provided, and fuelVolume is empty, calculate fuelVolume. - else if let p = price, let c = costVal, p > 0, fuelVolume.trimmingCharacters(in: .whitespaces).isEmpty { + } else if let p = price, let c = costVal, p > 0, fuelVolume.trimmingCharacters(in: .whitespaces).isEmpty { let computedFuel = roundToThree(c / p) let computedFuelStr = String(format: "%.3f", computedFuel) if fuelVolume != computedFuelStr { @@ -218,13 +197,18 @@ struct AddFuelLogView: View { } private func saveFuelLog() { - guard let newOdometer = Double(odometer) else { - return - } - // Validate that the new odometer reading is greater than the previous record - if let previous = previousOdometer, newOdometer <= previous { - showOdometerAlert = true - return + guard let newOdometer = Double(odometer) else { return } + + // Re-fetch the latest fuel log from Core Data + let fetchRequest: NSFetchRequest = FuelLog.fetchRequest() + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)] + fetchRequest.fetchLimit = 1 + if let lastFuelLog = try? viewContext.fetch(fetchRequest).first { + // Compare against the latest fuel log's odometer. + if newOdometer <= lastFuelLog.odometer { + showOdometerAlert = true + return + } } let newLog = FuelLog(context: viewContext) @@ -238,12 +222,20 @@ struct AddFuelLogView: View { newLog.octane = Int16(selectedOctane) newLog.pricePerGalon = Double(pricePerGalon) ?? 0 + // Set the vehicle relationship if a vehicle was selected: + if let vehicleID = selectedVehicleID, + let selectedVehicle = vehicles.first(where: { $0.id == vehicleID }) { + newLog.vehicle = selectedVehicle + } + do { try viewContext.save() + print("Saved fuel log with vehicle: \(newLog.vehicle?.make ?? "none")") dismiss() } catch { let nsError = error as NSError fatalError("Unresolved error \(nsError), \(nsError.userInfo)") } } + } diff --git a/Gas Man/AddVehicleView.swift b/Gas Man/AddVehicleView.swift new file mode 100644 index 0000000..23d937f --- /dev/null +++ b/Gas Man/AddVehicleView.swift @@ -0,0 +1,177 @@ +// +// AddVehicleView.swift +// Gas Man +// +// Created by Kameron Kenny on 3/18/25. +// + +import SwiftUI + +struct AddVehicleView: View { + @Environment(\.managedObjectContext) private var viewContext + @Environment(\.dismiss) var dismiss + + // Basic information + @State private var year: String = "" + @State private var make: String = "" + @State private var model: String = "" + @State private var color: String = "" + + // Purchase information + @State private var purchaseDate: Date = Date() + @State private var purchaseDateEnabled: Bool = false + @State private var purchasePrice: String = "" + @State private var odometerAtPurchase: String = "" + + // Sale information + @State private var soldDate: Date = Date() + @State private var soldDateEnabled: Bool = false + @State private var odometerAtSale: String = "" + + // Additional notes + @State private var notes: String = "" + + // New additional vehicle fields + @State private var engineName: String = "" + @State private var engineDisplacement: String = "" + @State private var transmission: String = "Automatic" // default value + @State private var vehicleType: String = "" + @State private var wheelSizeWidth: String = "" + @State private var wheelSizeDiameter: String = "" + @State private var tireSizeWidth: String = "" + @State private var tireSizeHeight: String = "" + @State private var tireSizeRadius: String = "" + + // New additional vehicle fields for tire and wheel info: + @State private var tireBrand: String = "" + @State private var tireModel: String = "" + @State private var wheelBrand: String = "" + @State private var wheelModel: String = "" + + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + return formatter + }() + + var body: some View { + NavigationView { + Form { + Section(header: Text("Basic Information")) { + Picker("Vehicle Type", selection: $vehicleType) { + Text("Sedan").tag("Sedan") + Text("SUV").tag("SUV") + Text("Truck").tag("Truck") + Text("Van").tag("Van") + } + .pickerStyle(MenuPickerStyle()) + TextField("Year", text: $year) + .keyboardType(.numberPad) + TextField("Make", text: $make) + TextField("Model", text: $model) + TextField("Color", text: $color) + } + Section(header: Text("Purchase Information")) { + Toggle("Add Purchase Date", isOn: $purchaseDateEnabled) + if purchaseDateEnabled { + DatePicker("Purchase Date", selection: $purchaseDate, displayedComponents: .date) + TextField("Purchase Price", text: $purchasePrice) + .keyboardType(.decimalPad) + TextField("Odometer at Purchase", text: $odometerAtPurchase) + .keyboardType(.decimalPad) + } + } + Section(header: Text("Engine & Transmission")) { + TextField("Engine Name", text: $engineName) + TextField("Engine Displacement (L)", text: $engineDisplacement) + .keyboardType(.decimalPad) + Picker("Transmission", selection: $transmission) { + Text("Automatic").tag("Automatic") + Text("Manual").tag("Manual") + } + .pickerStyle(MenuPickerStyle()) + } + + Section(header: Text("Tires")) { + TextField("Tire Brand", text: $tireBrand) + TextField("Tire Model", text: $tireModel) + TextField("Tire Size Width", text: $tireSizeWidth) + .keyboardType(.decimalPad) + TextField("Tire Size Height", text: $tireSizeHeight) + .keyboardType(.decimalPad) + TextField("Tire Size Radius", text: $tireSizeRadius) + .keyboardType(.decimalPad) + } + + Section(header: Text("Wheels")) { + TextField("Wheel Brand", text: $wheelBrand) + TextField("Wheel Model", text: $wheelModel) + TextField("Wheel Size Width", text: $wheelSizeWidth) + .keyboardType(.decimalPad) + TextField("Wheel Size Diameter", text: $wheelSizeDiameter) + .keyboardType(.decimalPad) + } + Section(header: Text("Sale Information")) { + Toggle("Add Sold Date", isOn: $soldDateEnabled) + if soldDateEnabled { + DatePicker("Date Sold", selection: $soldDate, displayedComponents: .date) + TextField("Odometer at Sale", text: $odometerAtSale) + .keyboardType(.decimalPad) + } + + } + Section(header: Text("Notes")) { + TextField("Notes", text: $notes) + } + } + .navigationTitle("Add Vehicle") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { saveVehicle() } + } + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { dismiss() } + } + } + } + } + + private func saveVehicle() { + let newVehicle = Vehicle(context: viewContext) + newVehicle.id = UUID() + newVehicle.year = Int16(year) ?? 0 + newVehicle.make = make + newVehicle.model = model + newVehicle.color = color + newVehicle.purchaseDate = purchaseDateEnabled ? purchaseDate : nil + newVehicle.purchasePrice = Double(purchasePrice).map { NSNumber(value: $0) } + newVehicle.odometerAtPurchase = Double(odometerAtPurchase).map { NSNumber(value: $0) } + newVehicle.soldDate = soldDateEnabled ? soldDate : nil + newVehicle.odometerAtSale = Double(odometerAtSale).map { NSNumber(value: $0) } + newVehicle.notes = notes.isEmpty ? nil : notes + newVehicle.tireBrand = tireBrand.isEmpty ? nil : tireBrand + newVehicle.tireModel = tireModel.isEmpty ? nil : tireModel + newVehicle.wheelBrand = wheelBrand.isEmpty ? nil : wheelBrand + newVehicle.wheelModel = wheelModel.isEmpty ? nil : wheelModel + + // Save additional vehicle information: + newVehicle.engineName = engineName.isEmpty ? nil : engineName + newVehicle.engineDisplacement = Double(engineDisplacement).map { NSNumber(value: $0) } + newVehicle.transmission = transmission.isEmpty ? "Automatic" : transmission + newVehicle.vehicleType = vehicleType.isEmpty ? nil : vehicleType + newVehicle.wheelSizeWidth = Double(wheelSizeWidth).map { NSNumber(value: $0) } + newVehicle.wheelSizeDiameter = Double(wheelSizeDiameter).map { NSNumber(value: $0) } + newVehicle.tireSizeWidth = Double(tireSizeWidth).map { NSNumber(value: $0) } + newVehicle.tireSizeHeight = Double(tireSizeHeight).map { NSNumber(value: $0) } + newVehicle.tireSizeRadius = Double(tireSizeRadius).map { NSNumber(value: $0) } + + do { + try viewContext.save() + dismiss() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } +} + diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/Contents.json b/Gas Man/Assets.xcassets/AppIcon.appiconset/Contents.json index ffdfe15..f84a390 100644 --- a/Gas Man/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Gas Man/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "fuel pump-1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -12,6 +13,7 @@ "value" : "dark" } ], + "filename" : "fuel pump-1024 1.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -23,56 +25,67 @@ "value" : "tinted" } ], + "filename" : "fuel pump-1024 2.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { + "filename" : "fuel pump-16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { + "filename" : "fuel pump-32.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { + "filename" : "fuel pump-32 1.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { + "filename" : "fuel pump-64.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { + "filename" : "fuel pump-128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { + "filename" : "fuel pump-256 1.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { + "filename" : "fuel pump-256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { + "filename" : "fuel pump-512 1.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { + "filename" : "fuel pump-512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { + "filename" : "fuel pump-1024 3.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024 1.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024 1.png new file mode 100644 index 0000000..ef4431a Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024 1.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024 2.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024 2.png new file mode 100644 index 0000000..ef4431a Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024 2.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024 3.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024 3.png new file mode 100644 index 0000000..ef4431a Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024 3.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024.png new file mode 100644 index 0000000..ef4431a Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-1024.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-128.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-128.png new file mode 100644 index 0000000..2f9ab04 Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-128.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-16.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-16.png new file mode 100644 index 0000000..5de3288 Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-16.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-256 1.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-256 1.png new file mode 100644 index 0000000..d6ad0d4 Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-256 1.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-256.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-256.png new file mode 100644 index 0000000..d6ad0d4 Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-256.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-32 1.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-32 1.png new file mode 100644 index 0000000..223f8ba Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-32 1.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-32.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-32.png new file mode 100644 index 0000000..223f8ba Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-32.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-512 1.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-512 1.png new file mode 100644 index 0000000..75e213c Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-512 1.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-512.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-512.png new file mode 100644 index 0000000..75e213c Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-512.png differ diff --git a/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-64.png b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-64.png new file mode 100644 index 0000000..4a04c1b Binary files /dev/null and b/Gas Man/Assets.xcassets/AppIcon.appiconset/fuel pump-64.png differ diff --git a/Gas Man/EditVehicleView.swift b/Gas Man/EditVehicleView.swift new file mode 100644 index 0000000..2c96b0d --- /dev/null +++ b/Gas Man/EditVehicleView.swift @@ -0,0 +1,256 @@ +// +// EditVehicleView.swift +// Gas Man +// +// Created by Kameron Kenny on 3/18/25. +// + + +import SwiftUI + +struct EditVehicleView: View { + @Environment(\.managedObjectContext) private var viewContext + @Environment(\.dismiss) var dismiss + + @ObservedObject var vehicle: Vehicle + + // Basic Information + @State private var year: String = "" + @State private var make: String = "" + @State private var model: String = "" + @State private var color: String = "" + + // Purchase Information + @State private var purchaseDate: Date = Date() + @State private var purchaseDateEnabled: Bool = false + @State private var purchasePrice: String = "" + @State private var odometerAtPurchase: String = "" + + // Sale Information + @State private var soldDate: Date = Date() + @State private var soldDateEnabled: Bool = false + @State private var odometerAtSale: String = "" + + // Additional Vehicle Information + @State private var engineName: String = "" + @State private var engineDisplacement: String = "" + @State private var transmission: String = "Automatic" // default + @State private var vehicleType: String = "" + @State private var wheelSizeWidth: String = "" + @State private var wheelSizeDiameter: String = "" + @State private var tireSizeWidth: String = "" + @State private var tireSizeHeight: String = "" + @State private var tireSizeRadius: String = "" + + // Tire & Wheel Brand/Model + @State private var tireBrand: String = "" + @State private var tireModel: String = "" + @State private var wheelBrand: String = "" + @State private var wheelModel: String = "" + + // Additional Notes + @State private var notes: String = "" + + var body: some View { + NavigationView { + Form { + Section(header: Text("Basic Information")) { + Picker("Vehicle Type", selection: $vehicleType) { + Text("Sedan").tag("Sedan") + Text("SUV").tag("SUV") + Text("Truck").tag("Truck") + Text("Van").tag("Van") + } + .pickerStyle(MenuPickerStyle()) + TextField("Year", text: $year) + .keyboardType(.numberPad) + TextField("Make", text: $make) + TextField("Model", text: $model) + TextField("Color", text: $color) + } + Section(header: Text("Purchase Information")) { + Toggle("Add Purchase Date", isOn: $purchaseDateEnabled) + if purchaseDateEnabled { + DatePicker("Purchase Date", selection: $purchaseDate, displayedComponents: .date) + TextField("Purchase Price", text: $purchasePrice) + .keyboardType(.decimalPad) + TextField("Odometer at Purchase", text: $odometerAtPurchase) + .keyboardType(.decimalPad) + } + } + Section(header: Text("Engine & Transmission")) { + TextField("Engine Name", text: $engineName) + TextField("Engine Displacement (L)", text: $engineDisplacement) + .keyboardType(.decimalPad) + Picker("Transmission", selection: $transmission) { + Text("Automatic").tag("Automatic") + Text("Manual").tag("Manual") + } + .pickerStyle(MenuPickerStyle()) + } + + Section(header: Text("Tires")) { + TextField("Tire Brand", text: $tireBrand) + TextField("Tire Model", text: $tireModel) + TextField("Tire Size Width", text: $tireSizeWidth) + .keyboardType(.decimalPad) + TextField("Tire Size Height", text: $tireSizeHeight) + .keyboardType(.decimalPad) + TextField("Tire Size Radius", text: $tireSizeRadius) + .keyboardType(.decimalPad) + } + + Section(header: Text("Wheels")) { + TextField("Wheel Brand", text: $wheelBrand) + TextField("Wheel Model", text: $wheelModel) + TextField("Wheel Size Width", text: $wheelSizeWidth) + .keyboardType(.decimalPad) + TextField("Wheel Size Diameter", text: $wheelSizeDiameter) + .keyboardType(.decimalPad) + } + Section(header: Text("Sale Information")) { + Toggle("Add Sold Date", isOn: $soldDateEnabled) + if soldDateEnabled { + DatePicker("Date Sold", selection: $soldDate, displayedComponents: .date) + TextField("Odometer at Sale", text: $odometerAtSale) + .keyboardType(.decimalPad) + } + + } + Section(header: Text("Notes")) { + TextField("Notes", text: $notes) + } + } + .navigationTitle("Edit Vehicle") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { saveChanges() } + } + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { dismiss() } + } + } + } + .onAppear(perform: loadVehicleData) + } + + private func loadVehicleData() { + // Basic Information + year = "\(vehicle.year)" + make = vehicle.make ?? "" + model = vehicle.model ?? "" + color = vehicle.color ?? "" + + // Purchase Information + if let pDate = vehicle.purchaseDate { + purchaseDate = pDate + purchaseDateEnabled = true + } else { + purchaseDateEnabled = false + } + if let pPrice = vehicle.purchasePrice?.doubleValue { + purchasePrice = String(format: "%.2f", pPrice) + } else { + purchasePrice = "" + } + if let odoPurchase = vehicle.odometerAtPurchase?.doubleValue { + odometerAtPurchase = String(format: "%.0f", odoPurchase) + } else { + odometerAtPurchase = "" + } + + // Sale Information + if let sDate = vehicle.soldDate { + soldDate = sDate + soldDateEnabled = true + } else { + soldDateEnabled = false + } + if let odoSale = vehicle.odometerAtSale?.doubleValue { + odometerAtSale = String(format: "%.0f", odoSale) + } else { + odometerAtSale = "" + } + + // Additional Vehicle Information + engineName = vehicle.engineName ?? "" + if let ed = vehicle.engineDisplacement?.doubleValue { + engineDisplacement = String(format: "%.3f", ed) + } else { + engineDisplacement = "" + } + transmission = vehicle.transmission ?? "Automatic" + vehicleType = vehicle.vehicleType ?? "" + if let wsWidth = vehicle.wheelSizeWidth?.doubleValue { + wheelSizeWidth = String(format: "%.3f", wsWidth) + } else { + wheelSizeWidth = "" + } + if let wsDiameter = vehicle.wheelSizeDiameter?.doubleValue { + wheelSizeDiameter = String(format: "%.3f", wsDiameter) + } else { + wheelSizeDiameter = "" + } + if let tsWidth = vehicle.tireSizeWidth?.doubleValue { + tireSizeWidth = String(format: "%.3f", tsWidth) + } else { + tireSizeWidth = "" + } + if let tsHeight = vehicle.tireSizeHeight?.doubleValue { + tireSizeHeight = String(format: "%.3f", tsHeight) + } else { + tireSizeHeight = "" + } + if let tsRadius = vehicle.tireSizeRadius?.doubleValue { + tireSizeRadius = String(format: "%.3f", tsRadius) + } else { + tireSizeRadius = "" + } + + // Tire & Wheel Brand/Model + tireBrand = vehicle.tireBrand ?? "" + tireModel = vehicle.tireModel ?? "" + wheelBrand = vehicle.wheelBrand ?? "" + wheelModel = vehicle.wheelModel ?? "" + + // Notes + notes = vehicle.notes ?? "" + } + + private func saveChanges() { + vehicle.year = Int16(year) ?? 0 + vehicle.make = make + vehicle.model = model + vehicle.color = color + vehicle.purchaseDate = purchaseDateEnabled ? purchaseDate : nil + vehicle.purchasePrice = Double(purchasePrice).map { NSNumber(value: $0) } + vehicle.odometerAtPurchase = Double(odometerAtPurchase).map { NSNumber(value: $0) } + vehicle.soldDate = soldDateEnabled ? soldDate : nil + vehicle.odometerAtSale = Double(odometerAtSale).map { NSNumber(value: $0) } + + vehicle.engineName = engineName.isEmpty ? nil : engineName + vehicle.engineDisplacement = Double(engineDisplacement).map { NSNumber(value: $0) } + vehicle.transmission = transmission.isEmpty ? "Automatic" : transmission + vehicle.vehicleType = vehicleType.isEmpty ? nil : vehicleType + vehicle.wheelSizeWidth = Double(wheelSizeWidth).map { NSNumber(value: $0) } + vehicle.wheelSizeDiameter = Double(wheelSizeDiameter).map { NSNumber(value: $0) } + vehicle.tireSizeWidth = Double(tireSizeWidth).map { NSNumber(value: $0) } + vehicle.tireSizeHeight = Double(tireSizeHeight).map { NSNumber(value: $0) } + vehicle.tireSizeRadius = Double(tireSizeRadius).map { NSNumber(value: $0) } + + vehicle.tireBrand = tireBrand.isEmpty ? nil : tireBrand + vehicle.tireModel = tireModel.isEmpty ? nil : tireModel + vehicle.wheelBrand = wheelBrand.isEmpty ? nil : wheelBrand + vehicle.wheelModel = wheelModel.isEmpty ? nil : wheelModel + + vehicle.notes = notes.isEmpty ? nil : notes + + do { + try viewContext.save() + dismiss() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } +} diff --git a/Gas Man/FuelLogDetailView.swift b/Gas Man/FuelLogDetailView.swift index 6857a66..cca6550 100644 --- a/Gas Man/FuelLogDetailView.swift +++ b/Gas Man/FuelLogDetailView.swift @@ -14,13 +14,21 @@ private let dateFormatter: DateFormatter = { return formatter }() - struct FuelLogDetailView: View { var fuelLog: FuelLog var body: some View { Form { - Section(header: Text("Basic Information")) { + // Vehicle header section + if let vehicle = fuelLog.vehicle { + Section { + Text("\(vehicle.year) \(vehicle.model ?? "")") + .font(.headline) + .frame(maxWidth: .infinity, alignment: .center) + } + } + + Section(header: Text("")) { HStack { Text("Date:") Spacer() diff --git a/Gas Man/FuelLogListView.swift b/Gas Man/FuelLogListView.swift index f184b57..ba88bac 100644 --- a/Gas Man/FuelLogListView.swift +++ b/Gas Man/FuelLogListView.swift @@ -1,79 +1,159 @@ -// -// FuelLogListView.swift -// Gas Man -// -// Created by Kameron Kenny on 3/17/25. -// - - 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 - @State private var showingAddFuelLog = false // Controls sheet presentation + // All vehicles (sorted by make) + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Vehicle.make, ascending: true)], + animation: .default) + private var vehicles: FetchedResults + + @State private var showingAddFuelLog = false + @State private var selectedVehicleID: UUID? = nil + @State private var showAddVehicleSheet = false + + // For tracking the previous odometer reading + @State private var previousOdometer: Double? = nil + @State private var showOdometerAlert = false + @State private var isUpdatingCalculation = 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) + } + } var body: some View { NavigationView { - List { - ForEach(Array(fuelLogs.enumerated()), id: \.element) { index, log in - // Compute distance since the previous record: - let distance: Double? = { - if index < fuelLogs.count - 1 { - let previousLog = fuelLogs[index + 1] - let milesDriven = log.odometer - previousLog.odometer - return milesDriven > 0 ? milesDriven : nil - } - return nil - }() - - // Compute MPG (if possible) - let mpg: Double? = { - if index < fuelLogs.count - 1 { - let previousLog = fuelLogs[index + 1] - let milesDriven = log.odometer - previousLog.odometer - if log.fuelVolume > 0 && milesDriven > 0 { - return milesDriven / log.fuelVolume + 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: \.objectID) { vehicle in + // Only tag if vehicle.id is not nil. + if let vid = vehicle.id { + Text("\(vehicle.year) \(vehicle.make ?? "") \(vehicle.model ?? "")") + .tag(vid as UUID?) + } else { + // If id is nil, you can still show the row but tag with nil. + Text("\(vehicle.year) \(vehicle.make ?? "") \(vehicle.model ?? "")") + .tag(nil as UUID?) + } } } - return nil - }() + .pickerStyle(MenuPickerStyle()) + } - NavigationLink(destination: FuelLogDetailView(fuelLog: log)) { - FuelLogSummaryView(log: log, mpg: mpg, distanceSincePrevious: distance) + // 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? = { + 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() } } - .onDelete(perform: deleteFuelLogs) - } - .listStyle(InsetGroupedListStyle()) - .navigationTitle("Fuel Logs") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - showingAddFuelLog = true // Present AddFuelLogView - }) { - Label("Add Fuel Log", systemImage: "plus") - } + .sheet(isPresented: $showingAddFuelLog) { + AddFuelLogView().environment(\.managedObjectContext, viewContext) } - ToolbarItem(placement: .navigationBarLeading) { - EditButton() + .onAppear { + print("Total fuel logs: \(fuelLogs.count)") + setDefaultVehicle() } } - // Sheet presentation for AddFuelLogView - .sheet(isPresented: $showingAddFuelLog) { - AddFuelLogView().environment(\.managedObjectContext, viewContext) + } + .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 { - offsets.map { fuelLogs[$0] }.forEach(viewContext.delete) + let logsToDelete = offsets.map { filteredFuelLogs[$0] } + logsToDelete.forEach { viewContext.delete($0) } do { try viewContext.save() } catch { diff --git a/Gas Man/Gas_Man.xcdatamodeld/Gas_Man.xcdatamodel/contents b/Gas Man/Gas_Man.xcdatamodeld/Gas_Man.xcdatamodel/contents index 066b8a8..dbb6761 100644 --- a/Gas Man/Gas_Man.xcdatamodeld/Gas_Man.xcdatamodel/contents +++ b/Gas Man/Gas_Man.xcdatamodeld/Gas_Man.xcdatamodel/contents @@ -1,15 +1,17 @@ - - - + + + + - - - + + + + @@ -22,4 +24,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Gas Man/MainTabView.swift b/Gas Man/MainTabView.swift index aa38f94..b80e869 100644 --- a/Gas Man/MainTabView.swift +++ b/Gas Man/MainTabView.swift @@ -14,6 +14,10 @@ struct MainTabView: View { .tabItem { Label("Fuel", systemImage: "fuelpump.fill") } + VehicleListView() + .tabItem { + Label("Vehicles", systemImage: "car.fill") + } MaintenanceListView() .tabItem { Label("Maintenance", systemImage: "wrench.fill") diff --git a/Gas Man/Vehicle.swift b/Gas Man/Vehicle.swift new file mode 100644 index 0000000..9994c50 --- /dev/null +++ b/Gas Man/Vehicle.swift @@ -0,0 +1,56 @@ +// +// Vehicle.swift +// Gas Man +// +// Created by Kameron Kenny on 3/18/25. +// + +import Foundation +import CoreData + +@objc(Vehicle) +public class Vehicle: NSManagedObject { + public override func awakeFromInsert() { + super.awakeFromInsert() + self.id = UUID() + print("Assigned new UUID: \(self.id)") + } +} + +extension Vehicle: Identifiable { + @NSManaged public var id: UUID? + + // Computed property to always get a non-optional UUID. + public var nonOptionalID: UUID { + id ?? UUID() + } + + @NSManaged public var year: Int16 + @NSManaged public var make: String? + @NSManaged public var model: String? + @NSManaged public var color: String? + @NSManaged public var purchaseDate: Date? + @NSManaged public var purchasePrice: NSNumber? + @NSManaged public var soldDate: Date? + @NSManaged public var odometerAtPurchase: NSNumber? + @NSManaged public var odometerAtSale: NSNumber? + @NSManaged public var notes: String? + + // Existing additional fields + @NSManaged public var engineName: String? + @NSManaged public var engineDisplacement: NSNumber? + @NSManaged public var transmission: String? + @NSManaged public var vehicleType: String? + @NSManaged public var wheelSizeWidth: NSNumber? + @NSManaged public var wheelSizeDiameter: NSNumber? + @NSManaged public var tireSizeWidth: NSNumber? + @NSManaged public var tireSizeHeight: NSNumber? + @NSManaged public var tireSizeRadius: NSNumber? + + // New fields for tire and wheel brands/models + @NSManaged public var tireBrand: String? + @NSManaged public var tireModel: String? + @NSManaged public var wheelBrand: String? + @NSManaged public var wheelModel: String? +} + diff --git a/Gas Man/VehicleDetailView.swift b/Gas Man/VehicleDetailView.swift new file mode 100644 index 0000000..68f90c3 --- /dev/null +++ b/Gas Man/VehicleDetailView.swift @@ -0,0 +1,195 @@ +// +// VehicleDetailView.swift +// Gas Man +// +// Created by Kameron Kenny on 3/18/25. +// +import SwiftUI + +struct VehicleDetailView: View { + @ObservedObject var vehicle: Vehicle + @State private var showingEditVehicle = false + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + return formatter + }() + + var body: some View { + Form { + Section(header: Text("Basic Information")) { + HStack { + Text("Vehicle Type:") + Spacer() + Text(vehicle.vehicleType ?? "") + } + HStack { + Text("Year:") + Spacer() + Text("\(vehicle.year)") + } + HStack { + Text("Make:") + Spacer() + Text(vehicle.make ?? "") + } + HStack { + Text("Model:") + Spacer() + Text(vehicle.model ?? "") + } + HStack { + Text("Color:") + Spacer() + Text(vehicle.color ?? "") + } + } + Section(header: Text("Purchase Information")) { + HStack { + Text("Date:") + Spacer() + Text(vehicle.purchaseDate != nil ? dateFormatter.string(from: vehicle.purchaseDate!) : "") + } + HStack { + Text("Price:") + Spacer() + if let price = vehicle.purchasePrice { + Text("$\(price.doubleValue, specifier: "%.2f")") + } else { + Text("") + } + } + HStack { + Text("Odometer:") + Spacer() + if let odo = vehicle.odometerAtPurchase { + Text("\(odo.doubleValue, specifier: "%.0f") miles") + } else { + Text("") + } + } + } + + Section(header: Text("Engine & Transmission")) { + HStack { + Text("Engine Name:") + Spacer() + Text(vehicle.engineName ?? "") + } + HStack { + Text("Engine Displacement:") + Spacer() + if let ed = vehicle.engineDisplacement { + Text("\(ed.doubleValue, specifier: "%.3f") L") + } else { + Text("") + } + } + HStack { + Text("Transmission:") + Spacer() + Text(vehicle.transmission ?? "Automatic") + } + } + + Section(header: Text("Tires")) { + HStack { + Text("Brand:") + Spacer() + Text(vehicle.tireBrand ?? "") + } + HStack { + Text("Model:") + Spacer() + Text(vehicle.tireModel ?? "") + } + HStack { + Text("Size:") + Spacer() + if let tw = vehicle.tireSizeWidth { + Text("\(tw.doubleValue, specifier: "%.0f")") + } else { + Text("-") + } + Text("/") + if let th = vehicle.tireSizeHeight { + Text("\(th.doubleValue, specifier: "%.0f")") + } else { + Text("-") + } + Text("r") + if let tr = vehicle.tireSizeRadius { + Text("\(tr.doubleValue, specifier: "%.0f")") + } else { + Text("-") + } + + } + } + + Section(header: Text("Wheels")) { + HStack { + Text("Brand:") + Spacer() + Text(vehicle.wheelBrand ?? "") + } + HStack { + Text("Model:") + Spacer() + Text(vehicle.wheelModel ?? "") + } + HStack { + Text("Size:") + Spacer() + if let wd = vehicle.wheelSizeDiameter { + Text("\(wd.doubleValue, specifier: "%.3f")") + } else { + Text("-") + } + Text(" x ") + if let ws = vehicle.wheelSizeWidth { + Text("\(ws.doubleValue, specifier: "%.3f")") + } else { + Text("-") + } + } + } + + if vehicle.soldDate != nil { + Section(header: Text("Sale Information")) { + HStack { + Text("Date Sold:") + Spacer() + Text(vehicle.soldDate != nil ? dateFormatter.string(from: vehicle.soldDate!) : "") + } + HStack { + Text("Odometer at Sale:") + Spacer() + if let odo = vehicle.odometerAtSale { + Text("\(odo.doubleValue, specifier: "%.0f") miles") + } else { + Text("") + } + } + } + } + + Section(header: Text("Notes")) { + Text(vehicle.notes ?? "") + } + } + .navigationTitle("\(vehicle.make ?? "") \(vehicle.model ?? "")") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Edit") { + showingEditVehicle = true + } + } + } + .sheet(isPresented: $showingEditVehicle) { + EditVehicleView(vehicle: vehicle) + .environment(\.managedObjectContext, vehicle.managedObjectContext!) + } + } +} + diff --git a/Gas Man/VehicleListView.swift b/Gas Man/VehicleListView.swift new file mode 100644 index 0000000..3814c8a --- /dev/null +++ b/Gas Man/VehicleListView.swift @@ -0,0 +1,59 @@ +// +// VehicleListView.swift +// Gas Man +// +// Created by Kameron Kenny on 3/18/25. +// + + +import SwiftUI +import CoreData + +struct VehicleListView: View { + @Environment(\.managedObjectContext) private var viewContext + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Vehicle.make, ascending: true)], + animation: .default) + private var vehicles: FetchedResults + + @State private var showingAddVehicle = false + + var body: some View { + NavigationView { + List { + ForEach(vehicles) { vehicle in + NavigationLink(destination: VehicleDetailView(vehicle: vehicle)) { + VehicleRowView(vehicle: vehicle) + } + } + .onDelete(perform: deleteVehicles) + } + .navigationTitle("Vehicles") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { showingAddVehicle = true }) { + Label("Add Vehicle", systemImage: "plus") + } + } + ToolbarItem(placement: .navigationBarLeading) { + EditButton() + } + } + .sheet(isPresented: $showingAddVehicle) { + AddVehicleView().environment(\.managedObjectContext, viewContext) + } + } + } + + private func deleteVehicles(offsets: IndexSet) { + withAnimation { + offsets.map { vehicles[$0] }.forEach(viewContext.delete) + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } +} diff --git a/Gas Man/VehicleRowView.swift b/Gas Man/VehicleRowView.swift new file mode 100644 index 0000000..f6c70f2 --- /dev/null +++ b/Gas Man/VehicleRowView.swift @@ -0,0 +1,23 @@ +// +// VehicleRowView.swift +// Gas Man +// +// Created by Kameron Kenny on 3/18/25. +// + + +import SwiftUI + +struct VehicleRowView: View { + var vehicle: Vehicle + + var body: some View { + VStack(alignment: .leading) { + Text("\(vehicle.year) \(vehicle.make ?? "") \(vehicle.model ?? "")") + .font(.headline) + Text(vehicle.color ?? "") + .font(.subheadline) + .foregroundColor(.secondary) + } + } +}