|
|
@@ -6,12 +6,16 @@
|
|
|
//
|
|
|
import Charts
|
|
|
import SwiftUI
|
|
|
+import UIKit
|
|
|
|
|
|
/// Glucose target step view for setting target glucose range.
|
|
|
struct GlucoseTargetStepView: View {
|
|
|
@State var onboardingData: OnboardingData
|
|
|
@State private var showUnitPicker = false
|
|
|
@State private var showTimeSelector = false
|
|
|
+ @State private var selectedTargetIndex: Int?
|
|
|
+ @State private var showAlert = false
|
|
|
+ @State private var errorMessage = ""
|
|
|
@State private var refreshUI = UUID() // to update chart when slider value changes
|
|
|
|
|
|
// Formatter for glucose values
|
|
|
@@ -56,29 +60,37 @@ struct GlucoseTargetStepView: View {
|
|
|
.cornerRadius(8)
|
|
|
}
|
|
|
.actionSheet(isPresented: $showUnitPicker) {
|
|
|
- ActionSheet(
|
|
|
+ let mgdlAction = ActionSheet.Button.default(Text("mg/dL")) {
|
|
|
+ // Store current unit
|
|
|
+ let oldUnit = onboardingData.units
|
|
|
+ // Change to new unit
|
|
|
+ onboardingData.units = .mgdL
|
|
|
+ // Adjust values for unit change, only if unit actually changed
|
|
|
+ if oldUnit != .mgdL {
|
|
|
+ onboardingData.targetLow = max(70, onboardingData.targetLow * 18)
|
|
|
+ onboardingData.targetHigh = max(120, onboardingData.targetHigh * 18)
|
|
|
+ onboardingData.isf = max(30, onboardingData.isf * 18)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let mmolAction = ActionSheet.Button.default(Text("mmol/L")) {
|
|
|
+ // Store current unit
|
|
|
+ let oldUnit = onboardingData.units
|
|
|
+ // Change to new unit
|
|
|
+ onboardingData.units = .mmolL
|
|
|
+ // Adjust values for unit change, only if unit actually changed
|
|
|
+ if oldUnit != .mmolL {
|
|
|
+ onboardingData.targetLow = max(3.9, onboardingData.targetLow / 18)
|
|
|
+ onboardingData.targetHigh = max(6.7, onboardingData.targetHigh / 18)
|
|
|
+ onboardingData.isf = max(1.7, onboardingData.isf / 18)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let cancelAction = ActionSheet.Button.cancel()
|
|
|
+
|
|
|
+ return ActionSheet(
|
|
|
title: Text("Select Blood Glucose Units"),
|
|
|
- buttons: [
|
|
|
- .default(Text("mg/dL")) {
|
|
|
- onboardingData.units = .mgdL
|
|
|
- // Adjust values for unit change
|
|
|
- if onboardingData.units == .mgdL {
|
|
|
- onboardingData.targetLow = max(70, onboardingData.targetLow * 18)
|
|
|
- onboardingData.targetHigh = max(120, onboardingData.targetHigh * 18)
|
|
|
- onboardingData.isf = max(30, onboardingData.isf * 18)
|
|
|
- }
|
|
|
- },
|
|
|
- .default(Text("mmol/L")) {
|
|
|
- onboardingData.units = .mmolL
|
|
|
- // Adjust values for unit change
|
|
|
- if onboardingData.units == .mmolL {
|
|
|
- onboardingData.targetLow = max(3.9, onboardingData.targetLow / 18)
|
|
|
- onboardingData.targetHigh = max(6.7, onboardingData.targetHigh / 18)
|
|
|
- onboardingData.isf = max(1.7, onboardingData.isf / 18)
|
|
|
- }
|
|
|
- },
|
|
|
- .cancel()
|
|
|
- ]
|
|
|
+ buttons: [mgdlAction, mmolAction, cancelAction]
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
@@ -159,7 +171,7 @@ struct GlucoseTargetStepView: View {
|
|
|
.padding(.horizontal)
|
|
|
}
|
|
|
.padding(.vertical, 5)
|
|
|
- .background(Color.orange.opacity(0.05))
|
|
|
+ .background(Color.blue.opacity(0.05))
|
|
|
.cornerRadius(10)
|
|
|
}
|
|
|
|
|
|
@@ -178,87 +190,94 @@ struct GlucoseTargetStepView: View {
|
|
|
}) {
|
|
|
HStack {
|
|
|
Image(systemName: "plus.circle.fill")
|
|
|
- Text("Add Ratio")
|
|
|
+ Text("Add Target")
|
|
|
}
|
|
|
- .foregroundColor(.orange)
|
|
|
+ .foregroundColor(.blue)
|
|
|
}
|
|
|
- .disabled(!canAddRatio)
|
|
|
+ .disabled(!canAddTarget)
|
|
|
}
|
|
|
}
|
|
|
.padding(.horizontal)
|
|
|
|
|
|
// List of targets
|
|
|
VStack(spacing: 2) {
|
|
|
-// ForEach(Array(onboardingData.targetItems.enumerated()), id: \.element.id) { index, item in
|
|
|
-// HStack {
|
|
|
-// // Time display
|
|
|
-// Text(
|
|
|
-// dateFormatter
|
|
|
-// .string(from: Date(
|
|
|
-// timeIntervalSince1970: onboardingData
|
|
|
-// .targetTimeValues[item.lowIndex]
|
|
|
-// ))
|
|
|
-// )
|
|
|
-// .frame(width: 80, alignment: .leading)
|
|
|
-// .padding(.leading)
|
|
|
-//
|
|
|
-// // Ratio slider
|
|
|
-// Slider(
|
|
|
-// value: Binding(
|
|
|
-// get: {
|
|
|
-// Double(
|
|
|
-// truncating: onboardingData
|
|
|
-// .targetRateValues[item.lowIndex] as NSNumber
|
|
|
-// ) },
|
|
|
-// set: { newValue in
|
|
|
-// // Find closest match in rateValues array
|
|
|
-// let newIndex = onboardingData.targetRateValues
|
|
|
-// .firstIndex { abs(Double($0) - newValue) < 0.05 } ?? item.lowIndex
|
|
|
-// onboardingData.targetItems[index].lowIndex = newIndex
|
|
|
-// // Force refresh when slider changes
|
|
|
-// refreshUI = UUID()
|
|
|
-// }
|
|
|
-// ),
|
|
|
-// in: Double(truncating: onboardingData.targetRateValues.first! as NSNumber) ...
|
|
|
-// Double(truncating: onboardingData.targetRateValues.last! as NSNumber),
|
|
|
-// step: 0.5
|
|
|
-// )
|
|
|
-// .accentColor(.orange)
|
|
|
-// .padding(.horizontal, 5)
|
|
|
-// .onChange(of: onboardingData.targetItems[index].lowIndex) { _, _ in
|
|
|
-// let impact = UIImpactFeedbackGenerator(style: .light)
|
|
|
-// impact.impactOccurred()
|
|
|
-// }
|
|
|
-//
|
|
|
-// // Display the current value
|
|
|
-// Text(
|
|
|
-// "\(Formatters.decimalFormatterWithOneFractionDigit.string(from: onboardingData.targetRateValues[item.lowIndex] as NSNumber) ?? "--") g/U"
|
|
|
-// )
|
|
|
-// .frame(width: 80, alignment: .trailing)
|
|
|
-// .lineLimit(1)
|
|
|
-// .minimumScaleFactor(0.8)
|
|
|
-//
|
|
|
-// // Delete button (not for the first entry at 00:00)
|
|
|
-// if index > 0 {
|
|
|
-// Button(action: {
|
|
|
-// onboardingData.targetItems.remove(at: index)
|
|
|
-// }) {
|
|
|
-// Image(systemName: "trash")
|
|
|
-// .foregroundColor(.red)
|
|
|
-// .padding(.horizontal, 5)
|
|
|
-// }
|
|
|
-// } else {
|
|
|
-// // Spacer to maintain alignment
|
|
|
-// Spacer()
|
|
|
-// .frame(width: 30)
|
|
|
-// }
|
|
|
-// }
|
|
|
-// .padding(.vertical, 12)
|
|
|
-// .background(index % 2 == 0 ? Color.orange.opacity(0.05) : Color.clear)
|
|
|
-// .cornerRadius(8)
|
|
|
-// }
|
|
|
+ ForEach(onboardingData.targetItems.indices, id: \.self) { index in
|
|
|
+ let item = onboardingData.targetItems[index]
|
|
|
+ HStack {
|
|
|
+ // Time display
|
|
|
+ Text(
|
|
|
+ dateFormatter
|
|
|
+ .string(from: Date(
|
|
|
+ timeIntervalSince1970: onboardingData
|
|
|
+ .targetTimeValues[item.timeIndex]
|
|
|
+ ))
|
|
|
+ )
|
|
|
+ .frame(width: 80, alignment: .leading)
|
|
|
+ .padding(.leading)
|
|
|
+
|
|
|
+ // Low target slider
|
|
|
+ Slider(
|
|
|
+ value: Binding(
|
|
|
+ get: {
|
|
|
+ Double(
|
|
|
+ truncating: onboardingData
|
|
|
+ .targetRateValues[item.lowIndex] as NSNumber
|
|
|
+ ) },
|
|
|
+ set: { newValue in
|
|
|
+ // Find closest match in rateValues array
|
|
|
+ let newIndex = onboardingData.targetRateValues
|
|
|
+ .firstIndex { abs(Double($0) - newValue) < 0.05 } ?? item.lowIndex
|
|
|
+ onboardingData.targetItems[index].lowIndex = newIndex
|
|
|
+
|
|
|
+ // Ensure high target is at least as high as low target
|
|
|
+ if onboardingData.targetItems[index].highIndex < newIndex {
|
|
|
+ onboardingData.targetItems[index].highIndex = newIndex
|
|
|
+ }
|
|
|
+
|
|
|
+ // Force refresh when slider changes
|
|
|
+ refreshUI = UUID()
|
|
|
+ }
|
|
|
+ ),
|
|
|
+ in: Double(truncating: onboardingData.targetRateValues.first! as NSNumber) ...
|
|
|
+ Double(truncating: onboardingData.targetRateValues.last! as NSNumber),
|
|
|
+ step: onboardingData.units == .mgdL ? 1 : 0.1
|
|
|
+ )
|
|
|
+ .accentColor(.blue)
|
|
|
+ .padding(.horizontal, 5)
|
|
|
+ .onChange(of: onboardingData.targetItems[index].lowIndex) { _, _ in
|
|
|
+ let impact = UIImpactFeedbackGenerator(style: .light)
|
|
|
+ impact.impactOccurred()
|
|
|
+ }
|
|
|
+
|
|
|
+ // Display the current value
|
|
|
+ Text(
|
|
|
+ "\(numberFormatter.string(from: onboardingData.targetRateValues[item.lowIndex] as NSNumber) ?? "--") \(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")"
|
|
|
+ )
|
|
|
+ .frame(width: 80, alignment: .trailing)
|
|
|
+ .lineLimit(1)
|
|
|
+ .minimumScaleFactor(0.8)
|
|
|
+
|
|
|
+ // Delete button (not for the first entry at 00:00)
|
|
|
+ if index > 0 {
|
|
|
+ Button(action: {
|
|
|
+ onboardingData.targetItems.remove(at: index)
|
|
|
+ }) {
|
|
|
+ Image(systemName: "trash")
|
|
|
+ .foregroundColor(.red)
|
|
|
+ .padding(.horizontal, 5)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Spacer to maintain alignment
|
|
|
+ Spacer()
|
|
|
+ .frame(width: 30)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .padding(.vertical, 12)
|
|
|
+ .background(index % 2 == 0 ? Color.blue.opacity(0.05) : Color.clear)
|
|
|
+ .cornerRadius(8)
|
|
|
+ }
|
|
|
}
|
|
|
- .background(Color.orange.opacity(0.05))
|
|
|
+ .background(Color.blue.opacity(0.05))
|
|
|
.cornerRadius(10)
|
|
|
.padding(.horizontal)
|
|
|
.onAppear {
|
|
|
@@ -334,19 +353,22 @@ struct GlucoseTargetStepView: View {
|
|
|
// Find available time slots in 1-hour increments
|
|
|
for hour in 0 ..< 24 {
|
|
|
let hourInMinutes = hour * 60
|
|
|
- // Calculate lowIndex for this hour
|
|
|
+ // Calculate timeIndex for this hour
|
|
|
let timeIndex = onboardingData.targetTimeValues.firstIndex { abs($0 - Double(hourInMinutes * 60)) < 10 } ?? 0
|
|
|
|
|
|
// Check if this hour is already in the profile
|
|
|
if !onboardingData.targetItems.contains(where: { $0.timeIndex == timeIndex }) {
|
|
|
buttons.append(.default(Text("\(String(format: "%02d:00", hour))")) {
|
|
|
- // Get the current ratio from the last item
|
|
|
+ // Get the current low and high values from the last item
|
|
|
let lowIndex = onboardingData.targetItems.last?.lowIndex ?? 0
|
|
|
+ let highIndex = onboardingData.targetItems.last?.highIndex ?? lowIndex
|
|
|
+
|
|
|
// Create new item with the specified time
|
|
|
- let newItem = TargetsEditor.Item(lowIndex: lowIndex, highIndex: lowIndex, timeIndex: timeIndex)
|
|
|
- // Add the new item and sort the list
|
|
|
+ let newItem = TargetsEditor.Item(lowIndex: lowIndex, highIndex: highIndex, timeIndex: timeIndex)
|
|
|
+
|
|
|
+ // Add the new item and sort the list by timeIndex
|
|
|
onboardingData.targetItems.append(newItem)
|
|
|
- onboardingData.targetItems.sort(by: { $0.lowIndex < $1.lowIndex })
|
|
|
+ onboardingData.targetItems.sort(by: { $0.timeIndex < $1.timeIndex })
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
@@ -355,58 +377,95 @@ struct GlucoseTargetStepView: View {
|
|
|
|
|
|
return ActionSheet(
|
|
|
title: Text("Select Start Time"),
|
|
|
- message: Text("Choose when this carb ratio should start"),
|
|
|
+ message: Text("Choose when this target should start"),
|
|
|
buttons: buttons
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Computed property to check if we can add more carb ratios
|
|
|
- private var canAddRatio: Bool {
|
|
|
+ // Computed property to check if we can add more targets
|
|
|
+ private var canAddTarget: Bool {
|
|
|
guard let lastItem = onboardingData.targetItems.last else { return true }
|
|
|
- return lastItem.lowIndex < onboardingData.targetTimeValues.count - 1
|
|
|
+ return lastItem.timeIndex < onboardingData.targetTimeValues.count - 1
|
|
|
}
|
|
|
|
|
|
- // Chart for visualizing carb ratios
|
|
|
+ // Chart for visualizing glucose targets
|
|
|
private var glucoseTargetChart: some View {
|
|
|
Chart {
|
|
|
- ForEach(Array(onboardingData.targetItems.enumerated()), id: \.element.id) { index, item in
|
|
|
- let displayValue = onboardingData.targetRateValues[item.lowIndex]
|
|
|
-
|
|
|
- let tzOffset = TimeZone.current.secondsFromGMT() * -1
|
|
|
- let startDate = Date(timeIntervalSinceReferenceDate: onboardingData.targetTimeValues[item.lowIndex])
|
|
|
- .addingTimeInterval(TimeInterval(tzOffset))
|
|
|
- let endDate = onboardingData.targetItems.count > index + 1 ?
|
|
|
- Date(
|
|
|
- timeIntervalSinceReferenceDate: onboardingData
|
|
|
- .targetTimeValues[onboardingData.targetItems[index + 1].lowIndex]
|
|
|
- )
|
|
|
- .addingTimeInterval(TimeInterval(tzOffset)) :
|
|
|
- Date(timeIntervalSinceReferenceDate: onboardingData.targetTimeValues.last!).addingTimeInterval(30 * 60)
|
|
|
- .addingTimeInterval(TimeInterval(tzOffset))
|
|
|
-
|
|
|
- RectangleMark(
|
|
|
- xStart: .value("start", startDate),
|
|
|
- xEnd: .value("end", endDate),
|
|
|
- yStart: .value("rate-start", displayValue),
|
|
|
- yEnd: .value("rate-end", 0)
|
|
|
- ).foregroundStyle(
|
|
|
- .linearGradient(
|
|
|
- colors: [
|
|
|
- Color.orange.opacity(0.6),
|
|
|
- Color.orange.opacity(0.1)
|
|
|
- ],
|
|
|
- startPoint: .bottom,
|
|
|
- endPoint: .top
|
|
|
- )
|
|
|
- ).alignsMarkStylesWithPlotArea()
|
|
|
-
|
|
|
- LineMark(x: .value("End Date", startDate), y: .value("Ratio", displayValue))
|
|
|
- .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
|
|
|
-
|
|
|
- LineMark(x: .value("Start Date", endDate), y: .value("Ratio", displayValue))
|
|
|
- .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.orange)
|
|
|
- }
|
|
|
+// ForEach(onboardingData.targetItems.indices, id: \.self) { index in
|
|
|
+// let item = onboardingData.targetItems[index]
|
|
|
+// let lowValue = onboardingData.targetRateValues[item.lowIndex]
|
|
|
+// let highValue = onboardingData.targetRateValues[item.highIndex]
|
|
|
+//
|
|
|
+// let tzOffset = TimeZone.current.secondsFromGMT() * -1
|
|
|
+// let startDate = Date(timeIntervalSinceReferenceDate: onboardingData.targetTimeValues[item.timeIndex])
|
|
|
+// .addingTimeInterval(TimeInterval(tzOffset))
|
|
|
+//
|
|
|
+// // Calculate end date (next target or end of day)
|
|
|
+// let endDate: Date
|
|
|
+// if index < onboardingData.targetItems.count - 1 {
|
|
|
+// let nextItem = onboardingData.targetItems[index + 1]
|
|
|
+// endDate = Date(timeIntervalSinceReferenceDate: onboardingData.targetTimeValues[nextItem.timeIndex])
|
|
|
+// .addingTimeInterval(TimeInterval(tzOffset))
|
|
|
+// } else {
|
|
|
+// endDate = Date(timeIntervalSinceReferenceDate: onboardingData.targetTimeValues.last!)
|
|
|
+// .addingTimeInterval(30 * 60)
|
|
|
+// .addingTimeInterval(TimeInterval(tzOffset))
|
|
|
+// }
|
|
|
+//
|
|
|
+// // Low target line
|
|
|
+// LineMark(
|
|
|
+// x: .value("Start", startDate),
|
|
|
+// y: .value("Low Target", lowValue)
|
|
|
+// )
|
|
|
+// .foregroundStyle(Color.blue)
|
|
|
+// .lineStyle(StrokeStyle(lineWidth: 2))
|
|
|
+//
|
|
|
+// LineMark(
|
|
|
+// x: .value("End", endDate),
|
|
|
+// y: .value("Low Target", lowValue)
|
|
|
+// )
|
|
|
+// .foregroundStyle(Color.blue)
|
|
|
+// .lineStyle(StrokeStyle(lineWidth: 2))
|
|
|
+//
|
|
|
+// // High target line
|
|
|
+// LineMark(
|
|
|
+// x: .value("Start", startDate),
|
|
|
+// y: .value("High Target", highValue)
|
|
|
+// )
|
|
|
+// .foregroundStyle(Color.blue.opacity(0.6))
|
|
|
+// .lineStyle(StrokeStyle(lineWidth: 2))
|
|
|
+//
|
|
|
+// LineMark(
|
|
|
+// x: .value("End", endDate),
|
|
|
+// y: .value("High Target", highValue)
|
|
|
+// )
|
|
|
+// .foregroundStyle(Color.blue.opacity(0.6))
|
|
|
+// .lineStyle(StrokeStyle(lineWidth: 2))
|
|
|
+//
|
|
|
+// // Target range area
|
|
|
+// AreaMark(
|
|
|
+// x: .value("Start", startDate),
|
|
|
+// yStart: .value("Low Target", lowValue),
|
|
|
+// yEnd: .value("High Target", highValue)
|
|
|
+// )
|
|
|
+// .foregroundStyle(.linearGradient(
|
|
|
+// colors: [Color.blue.opacity(0.3), Color.blue.opacity(0.1)],
|
|
|
+// startPoint: .bottom,
|
|
|
+// endPoint: .top
|
|
|
+// ))
|
|
|
+//
|
|
|
+// AreaMark(
|
|
|
+// x: .value("End", endDate),
|
|
|
+// yStart: .value("Low Target", lowValue),
|
|
|
+// yEnd: .value("High Target", highValue)
|
|
|
+// )
|
|
|
+// .foregroundStyle(.linearGradient(
|
|
|
+// colors: [Color.blue.opacity(0.3), Color.blue.opacity(0.1)],
|
|
|
+// startPoint: .bottom,
|
|
|
+// endPoint: .top
|
|
|
+// ))
|
|
|
+// }
|
|
|
}
|
|
|
.id(refreshUI) // Force chart update
|
|
|
.chartXAxis {
|