diff --git a/Gas Man.xcodeproj/project.pbxproj b/Gas Man.xcodeproj/project.pbxproj
index d167c41..d459cf9 100644
--- a/Gas Man.xcodeproj/project.pbxproj
+++ b/Gas Man.xcodeproj/project.pbxproj
@@ -289,7 +289,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "Gas Man/Gas_Man.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 202503191240;
+ CURRENT_PROJECT_VERSION = 202503191418;
DEVELOPMENT_ASSET_PATHS = "\"Gas Man/Preview Content\"";
DEVELOPMENT_TEAM = Z734T5CD6B;
ENABLE_HARDENED_RUNTIME = YES;
@@ -332,7 +332,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "Gas Man/Gas_Man.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 202503191240;
+ CURRENT_PROJECT_VERSION = 202503191418;
DEVELOPMENT_ASSET_PATHS = "\"Gas Man/Preview Content\"";
DEVELOPMENT_TEAM = Z734T5CD6B;
ENABLE_HARDENED_RUNTIME = YES;
diff --git a/Gas Man/FuelLogs/FuelLogImporterView.swift b/Gas Man/FuelLogs/FuelLogImporterView.swift
new file mode 100644
index 0000000..34c481a
--- /dev/null
+++ b/Gas Man/FuelLogs/FuelLogImporterView.swift
@@ -0,0 +1,168 @@
+import SwiftUI
+import UniformTypeIdentifiers
+import CoreData
+
+struct FuelLogImporterView: View {
+ @Environment(\.managedObjectContext) private var viewContext
+ @Environment(\.dismiss) var dismiss
+
+ // Pass in the selected vehicle from FuelLogListView.
+ var selectedVehicle: Vehicle?
+
+ @State private var isFileImporterPresented = false
+ @State private var importError: String? = nil
+
+ var body: some View {
+ VStack(spacing: 20) {
+ Text("Import Fuel Logs from CSV")
+ .font(.headline)
+ Button("Select CSV File") {
+ isFileImporterPresented = true
+ }
+ .padding()
+ .background(Color.blue)
+ .foregroundColor(.white)
+ .cornerRadius(8)
+
+ if let error = importError {
+ Text("Error: \(error)")
+ .foregroundColor(.red)
+ }
+ }
+ .fileImporter(
+ isPresented: $isFileImporterPresented,
+ allowedContentTypes: [UTType.commaSeparatedText],
+ allowsMultipleSelection: false
+ ) { result in
+ switch result {
+ case .success(let urls):
+ if let url = urls.first {
+ // Request access to the file URL.
+ guard url.startAccessingSecurityScopedResource() else {
+ importError = "Permission error: Unable to access the selected file."
+ return
+ }
+ defer { url.stopAccessingSecurityScopedResource() }
+ importCSV(from: url)
+ }
+ case .failure(let error):
+ importError = error.localizedDescription
+ }
+ }
+ .navigationTitle("Import Fuel Logs")
+ }
+
+ private func importCSV(from url: URL) {
+ do {
+ let data = try Data(contentsOf: url)
+ guard let csvString = String(data: data, encoding: .utf8) else {
+ importError = "Could not decode file."
+ return
+ }
+ // Assume the first line is a header.
+ let lines = csvString.components(separatedBy: "\n").filter { !$0.isEmpty }
+ guard lines.count > 1 else {
+ importError = "CSV file appears empty."
+ return
+ }
+ // For this example, assume the CSV columns are:
+ // date,odometer,fuelVolume,cost,locationCoordinates,locationName,octane,pricePerGalon,fullTank
+ let header = lines[0].components(separatedBy: ",")
+ for line in lines.dropFirst() {
+ let fields = line.components(separatedBy: ",")
+ if fields.count < header.count {
+ print("Skipping row: \(line) – not enough fields (found \(fields.count), expected \(header.count))")
+ continue
+ }
+
+ // Trim whitespace for each field
+ let dateStr = fields[0].trimmingCharacters(in: .whitespacesAndNewlines)
+ let odometerStr = fields[1].trimmingCharacters(in: .whitespacesAndNewlines)
+ let fuelVolumeStr = fields[2].trimmingCharacters(in: .whitespacesAndNewlines)
+ let costStr = fields[3].trimmingCharacters(in: .whitespacesAndNewlines)
+ let locationCoordinates = fields[4].trimmingCharacters(in: .whitespacesAndNewlines)
+ let locationName = fields[5].trimmingCharacters(in: .whitespacesAndNewlines)
+ let octaneStr = fields[6].trimmingCharacters(in: .whitespacesAndNewlines)
+ let pricePerGalonStr = fields[7].trimmingCharacters(in: .whitespacesAndNewlines)
+ let fullTankStr = fields[8].trimmingCharacters(in: .whitespacesAndNewlines)
+
+ // Debug prints for each field
+ print("Row: \(line)")
+ print("Date: \(dateStr), Odometer: \(odometerStr), Fuel Volume: \(fuelVolumeStr), Cost: \(costStr), Octane: \(octaneStr), Price/Gal: \(pricePerGalonStr), FullTank: \(fullTankStr)")
+
+ // Setup date formatter – adjust format as needed.
+ var parsedDate: Date?
+ let formats = ["M/d/yyyy", "yyyy-MM-dd HH:mm"]
+ for format in formats {
+ let formatter = DateFormatter()
+ formatter.dateFormat = format
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ if let date = formatter.date(from: dateStr) {
+ parsedDate = date
+ break
+ }
+ }
+ guard let date = parsedDate else {
+ print("Skipping row – invalid date: \(dateStr)")
+ continue
+ }
+
+ guard let odometer = Double(odometerStr) else {
+ print("Skipping row – invalid odometer: \(odometerStr)")
+ continue
+ }
+
+ guard let fuelVolume = Double(fuelVolumeStr) else {
+ print("Skipping row – invalid fuel volume: \(fuelVolumeStr)")
+ continue
+ }
+
+ guard let cost = Double(costStr) else {
+ print("Skipping row – invalid cost: \(costStr)")
+ continue
+ }
+
+ guard let octane = Int16(octaneStr) else {
+ print("Skipping row – invalid octane: \(octaneStr)")
+ continue
+ }
+
+ guard let pricePerGalon = Double(pricePerGalonStr) else {
+ print("Skipping row – invalid price per gallon: \(pricePerGalonStr)")
+ continue
+ }
+
+ // If all conversions succeed, create the fuel log.
+ let fullTank = (fullTankStr.lowercased() == "true" || fullTankStr == "1")
+
+ let newLog = FuelLog(context: viewContext)
+ newLog.id = UUID()
+ newLog.date = date
+ newLog.odometer = odometer
+ newLog.fuelVolume = fuelVolume
+ newLog.cost = cost
+ newLog.locationCoordinates = locationCoordinates
+ newLog.locationName = locationName
+ newLog.octane = octane
+ newLog.pricePerGalon = pricePerGalon
+ newLog.fullTank = fullTank
+
+ // Optionally assign vehicle if needed.
+ print("Assigning vehicle: \(selectedVehicle?.make ?? "none")")
+ newLog.vehicle = selectedVehicle
+
+ print("Imported row successfully.")
+ }
+ try viewContext.save()
+ dismiss()
+ } catch {
+ importError = error.localizedDescription
+ }
+ }
+}
+
+struct FuelLogImporterView_Previews: PreviewProvider {
+ static var previews: some View {
+ FuelLogImporterView(selectedVehicle: nil)
+ }
+}
diff --git a/Gas Man/FuelLogs/FuelLogListView.swift b/Gas Man/FuelLogs/FuelLogListView.swift
index 212c7c2..d907c70 100644
--- a/Gas Man/FuelLogs/FuelLogListView.swift
+++ b/Gas Man/FuelLogs/FuelLogListView.swift
@@ -24,6 +24,7 @@ struct FuelLogListView: View {
@State private var previousOdometer: Double? = nil
@State private var showOdometerAlert = false
@State private var isUpdatingCalculation = false
+ @State private var showingImporter = false
// Computed property for formatted previous odometer value
private var previousOdometerString: String {
@@ -154,10 +155,28 @@ struct FuelLogListView: View {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("Import CSV") {
+ showingImporter = true
+ }
+ }
}
.sheet(isPresented: $showingAddFuelLog) {
AddFuelLogView().environment(\.managedObjectContext, viewContext)
}
+ .sheet(isPresented: $showingImporter) {
+ // Retrieve the selected vehicle from the vehicles fetched results.
+ if let selectedVehicle = vehicles.first(where: { $0.id == selectedVehicleID }) {
+ FuelLogImporterView(selectedVehicle: selectedVehicle)
+ .environment(\.managedObjectContext, viewContext)
+ } else {
+ // Optionally, provide a fallback, e.g. use the first vehicle.
+ FuelLogImporterView(selectedVehicle: vehicles.first)
+ .environment(\.managedObjectContext, viewContext)
+ }
+// FuelLogImporterView()
+// .environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
+ }
.onAppear {
print("Total fuel logs: \(fuelLogs.count)")
setDefaultVehicle()
diff --git a/Gas Man/FuelLogs/FuelLogSummaryView.swift b/Gas Man/FuelLogs/FuelLogSummaryView.swift
index c1d2acc..441fe82 100644
--- a/Gas Man/FuelLogs/FuelLogSummaryView.swift
+++ b/Gas Man/FuelLogs/FuelLogSummaryView.swift
@@ -21,7 +21,7 @@ struct FuelLogSummaryView: View {
var distanceSincePrevious: Double?
var body: some View {
- VStack(alignment: .leading, spacing: 8) {
+ VStack(alignment: .leading, spacing: 2) {
// Row 1: Date and MPG (if available)
HStack {
Text(log.date ?? Date(), formatter: dateFormatter)
@@ -31,13 +31,18 @@ struct FuelLogSummaryView: View {
if let mpg = mpg {
Text("MPG: \(mpg, specifier: "%.3f")")
.font(.subheadline)
- .foregroundColor(.secondary)
+ .foregroundColor(.primary)
+ .bold()
}
}
+ .padding(2)
+ .background(Color.blue)
+ .cornerRadius(10)
+
Divider()
// Row 2: Distance (instead of odometer) and Fuel Volume
HStack {
- VStack(alignment: .leading) {
+ HStack() {
Text("Distance")
.font(.caption)
.foregroundColor(.secondary)
@@ -50,17 +55,17 @@ struct FuelLogSummaryView: View {
}
}
Spacer()
- VStack(alignment: .leading) {
- Text("Fuel")
+ HStack() {
+ Text("Fuel (Gal)")
.font(.caption)
.foregroundColor(.secondary)
- Text("\(log.fuelVolume, specifier: "%.3f") gal")
+ Text("\(log.fuelVolume, specifier: "%.3f")")
.bold()
}
}
// Row 3: Cost and Price per Gallon
HStack {
- VStack(alignment: .leading) {
+ HStack() {
Text("Cost")
.font(.caption)
.foregroundColor(.secondary)
@@ -68,7 +73,7 @@ struct FuelLogSummaryView: View {
.bold()
}
Spacer()
- VStack(alignment: .leading) {
+ HStack() {
Text("Price/Gal")
.font(.caption)
.foregroundColor(.secondary)
diff --git a/Gas Man/Gas_Man.entitlements b/Gas Man/Gas_Man.entitlements
index ba12f8a..2665a5d 100644
--- a/Gas Man/Gas_Man.entitlements
+++ b/Gas Man/Gas_Man.entitlements
@@ -11,9 +11,14 @@
com.apple.developer.icloud-services
CloudKit
+ CloudDocuments
+ com.apple.developer.ubiquity-container-identifiers
+
com.apple.security.app-sandbox
+ com.apple.security.assets.pictures.read-only
+
com.apple.security.files.user-selected.read-write
com.apple.security.personal-information.location
diff --git a/Gas Man/Stats/DistanceBetweenFillupsTrendChartView.swift b/Gas Man/Stats/DistanceBetweenFillupsTrendChartView.swift
index cb960b4..08bda83 100644
--- a/Gas Man/Stats/DistanceBetweenFillupsTrendChartView.swift
+++ b/Gas Man/Stats/DistanceBetweenFillupsTrendChartView.swift
@@ -48,6 +48,12 @@ struct DistanceBetweenFillupsTrendChartView: View {
x: .value("Date", point.date),
y: .value("Price", point.distance)
)
+ .annotation(position: .top) {
+ // This annotation displays the data point number and the MPG value.
+ Text("\(point.distance, specifier: "%.0f")")
+ .font(.caption2)
+ .foregroundColor(.blue)
+ }
}
}
.chartXAxis {
diff --git a/Gas Man/Stats/GalPerFuelUpTrendChartView.swift b/Gas Man/Stats/GalPerFuelUpTrendChartView.swift
index 0e52248..32c1c19 100644
--- a/Gas Man/Stats/GalPerFuelUpTrendChartView.swift
+++ b/Gas Man/Stats/GalPerFuelUpTrendChartView.swift
@@ -42,6 +42,12 @@ struct GalPerFuelUpTrendChartView: View {
x: .value("Date", point.date),
y: .value("Price", point.gal)
)
+ .annotation(position: .top) {
+ // This annotation displays the data point number and the MPG value.
+ Text("\(point.gal, specifier: "%.1f")")
+ .font(.caption2)
+ .foregroundColor(.blue)
+ }
}
}
.chartXAxis {
@@ -50,6 +56,7 @@ struct GalPerFuelUpTrendChartView: View {
.chartYAxis {
AxisMarks(values: .automatic(desiredCount: 5))
}
+
}
}
.padding()
diff --git a/Gas Man/Stats/MPGTrendChartView.swift b/Gas Man/Stats/MPGTrendChartView.swift
index b03d2ad..56c2f59 100644
--- a/Gas Man/Stats/MPGTrendChartView.swift
+++ b/Gas Man/Stats/MPGTrendChartView.swift
@@ -1,10 +1,3 @@
-//
-// MPGTrendChartView.swift
-// Gas Man
-//
-// Created by Kameron Kenny on 3/19/25.
-//
-
import SwiftUI
import Charts
import CoreData
@@ -38,7 +31,8 @@ struct MPGTrendChartView: View {
.foregroundColor(.secondary)
} else {
Chart {
- ForEach(trendData, id: \.date) { point in
+ // Enumerate the trend data to get an index for each data point.
+ ForEach(Array(trendData.enumerated()), id: \.element.date) { index, point in
LineMark(
x: .value("Date", point.date),
y: .value("MPG", point.mpg)
@@ -47,13 +41,19 @@ struct MPGTrendChartView: View {
x: .value("Date", point.date),
y: .value("MPG", point.mpg)
)
+ .annotation(position: .top) {
+ // This annotation displays the data point number and the MPG value.
+ Text("\(point.mpg, specifier: "%.1f")")
+ .font(.caption2)
+ .foregroundColor(.blue)
+ }
}
}
.chartXAxis {
- AxisMarks(values: .automatic(desiredCount: 4))
+ AxisMarks(values: .automatic(desiredCount: 6))
}
.chartYAxis {
- AxisMarks(values: .automatic(desiredCount: 5))
+ AxisMarks(values: .automatic(desiredCount: 8))
}
}
}
diff --git a/Gas Man/Stats/PricePerGallonTrendChartView.swift b/Gas Man/Stats/PricePerGallonTrendChartView.swift
index cb0155e..68db458 100644
--- a/Gas Man/Stats/PricePerGallonTrendChartView.swift
+++ b/Gas Man/Stats/PricePerGallonTrendChartView.swift
@@ -42,6 +42,12 @@ struct PricePerGallonTrendChartView: View {
x: .value("Date", point.date),
y: .value("Price", point.price)
)
+ .annotation(position: .top) {
+ // This annotation displays the data point number and the MPG value.
+ Text("\(point.price, specifier: "%.3f")")
+ .font(.caption2)
+ .foregroundColor(.blue)
+ }
}
}
.chartXAxis {