Implement importer, ui updates

This commit is contained in:
Kameron Kenny 2025-03-19 16:36:45 -04:00
parent e103168802
commit c3bb63ed62
9 changed files with 236 additions and 20 deletions

View File

@ -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;

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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)

View File

@ -11,9 +11,14 @@
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
<string>CloudDocuments</string>
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.assets.pictures.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.personal-information.location</key>

View File

@ -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 {

View File

@ -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()

View File

@ -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))
}
}
}

View File

@ -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 {