Implement importer, ui updates
This commit is contained in:
parent
e103168802
commit
c3bb63ed62
|
@ -289,7 +289,7 @@
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "Gas Man/Gas_Man.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Gas Man/Gas_Man.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 202503191240;
|
CURRENT_PROJECT_VERSION = 202503191418;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Gas Man/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Gas Man/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = Z734T5CD6B;
|
DEVELOPMENT_TEAM = Z734T5CD6B;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
|
@ -332,7 +332,7 @@
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "Gas Man/Gas_Man.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Gas Man/Gas_Man.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 202503191240;
|
CURRENT_PROJECT_VERSION = 202503191418;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Gas Man/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Gas Man/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = Z734T5CD6B;
|
DEVELOPMENT_TEAM = Z734T5CD6B;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
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 previousOdometer: Double? = nil
|
||||||
@State private var showOdometerAlert = false
|
@State private var showOdometerAlert = false
|
||||||
@State private var isUpdatingCalculation = false
|
@State private var isUpdatingCalculation = false
|
||||||
|
@State private var showingImporter = false
|
||||||
|
|
||||||
// Computed property for formatted previous odometer value
|
// Computed property for formatted previous odometer value
|
||||||
private var previousOdometerString: String {
|
private var previousOdometerString: String {
|
||||||
|
@ -154,10 +155,28 @@ struct FuelLogListView: View {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
EditButton()
|
EditButton()
|
||||||
}
|
}
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Import CSV") {
|
||||||
|
showingImporter = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingAddFuelLog) {
|
.sheet(isPresented: $showingAddFuelLog) {
|
||||||
AddFuelLogView().environment(\.managedObjectContext, viewContext)
|
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 {
|
.onAppear {
|
||||||
print("Total fuel logs: \(fuelLogs.count)")
|
print("Total fuel logs: \(fuelLogs.count)")
|
||||||
setDefaultVehicle()
|
setDefaultVehicle()
|
||||||
|
|
|
@ -21,7 +21,7 @@ struct FuelLogSummaryView: View {
|
||||||
var distanceSincePrevious: Double?
|
var distanceSincePrevious: Double?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
// Row 1: Date and MPG (if available)
|
// Row 1: Date and MPG (if available)
|
||||||
HStack {
|
HStack {
|
||||||
Text(log.date ?? Date(), formatter: dateFormatter)
|
Text(log.date ?? Date(), formatter: dateFormatter)
|
||||||
|
@ -31,13 +31,18 @@ struct FuelLogSummaryView: View {
|
||||||
if let mpg = mpg {
|
if let mpg = mpg {
|
||||||
Text("MPG: \(mpg, specifier: "%.3f")")
|
Text("MPG: \(mpg, specifier: "%.3f")")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.primary)
|
||||||
|
.bold()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.padding(2)
|
||||||
|
.background(Color.blue)
|
||||||
|
.cornerRadius(10)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
// Row 2: Distance (instead of odometer) and Fuel Volume
|
// Row 2: Distance (instead of odometer) and Fuel Volume
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
HStack() {
|
||||||
Text("Distance")
|
Text("Distance")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
@ -50,17 +55,17 @@ struct FuelLogSummaryView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
VStack(alignment: .leading) {
|
HStack() {
|
||||||
Text("Fuel")
|
Text("Fuel (Gal)")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Text("\(log.fuelVolume, specifier: "%.3f") gal")
|
Text("\(log.fuelVolume, specifier: "%.3f")")
|
||||||
.bold()
|
.bold()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Row 3: Cost and Price per Gallon
|
// Row 3: Cost and Price per Gallon
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
HStack() {
|
||||||
Text("Cost")
|
Text("Cost")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
@ -68,7 +73,7 @@ struct FuelLogSummaryView: View {
|
||||||
.bold()
|
.bold()
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
VStack(alignment: .leading) {
|
HStack() {
|
||||||
Text("Price/Gal")
|
Text("Price/Gal")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
|
@ -11,9 +11,14 @@
|
||||||
<key>com.apple.developer.icloud-services</key>
|
<key>com.apple.developer.icloud-services</key>
|
||||||
<array>
|
<array>
|
||||||
<string>CloudKit</string>
|
<string>CloudKit</string>
|
||||||
|
<string>CloudDocuments</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
||||||
|
<array/>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.assets.pictures.read-only</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.personal-information.location</key>
|
<key>com.apple.security.personal-information.location</key>
|
||||||
|
|
|
@ -48,6 +48,12 @@ struct DistanceBetweenFillupsTrendChartView: View {
|
||||||
x: .value("Date", point.date),
|
x: .value("Date", point.date),
|
||||||
y: .value("Price", point.distance)
|
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 {
|
.chartXAxis {
|
||||||
|
|
|
@ -42,6 +42,12 @@ struct GalPerFuelUpTrendChartView: View {
|
||||||
x: .value("Date", point.date),
|
x: .value("Date", point.date),
|
||||||
y: .value("Price", point.gal)
|
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 {
|
.chartXAxis {
|
||||||
|
@ -50,6 +56,7 @@ struct GalPerFuelUpTrendChartView: View {
|
||||||
.chartYAxis {
|
.chartYAxis {
|
||||||
AxisMarks(values: .automatic(desiredCount: 5))
|
AxisMarks(values: .automatic(desiredCount: 5))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
|
|
|
@ -1,10 +1,3 @@
|
||||||
//
|
|
||||||
// MPGTrendChartView.swift
|
|
||||||
// Gas Man
|
|
||||||
//
|
|
||||||
// Created by Kameron Kenny on 3/19/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Charts
|
import Charts
|
||||||
import CoreData
|
import CoreData
|
||||||
|
@ -38,7 +31,8 @@ struct MPGTrendChartView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
} else {
|
} else {
|
||||||
Chart {
|
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(
|
LineMark(
|
||||||
x: .value("Date", point.date),
|
x: .value("Date", point.date),
|
||||||
y: .value("MPG", point.mpg)
|
y: .value("MPG", point.mpg)
|
||||||
|
@ -47,13 +41,19 @@ struct MPGTrendChartView: View {
|
||||||
x: .value("Date", point.date),
|
x: .value("Date", point.date),
|
||||||
y: .value("MPG", point.mpg)
|
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 {
|
.chartXAxis {
|
||||||
AxisMarks(values: .automatic(desiredCount: 4))
|
AxisMarks(values: .automatic(desiredCount: 6))
|
||||||
}
|
}
|
||||||
.chartYAxis {
|
.chartYAxis {
|
||||||
AxisMarks(values: .automatic(desiredCount: 5))
|
AxisMarks(values: .automatic(desiredCount: 8))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,12 @@ struct PricePerGallonTrendChartView: View {
|
||||||
x: .value("Date", point.date),
|
x: .value("Date", point.date),
|
||||||
y: .value("Price", point.price)
|
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 {
|
.chartXAxis {
|
||||||
|
|
Loading…
Reference in New Issue