polscm32 1 年之前
父節點
當前提交
e160559f0a

+ 3 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -48664,6 +48664,9 @@
     "Choose when this sensitivity factor should start" : {
 
     },
+    "Choose when this target should start" : {
+
+    },
     "Choose whether or not to display one or both X- and Y-Axis grid lines." : {
       "localizations" : {
         "bg" : {

+ 201 - 142
Trio/Sources/Modules/Main/View/OnboardingSteps/GlucoseTargetStepView.swift

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