Implement importer, ui updates
This commit is contained in:
parent
e103168802
commit
c3bb63ed62
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue