250 lines
8.9 KiB
Swift
250 lines
8.9 KiB
Swift
//
|
|
// AddFuelLogView.swift
|
|
// Gas Man
|
|
//
|
|
// Created by Kameron Kenny on 3/17/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
import CoreData
|
|
|
|
struct AddFuelLogView: View {
|
|
@Environment(\.managedObjectContext) private var viewContext
|
|
@Environment(\.dismiss) var dismiss
|
|
|
|
// Form fields
|
|
@State private var date = Date()
|
|
@State private var odometer = ""
|
|
@State private var fuelVolume = ""
|
|
@State private var cost = ""
|
|
@State private var locationCoordinates = ""
|
|
@State private var locationName = ""
|
|
@State private var selectedOctane: Int = 87 // Default or from previous record
|
|
@State private var pricePerGalon = ""
|
|
|
|
// Allowed octane options
|
|
let octaneOptions = [87, 89, 91, 92, 93, 95]
|
|
|
|
// Location manager for automatic location updates
|
|
@StateObject private var locationManager = LocationManager()
|
|
|
|
// 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
|
|
|
|
// 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
|
|
}
|
|
.navigationTitle("Add Fuel Log")
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button("Save") { saveFuelLog() }
|
|
}
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
Button("Cancel") { dismiss() }
|
|
}
|
|
}
|
|
.onChange(of: locationManager.location) { newLocation in
|
|
if let loc = newLocation {
|
|
locationCoordinates = "\(loc.coordinate.latitude), \(loc.coordinate.longitude)"
|
|
}
|
|
}
|
|
.onChange(of: locationManager.placemark) { newPlacemark in
|
|
if let placemark = newPlacemark {
|
|
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)
|
|
}
|
|
}
|
|
.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 = ""
|
|
}
|
|
}
|
|
|
|
private func roundToThree(_ value: Double) -> Double {
|
|
return (value * 1000).rounded() / 1000
|
|
}
|
|
|
|
private func roundToTwo(_ value: Double) -> Double {
|
|
return (value * 100).rounded() / 100
|
|
}
|
|
|
|
private func updateCalculatedValues() {
|
|
// Prevent recursive updates.
|
|
guard !isUpdatingCalculation else { return }
|
|
isUpdatingCalculation = true
|
|
|
|
let fuel = Double(fuelVolume)
|
|
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 {
|
|
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 {
|
|
let computedFuel = roundToThree(c / p)
|
|
let computedFuelStr = String(format: "%.3f", computedFuel)
|
|
if fuelVolume != computedFuelStr {
|
|
fuelVolume = computedFuelStr
|
|
}
|
|
}
|
|
|
|
isUpdatingCalculation = false
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
let newLog = FuelLog(context: viewContext)
|
|
newLog.id = UUID()
|
|
newLog.date = date
|
|
newLog.odometer = newOdometer
|
|
newLog.fuelVolume = Double(fuelVolume) ?? 0
|
|
newLog.cost = Double(cost) ?? 0
|
|
newLog.locationCoordinates = locationCoordinates
|
|
newLog.locationName = locationName
|
|
newLog.octane = Int16(selectedOctane)
|
|
newLog.pricePerGalon = Double(pricePerGalon) ?? 0
|
|
|
|
do {
|
|
try viewContext.save()
|
|
dismiss()
|
|
} catch {
|
|
let nsError = error as NSError
|
|
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
|
}
|
|
}
|
|
}
|