|
@@ -4,12 +4,15 @@
|
|
|
//
|
|
//
|
|
|
// Created by Marvin Polscheit on 19.03.25.
|
|
// Created by Marvin Polscheit on 19.03.25.
|
|
|
//
|
|
//
|
|
|
|
|
+import Charts
|
|
|
import SwiftUI
|
|
import SwiftUI
|
|
|
|
|
|
|
|
/// Glucose target step view for setting target glucose range.
|
|
/// Glucose target step view for setting target glucose range.
|
|
|
struct GlucoseTargetStepView: View {
|
|
struct GlucoseTargetStepView: View {
|
|
|
@State var onboardingData: OnboardingData
|
|
@State var onboardingData: OnboardingData
|
|
|
@State private var showUnitPicker = false
|
|
@State private var showUnitPicker = false
|
|
|
|
|
+ @State private var showTimeSelector = false
|
|
|
|
|
+ @State private var refreshUI = UUID() // to update chart when slider value changes
|
|
|
|
|
|
|
|
// Formatter for glucose values
|
|
// Formatter for glucose values
|
|
|
private var numberFormatter: NumberFormatter {
|
|
private var numberFormatter: NumberFormatter {
|
|
@@ -19,177 +22,408 @@ struct GlucoseTargetStepView: View {
|
|
|
return formatter
|
|
return formatter
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private var dateFormatter: DateFormatter {
|
|
|
|
|
+ let formatter = DateFormatter()
|
|
|
|
|
+ formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
|
|
|
|
+ formatter.timeStyle = .short
|
|
|
|
|
+ return formatter
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // For chart scaling
|
|
|
|
|
+ private let chartScale = Calendar.current
|
|
|
|
|
+ .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
|
|
|
|
|
+
|
|
|
var body: some View {
|
|
var body: some View {
|
|
|
- VStack(alignment: .leading, spacing: 20) {
|
|
|
|
|
- // Unit selector
|
|
|
|
|
- HStack {
|
|
|
|
|
- Text("Blood Glucose Units")
|
|
|
|
|
- .font(.headline)
|
|
|
|
|
|
|
+ ScrollView {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 20) {
|
|
|
|
|
+ // Unit selector
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text("Blood Glucose Units")
|
|
|
|
|
+ .font(.headline)
|
|
|
|
|
|
|
|
- Spacer()
|
|
|
|
|
|
|
+ Spacer()
|
|
|
|
|
|
|
|
- Button(action: {
|
|
|
|
|
- showUnitPicker.toggle()
|
|
|
|
|
- }) {
|
|
|
|
|
- HStack {
|
|
|
|
|
- Text(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")
|
|
|
|
|
- Image(systemName: "chevron.down")
|
|
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ showUnitPicker.toggle()
|
|
|
|
|
+ }) {
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")
|
|
|
|
|
+ Image(systemName: "chevron.down")
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(.horizontal, 12)
|
|
|
|
|
+ .padding(.vertical, 8)
|
|
|
|
|
+ .background(Color.blue.opacity(0.1))
|
|
|
|
|
+ .cornerRadius(8)
|
|
|
|
|
+ }
|
|
|
|
|
+ .actionSheet(isPresented: $showUnitPicker) {
|
|
|
|
|
+ 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()
|
|
|
|
|
+ ]
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
- .padding(.horizontal, 12)
|
|
|
|
|
- .padding(.vertical, 8)
|
|
|
|
|
- .background(Color.blue.opacity(0.1))
|
|
|
|
|
- .cornerRadius(8)
|
|
|
|
|
- }
|
|
|
|
|
- .actionSheet(isPresented: $showUnitPicker) {
|
|
|
|
|
- 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()
|
|
|
|
|
- ]
|
|
|
|
|
- )
|
|
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- Divider()
|
|
|
|
|
|
|
|
|
|
- // Target glucose range
|
|
|
|
|
- VStack(alignment: .leading, spacing: 12) {
|
|
|
|
|
- Text("Target Glucose Range")
|
|
|
|
|
- .font(.headline)
|
|
|
|
|
|
|
+ Divider()
|
|
|
|
|
|
|
|
- Text("This range defines your ideal blood glucose values. Trio uses this to calculate insulin doses.")
|
|
|
|
|
- .font(.subheadline)
|
|
|
|
|
- .foregroundColor(.secondary)
|
|
|
|
|
|
|
+ // Target glucose range
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 12) {
|
|
|
|
|
+ Text("Target Glucose Range")
|
|
|
|
|
+ .font(.headline)
|
|
|
|
|
|
|
|
- // Low target
|
|
|
|
|
- VStack(alignment: .leading) {
|
|
|
|
|
- Text("Low Target")
|
|
|
|
|
|
|
+ Text("This range defines your ideal blood glucose values. Trio uses this to calculate insulin doses.")
|
|
|
.font(.subheadline)
|
|
.font(.subheadline)
|
|
|
|
|
+ .foregroundColor(.secondary)
|
|
|
|
|
|
|
|
- HStack {
|
|
|
|
|
- Slider(
|
|
|
|
|
- value: Binding(
|
|
|
|
|
- get: { Double(truncating: onboardingData.targetLow as NSNumber) },
|
|
|
|
|
- set: { onboardingData.targetLow = Decimal($0) }
|
|
|
|
|
- ),
|
|
|
|
|
- in: onboardingData.units == .mgdL ? 70 ... 120 : 3.9 ... 6.7,
|
|
|
|
|
- step: onboardingData.units == .mgdL ? 1 : 0.1
|
|
|
|
|
- )
|
|
|
|
|
- .accentColor(.green)
|
|
|
|
|
|
|
+ // Low target
|
|
|
|
|
+ VStack(alignment: .leading) {
|
|
|
|
|
+ Text("Low Target")
|
|
|
|
|
+ .font(.subheadline)
|
|
|
|
|
|
|
|
- Text(
|
|
|
|
|
- "\(numberFormatter.string(from: onboardingData.targetLow as NSNumber) ?? "--") \(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")"
|
|
|
|
|
- )
|
|
|
|
|
- .frame(width: 80, alignment: .trailing)
|
|
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Slider(
|
|
|
|
|
+ value: Binding(
|
|
|
|
|
+ get: { Double(truncating: onboardingData.targetLow as NSNumber) },
|
|
|
|
|
+ set: { onboardingData.targetLow = Decimal($0) }
|
|
|
|
|
+ ),
|
|
|
|
|
+ in: onboardingData.units == .mgdL ? 70 ... 120 : 3.9 ... 6.7,
|
|
|
|
|
+ step: onboardingData.units == .mgdL ? 1 : 0.1
|
|
|
|
|
+ )
|
|
|
|
|
+ .accentColor(.green)
|
|
|
|
|
+
|
|
|
|
|
+ Text(
|
|
|
|
|
+ "\(numberFormatter.string(from: onboardingData.targetLow as NSNumber) ?? "--") \(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")"
|
|
|
|
|
+ )
|
|
|
|
|
+ .frame(width: 80, alignment: .trailing)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ .padding(.vertical, 4)
|
|
|
|
|
+
|
|
|
|
|
+ // High target
|
|
|
|
|
+ VStack(alignment: .leading) {
|
|
|
|
|
+ Text("High Target")
|
|
|
|
|
+ .font(.subheadline)
|
|
|
|
|
+
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Slider(
|
|
|
|
|
+ value: Binding(
|
|
|
|
|
+ get: { Double(truncating: onboardingData.targetHigh as NSNumber) },
|
|
|
|
|
+ set: { onboardingData.targetHigh = Decimal($0) }
|
|
|
|
|
+ ),
|
|
|
|
|
+ in: onboardingData.units == .mgdL ?
|
|
|
|
|
+ Double(truncating: onboardingData.targetLow as NSNumber) + 10 ... 200 :
|
|
|
|
|
+ Double(truncating: onboardingData.targetLow as NSNumber) + 0.6 ... 11.1,
|
|
|
|
|
+ step: onboardingData.units == .mgdL ? 1 : 0.1
|
|
|
|
|
+ )
|
|
|
|
|
+ .accentColor(.green)
|
|
|
|
|
+
|
|
|
|
|
+ Text(
|
|
|
|
|
+ "\(numberFormatter.string(from: onboardingData.targetHigh as NSNumber) ?? "--") \(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")"
|
|
|
|
|
+ )
|
|
|
|
|
+ .frame(width: 80, alignment: .trailing)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(.vertical, 4)
|
|
|
}
|
|
}
|
|
|
- .padding(.vertical, 4)
|
|
|
|
|
|
|
|
|
|
- // High target
|
|
|
|
|
- VStack(alignment: .leading) {
|
|
|
|
|
- Text("High Target")
|
|
|
|
|
- .font(.subheadline)
|
|
|
|
|
|
|
+ Divider()
|
|
|
|
|
|
|
|
- HStack {
|
|
|
|
|
- Slider(
|
|
|
|
|
- value: Binding(
|
|
|
|
|
- get: { Double(truncating: onboardingData.targetHigh as NSNumber) },
|
|
|
|
|
- set: { onboardingData.targetHigh = Decimal($0) }
|
|
|
|
|
- ),
|
|
|
|
|
- in: onboardingData.units == .mgdL ?
|
|
|
|
|
- Double(truncating: onboardingData.targetLow as NSNumber) + 10 ... 200 :
|
|
|
|
|
- Double(truncating: onboardingData.targetLow as NSNumber) + 0.6 ... 11.1,
|
|
|
|
|
- step: onboardingData.units == .mgdL ? 1 : 0.1
|
|
|
|
|
- )
|
|
|
|
|
- .accentColor(.green)
|
|
|
|
|
|
|
+ // Chart visualization
|
|
|
|
|
+ if !onboardingData.targetItems.isEmpty {
|
|
|
|
|
+ VStack(alignment: .leading) {
|
|
|
|
|
+ Text("Glucose Targets")
|
|
|
|
|
+ .font(.headline)
|
|
|
|
|
+ .padding(.horizontal)
|
|
|
|
|
|
|
|
- Text(
|
|
|
|
|
- "\(numberFormatter.string(from: onboardingData.targetHigh as NSNumber) ?? "--") \(onboardingData.units == .mgdL ? "mg/dL" : "mmol/L")"
|
|
|
|
|
- )
|
|
|
|
|
- .frame(width: 80, alignment: .trailing)
|
|
|
|
|
|
|
+ glucoseTargetChart
|
|
|
|
|
+ .frame(height: 180)
|
|
|
|
|
+ .padding(.horizontal)
|
|
|
}
|
|
}
|
|
|
|
|
+ .padding(.vertical, 5)
|
|
|
|
|
+ .background(Color.orange.opacity(0.05))
|
|
|
|
|
+ .cornerRadius(10)
|
|
|
}
|
|
}
|
|
|
- .padding(.vertical, 4)
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- Divider()
|
|
|
|
|
-
|
|
|
|
|
- // Target range visualization
|
|
|
|
|
- VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
- Text("Your Target Range")
|
|
|
|
|
- .font(.headline)
|
|
|
|
|
-
|
|
|
|
|
- HStack(spacing: 0) {
|
|
|
|
|
- // Below range
|
|
|
|
|
- Rectangle()
|
|
|
|
|
- .fill(Color.red.opacity(0.3))
|
|
|
|
|
- .frame(width: 50, height: 30)
|
|
|
|
|
- .overlay(
|
|
|
|
|
- Text("Low")
|
|
|
|
|
- .font(.caption)
|
|
|
|
|
- .foregroundColor(.red)
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ // Glucose target list
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 10) {
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text("Glucose Targets")
|
|
|
|
|
+ .font(.headline)
|
|
|
|
|
|
|
|
- // Target range
|
|
|
|
|
- Rectangle()
|
|
|
|
|
- .fill(Color.green.opacity(0.3))
|
|
|
|
|
- .frame(width: 100, height: 30)
|
|
|
|
|
- .overlay(
|
|
|
|
|
- Text("Target")
|
|
|
|
|
- .font(.caption)
|
|
|
|
|
- .foregroundColor(.green)
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ Spacer()
|
|
|
|
|
|
|
|
- // Above range
|
|
|
|
|
- Rectangle()
|
|
|
|
|
- .fill(Color.yellow.opacity(0.3))
|
|
|
|
|
- .frame(width: 50, height: 30)
|
|
|
|
|
- .overlay(
|
|
|
|
|
- Text("High")
|
|
|
|
|
- .font(.caption)
|
|
|
|
|
|
|
+ // Add new target button
|
|
|
|
|
+ if onboardingData.targetItems.count < 24 {
|
|
|
|
|
+ Button(action: {
|
|
|
|
|
+ showTimeSelector = true
|
|
|
|
|
+ }) {
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Image(systemName: "plus.circle.fill")
|
|
|
|
|
+ Text("Add Ratio")
|
|
|
|
|
+ }
|
|
|
.foregroundColor(.orange)
|
|
.foregroundColor(.orange)
|
|
|
- )
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ .disabled(!canAddRatio)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .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.timeIndex]
|
|
|
|
|
+ ))
|
|
|
|
|
+ )
|
|
|
|
|
+ .frame(width: 80, alignment: .leading)
|
|
|
|
|
+ .padding(.leading)
|
|
|
|
|
+
|
|
|
|
|
+ // Ratio slider
|
|
|
|
|
+ Slider(
|
|
|
|
|
+ value: Binding(
|
|
|
|
|
+ get: {
|
|
|
|
|
+ Double(
|
|
|
|
|
+ truncating: onboardingData
|
|
|
|
|
+ .targetRateValues[item.rateIndex] as NSNumber
|
|
|
|
|
+ ) },
|
|
|
|
|
+ set: { newValue in
|
|
|
|
|
+ // Find closest match in rateValues array
|
|
|
|
|
+ let newIndex = onboardingData.targetRateValues
|
|
|
|
|
+ .firstIndex { abs(Double($0) - newValue) < 0.05 } ?? item.rateIndex
|
|
|
|
|
+ onboardingData.targetItems[index].rateIndex = 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].rateIndex) { _, _ in
|
|
|
|
|
+ let impact = UIImpactFeedbackGenerator(style: .light)
|
|
|
|
|
+ impact.impactOccurred()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Display the current value
|
|
|
|
|
+ Text(
|
|
|
|
|
+ "\(Formatters.decimalFormatterWithOneFractionDigit.string(from: onboardingData.targetRateValues[item.rateIndex] 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)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .background(Color.orange.opacity(0.05))
|
|
|
|
|
+ .cornerRadius(10)
|
|
|
|
|
+ .padding(.horizontal)
|
|
|
|
|
+ .onAppear {
|
|
|
|
|
+ if onboardingData.targetItems.isEmpty {
|
|
|
|
|
+ onboardingData.addTarget()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- .cornerRadius(8)
|
|
|
|
|
|
|
|
|
|
- // Range values
|
|
|
|
|
- HStack(spacing: 0) {
|
|
|
|
|
- Text("\(numberFormatter.string(from: onboardingData.targetLow as NSNumber) ?? "--")")
|
|
|
|
|
- .font(.caption)
|
|
|
|
|
- .frame(width: 50, alignment: .center)
|
|
|
|
|
|
|
+ // Target range visualization
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
+ Text("Your Target Range")
|
|
|
|
|
+ .font(.headline)
|
|
|
|
|
|
|
|
- Spacer()
|
|
|
|
|
- .frame(width: 100)
|
|
|
|
|
|
|
+ HStack(spacing: 0) {
|
|
|
|
|
+ // Below range
|
|
|
|
|
+ Rectangle()
|
|
|
|
|
+ .fill(Color.red.opacity(0.3))
|
|
|
|
|
+ .frame(width: 50, height: 30)
|
|
|
|
|
+ .overlay(
|
|
|
|
|
+ Text("Low")
|
|
|
|
|
+ .font(.caption)
|
|
|
|
|
+ .foregroundColor(.red)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // Target range
|
|
|
|
|
+ Rectangle()
|
|
|
|
|
+ .fill(Color.green.opacity(0.3))
|
|
|
|
|
+ .frame(width: 100, height: 30)
|
|
|
|
|
+ .overlay(
|
|
|
|
|
+ Text("Target")
|
|
|
|
|
+ .font(.caption)
|
|
|
|
|
+ .foregroundColor(.green)
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- Text("\(numberFormatter.string(from: onboardingData.targetHigh as NSNumber) ?? "--")")
|
|
|
|
|
|
|
+ // Above range
|
|
|
|
|
+ Rectangle()
|
|
|
|
|
+ .fill(Color.yellow.opacity(0.3))
|
|
|
|
|
+ .frame(width: 50, height: 30)
|
|
|
|
|
+ .overlay(
|
|
|
|
|
+ Text("High")
|
|
|
|
|
+ .font(.caption)
|
|
|
|
|
+ .foregroundColor(.orange)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ .cornerRadius(8)
|
|
|
|
|
+
|
|
|
|
|
+ // Range values
|
|
|
|
|
+ HStack(spacing: 0) {
|
|
|
|
|
+ Text("\(numberFormatter.string(from: onboardingData.targetLow as NSNumber) ?? "--")")
|
|
|
|
|
+ .font(.caption)
|
|
|
|
|
+ .frame(width: 50, alignment: .center)
|
|
|
|
|
+
|
|
|
|
|
+ Spacer()
|
|
|
|
|
+ .frame(width: 100)
|
|
|
|
|
+
|
|
|
|
|
+ Text("\(numberFormatter.string(from: onboardingData.targetHigh as NSNumber) ?? "--")")
|
|
|
|
|
+ .font(.caption)
|
|
|
|
|
+ .frame(width: 50, alignment: .center)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Text("These values reflect your personal target range and can be adjusted at any time in the Settings.")
|
|
|
.font(.caption)
|
|
.font(.caption)
|
|
|
- .frame(width: 50, alignment: .center)
|
|
|
|
|
|
|
+ .foregroundColor(.secondary)
|
|
|
|
|
+ .padding(.top, 8)
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding()
|
|
|
|
|
+ }
|
|
|
|
|
+ .actionSheet(isPresented: $showTimeSelector) {
|
|
|
|
|
+ var buttons: [ActionSheet.Button] = []
|
|
|
|
|
|
|
|
- Text("These values reflect your personal target range and can be adjusted at any time in the Settings.")
|
|
|
|
|
- .font(.caption)
|
|
|
|
|
- .foregroundColor(.secondary)
|
|
|
|
|
- .padding(.top, 8)
|
|
|
|
|
|
|
+ // Find available time slots in 1-hour increments
|
|
|
|
|
+ for hour in 0 ..< 24 {
|
|
|
|
|
+ let hourInMinutes = hour * 60
|
|
|
|
|
+ // 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
|
|
|
|
|
+ let rateIndex = onboardingData.targetItems.last?.rateIndex ?? 0
|
|
|
|
|
+ // Create new item with the specified time
|
|
|
|
|
+ let newItem = TargetsEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
|
|
|
|
|
+ // Add the new item and sort the list
|
|
|
|
|
+ onboardingData.targetItems.append(newItem)
|
|
|
|
|
+ onboardingData.targetItems.sort(by: { $0.timeIndex < $1.timeIndex })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ buttons.append(.cancel())
|
|
|
|
|
+
|
|
|
|
|
+ return ActionSheet(
|
|
|
|
|
+ title: Text("Select Start Time"),
|
|
|
|
|
+ message: Text("Choose when this carb ratio should start"),
|
|
|
|
|
+ buttons: buttons
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Computed property to check if we can add more carb ratios
|
|
|
|
|
+ private var canAddRatio: Bool {
|
|
|
|
|
+ guard let lastItem = onboardingData.targetItems.last else { return true }
|
|
|
|
|
+ return lastItem.timeIndex < onboardingData.targetTimeValues.count - 1
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Chart for visualizing carb ratios
|
|
|
|
|
+ private var glucoseTargetChart: some View {
|
|
|
|
|
+ Chart {
|
|
|
|
|
+ ForEach(Array(onboardingData.targetItems.enumerated()), id: \.element.id) { index, item in
|
|
|
|
|
+ let displayValue = onboardingData.targetRateValues[item.timeIndex]
|
|
|
|
|
+
|
|
|
|
|
+ let tzOffset = TimeZone.current.secondsFromGMT() * -1
|
|
|
|
|
+ let startDate = Date(timeIntervalSinceReferenceDate: onboardingData.targetTimeValues[item.timeIndex])
|
|
|
|
|
+ .addingTimeInterval(TimeInterval(tzOffset))
|
|
|
|
|
+ let endDate = onboardingData.targetItems.count > index + 1 ?
|
|
|
|
|
+ Date(
|
|
|
|
|
+ timeIntervalSinceReferenceDate: onboardingData
|
|
|
|
|
+ .targetTimeValues[onboardingData.targetItems[index + 1].timeIndex]
|
|
|
|
|
+ )
|
|
|
|
|
+ .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)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .id(refreshUI) // Force chart update
|
|
|
|
|
+ .chartXAxis {
|
|
|
|
|
+ AxisMarks(values: .automatic(desiredCount: 6)) { _ in
|
|
|
|
|
+ AxisValueLabel(format: .dateTime.hour())
|
|
|
|
|
+ AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .chartXScale(
|
|
|
|
|
+ domain: Calendar.current.startOfDay(for: chartScale!) ... Calendar.current.startOfDay(for: chartScale!)
|
|
|
|
|
+ .addingTimeInterval(60 * 60 * 24)
|
|
|
|
|
+ )
|
|
|
|
|
+ .chartYAxis {
|
|
|
|
|
+ AxisMarks(values: .automatic(desiredCount: 4)) { _ in
|
|
|
|
|
+ AxisValueLabel()
|
|
|
|
|
+ AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- .padding()
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|