vehicle
|
@ -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<Vehicle>
|
||||
|
||||
// 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 }
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Fetch the last FuelLog record to set defaults:
|
||||
let fetchRequest: NSFetchRequest<FuelLog> = 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)
|
||||
|
||||
// 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)
|
||||
// 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> = 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> = 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)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
After Width: | Height: | Size: 3.1 MiB |
After Width: | Height: | Size: 3.1 MiB |
After Width: | Height: | Size: 3.1 MiB |
After Width: | Height: | Size: 3.1 MiB |
After Width: | Height: | Size: 73 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 245 KiB |
After Width: | Height: | Size: 245 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 911 KiB |
After Width: | Height: | Size: 911 KiB |
After Width: | Height: | Size: 23 KiB |
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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<FuelLog>
|
||||
|
||||
@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<Vehicle>
|
||||
|
||||
@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 {
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="23H222" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
||||
<entity name="FuelLog" representedClassName="FuelLog" isAbstract="YES" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="cost" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="fuelVolume" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="cost" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" attributeType="Date" defaultDateTimeInterval="764004600" usesScalarValueType="NO"/>
|
||||
<attribute name="fuelVolume" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="fullTank" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="locationCoordinates" optional="YES" attributeType="String"/>
|
||||
<attribute name="locationName" optional="YES" attributeType="String"/>
|
||||
<attribute name="octane" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="odometer" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="pricePerGalon" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="octane" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="odometer" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="pricePerGalon" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="vehicle" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Vehicle" inverseName="fuelLog" inverseEntity="Vehicle"/>
|
||||
</entity>
|
||||
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class"/>
|
||||
<entity name="MaintenanceEvent" representedClassName="MaintenanceEvent" isAbstract="YES" syncable="YES" codeGenerationType="class">
|
||||
|
@ -22,4 +24,31 @@
|
|||
<attribute name="notes" optional="YES" attributeType="String"/>
|
||||
<attribute name="odometer" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
</entity>
|
||||
<entity name="Vehicle" representedClassName="Vehicle" syncable="YES">
|
||||
<attribute name="color" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="engineDisplacement" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="engineName" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO" customClassName="NSUUID"/>
|
||||
<attribute name="make" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="model" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="notes" optional="YES" attributeType="String"/>
|
||||
<attribute name="odometerAtPurchase" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="odometerAtSale" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="purchaseDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="purchasePrice" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="soldDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="tireBrand" optional="YES" attributeType="String"/>
|
||||
<attribute name="tireModel" optional="YES" attributeType="String"/>
|
||||
<attribute name="tireSizeHeight" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="tireSizeRadius" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="tireSizeWidth" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="transmission" optional="YES" attributeType="String" defaultValueString="Automatic"/>
|
||||
<attribute name="vehicleType" optional="YES" attributeType="String"/>
|
||||
<attribute name="wheelBrand" optional="YES" attributeType="String"/>
|
||||
<attribute name="wheelModel" optional="YES" attributeType="String"/>
|
||||
<attribute name="wheelSizeDiameter" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="wheelSizeWidth" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="year" attributeType="Integer 16" minValueString="1900" maxValueString="2100" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="fuelLog" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="FuelLog" inverseName="vehicle" inverseEntity="FuelLog"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -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")
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Vehicle>
|
||||
|
||||
@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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|