Browse Source

refine therapy settings UI in app (carb ratio)

Marvin Polscheit 7 months ago
parent
commit
aae326fb00

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

@@ -3176,6 +3176,7 @@
     },
     },
     " U/day" : {
     " U/day" : {
       "comment" : "Total AT / Scheduled basal insulin",
       "comment" : "Total AT / Scheduled basal insulin",
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -7863,6 +7864,7 @@
     },
     },
     "%@ U/hr" : {
     "%@ U/hr" : {
       "comment" : "Number of units per hour",
       "comment" : "Number of units per hour",
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -22927,6 +22929,7 @@
       }
       }
     },
     },
     "Add an entry by tapping 'Add Rate +' in the top right-hand corner of the screen." : {
     "Add an entry by tapping 'Add Rate +' in the top right-hand corner of the screen." : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -26383,6 +26386,7 @@
       }
       }
     },
     },
     "Add Rate" : {
     "Add Rate" : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {
@@ -45717,6 +45721,7 @@
       }
       }
     },
     },
     "Basal profile covers 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space." : {
     "Basal profile covers 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space." : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {

+ 20 - 0
Trio/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -9,6 +9,7 @@ extension BasalProfileEditor {
         var syncInProgress: Bool = false
         var syncInProgress: Bool = false
         var initialItems: [Item] = []
         var initialItems: [Item] = []
         var items: [Item] = []
         var items: [Item] = []
+        var therapyItems: [TherapySettingItem] = []
         var total: Decimal = 0.0
         var total: Decimal = 0.0
         var showAlert: Bool = false
         var showAlert: Bool = false
         var chartData: [BasalProfile]? = []
         var chartData: [BasalProfile]? = []
@@ -26,6 +27,25 @@ extension BasalProfileEditor {
             initialItems != items
             initialItems != items
         }
         }
 
 
+        // Convert items to TherapySettingItem format
+        func getTherapyItems() -> [TherapySettingItem] {
+            items.map { item in
+                TherapySettingItem(
+                    time: timeValues[item.timeIndex],
+                    value: rateValues[item.rateIndex]
+                )
+            }
+        }
+
+        // Update items from TherapySettingItem format
+        func updateFromTherapyItems(_ therapyItems: [TherapySettingItem]) {
+            items = therapyItems.map { therapyItem in
+                let timeIndex = timeValues.firstIndex(where: { abs($0 - therapyItem.time) < 1 }) ?? 0
+                let rateIndex = rateValues.firstIndex(of: therapyItem.value) ?? 0
+                return Item(rateIndex: rateIndex, timeIndex: timeIndex)
+            }
+        }
+
         override func subscribe() {
         override func subscribe() {
             rateValues = provider.supportedBasalRates ?? stride(from: 5.0, to: 1001.0, by: 5.0)
             rateValues = provider.supportedBasalRates ?? stride(from: 5.0, to: 1001.0, by: 5.0)
                 .map { ($0.decimal ?? .zero) / 100 }
                 .map { ($0.decimal ?? .zero) / 100 }

+ 117 - 162
Trio/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -6,41 +6,43 @@ extension BasalProfileEditor {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
         @State var state = StateModel()
         @State var state = StateModel()
-        @State private var editMode = EditMode.inactive
-
-        let chartScale = Calendar.current
-            .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
-        let tzOffset = TimeZone.current.secondsFromGMT()
+        @State private var refreshUI = UUID()
+        @State private var now = Date()
+        @Namespace private var bottomID
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
         @Environment(AppState.self) var appState
 
 
-        private var dateFormatter: DateFormatter {
-            let formatter = DateFormatter()
-            formatter.timeZone = TimeZone(secondsFromGMT: 0)
-            formatter.timeStyle = .short
-            return formatter
-        }
-
         private var rateFormatter: NumberFormatter {
         private var rateFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             formatter.numberStyle = .decimal
-
             return formatter
             return formatter
         }
         }
 
 
-        var now = Date()
-        var basalScheduleChart: some View {
+        // Chart for visualizing basal profile
+        private var basalProfileChart: some View {
             Chart {
             Chart {
-                ForEach(state.chartData!, id: \.self) { profile in
-                    let startDate = Calendar.current.startOfDay(for: now)
-                        .addingTimeInterval(profile.startDate.timeIntervalSinceReferenceDate + Double(tzOffset))
-                    let endDate = Calendar.current.startOfDay(for: now)
-                        .addingTimeInterval(profile.endDate!.timeIntervalSinceReferenceDate + Double(tzOffset))
+                ForEach(Array(state.items.enumerated()), id: \.element.id) { index, item in
+                    let displayValue = state.rateValues[item.rateIndex]
+
+                    let startDate = Calendar.current
+                        .startOfDay(for: now)
+                        .addingTimeInterval(state.timeValues[item.timeIndex])
+
+                    var offset: TimeInterval {
+                        if state.items.count > index + 1 {
+                            return state.timeValues[state.items[index + 1].timeIndex]
+                        } else {
+                            return state.timeValues.last! + 30 * 60
+                        }
+                    }
+
+                    let endDate = Calendar.current.startOfDay(for: now).addingTimeInterval(offset)
+
                     RectangleMark(
                     RectangleMark(
                         xStart: .value("start", startDate),
                         xStart: .value("start", startDate),
                         xEnd: .value("end", endDate),
                         xEnd: .value("end", endDate),
-                        yStart: .value("rate-start", profile.amount),
+                        yStart: .value("rate-start", displayValue),
                         yEnd: .value("rate-end", 0)
                         yEnd: .value("rate-end", 0)
                     ).foregroundStyle(
                     ).foregroundStyle(
                         .linearGradient(
                         .linearGradient(
@@ -53,30 +55,30 @@ extension BasalProfileEditor {
                         )
                         )
                     ).alignsMarkStylesWithPlotArea()
                     ).alignsMarkStylesWithPlotArea()
 
 
-                    LineMark(x: .value("End Date", endDate), y: .value("Amount", profile.amount))
+                    LineMark(x: .value("End Date", startDate), y: .value("Rate", displayValue))
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.purple)
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.purple)
 
 
-                    LineMark(x: .value("Start Date", startDate), y: .value("Amount", profile.amount))
+                    LineMark(x: .value("Start Date", endDate), y: .value("Rate", displayValue))
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.purple)
                         .lineStyle(.init(lineWidth: 1)).foregroundStyle(Color.purple)
                 }
                 }
             }
             }
+            .id(refreshUI) // Force chart update
             .chartXAxis {
             .chartXAxis {
                 AxisMarks(values: .automatic(desiredCount: 6)) { _ in
                 AxisMarks(values: .automatic(desiredCount: 6)) { _ in
                     AxisValueLabel(format: .dateTime.hour())
                     AxisValueLabel(format: .dateTime.hour())
                     AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
                     AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
                 }
                 }
             }
             }
+            .chartXScale(
+                domain: Calendar.current.startOfDay(for: now) ... Calendar.current.startOfDay(for: now)
+                    .addingTimeInterval(60 * 60 * 24)
+            )
             .chartYAxis {
             .chartYAxis {
-                AxisMarks(values: .automatic(desiredCount: 2)) { _ in
+                AxisMarks(values: .automatic(desiredCount: 4)) { _ in
                     AxisValueLabel()
                     AxisValueLabel()
                     AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
                     AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1, dash: [2, 4]))
                 }
                 }
             }
             }
-            .chartXScale(
-                domain: Calendar.current.startOfDay(for: now) ... Calendar
-                    .current.startOfDay(for: now)
-                    .addingTimeInterval(60 * 60 * 24)
-            )
         }
         }
 
 
         var saveButton: some View {
         var saveButton: some View {
@@ -117,146 +119,99 @@ extension BasalProfileEditor {
         }
         }
 
 
         var body: some View {
         var body: some View {
-            Form {
-                if !state.canAdd {
-                    Section {
-                        VStack(alignment: .leading) {
-                            Text(
-                                "Basal profile covers 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space."
-                            ).bold()
-                        }
-                    }.listRowBackground(Color.tabBar)
-                }
-
-                Section(header: Text("Schedule")) {
-                    if !state.items.isEmpty {
-                        basalScheduleChart.padding(.vertical)
-                    }
-
-                    list
-                }.listRowBackground(Color.chart)
-
-                Section {
-                    HStack {
-                        Text("Total")
-                            .bold()
-                            .foregroundColor(.primary)
-                        Spacer()
-                        Text(rateFormatter.string(from: state.total as NSNumber) ?? "0")
-                            .foregroundColor(.primary) +
-                            Text(" U/day")
-                            .foregroundColor(.secondary)
-                    }
-                }.listRowBackground(Color.chart)
+            ScrollViewReader { proxy in
+                VStack(spacing: 0) {
+                    ScrollView {
+                        LazyVStack {
+                            VStack(alignment: .leading, spacing: 0) {
+                                // Chart visualization
+                                if !state.items.isEmpty {
+                                    VStack(alignment: .leading) {
+                                        basalProfileChart
+                                            .frame(height: 180)
+                                            .padding(.horizontal)
+                                    }
+                                    .padding(.vertical)
+                                    .background(Color.chart.opacity(0.65))
+                                    .clipShape(
+                                        .rect(
+                                            topLeadingRadius: 10,
+                                            bottomLeadingRadius: 0,
+                                            bottomTrailingRadius: 0,
+                                            topTrailingRadius: 10
+                                        )
+                                    )
+                                    .padding(.horizontal)
+                                    .padding(.top)
+                                }
 
 
-                Section {} header: {
-                    VStack(alignment: .leading, spacing: 10) {
-                        HStack {
-                            Image(systemName: "note.text.badge.plus").foregroundStyle(.primary)
-                            Text("Add an entry by tapping 'Add Rate +' in the top right-hand corner of the screen.")
-                        }
-                        HStack {
-                            Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
-                            Text("Swipe to delete a single entry. Tap on it, to edit its time or rate.")
+                                // Basal profile list
+                                TherapySettingEditorView(
+                                    items: $state.therapyItems,
+                                    unit: .unitPerHour,
+                                    timeOptions: state.timeValues,
+                                    valueOptions: state.rateValues,
+                                    validateOnDelete: state.validate,
+                                    onItemAdded: {
+                                        withAnimation {
+                                            proxy.scrollTo(bottomID, anchor: .bottom)
+                                        }
+                                    }
+                                )
+                                .padding(.horizontal)
+
+                                Spacer(minLength: 20)
+
+                                // Total daily basal calculation
+                                if !state.items.isEmpty {
+                                    VStack(alignment: .leading, spacing: 0) {
+                                        HStack {
+                                            Text("Total")
+                                                .bold()
+
+                                            Spacer()
+
+                                            HStack {
+                                                Text(rateFormatter.string(from: state.total as NSNumber) ?? "0")
+                                                Text("U/day")
+                                                    .foregroundStyle(Color.secondary)
+                                            }
+                                            .id(refreshUI)
+                                        }
+                                    }
+                                    .padding()
+                                    .background(Color.chart.opacity(0.65))
+                                    .cornerRadius(10)
+                                    .padding(.horizontal)
+                                    .id(bottomID)
+                                }
+                            }
                         }
                         }
                     }
                     }
-                    .textCase(nil)
+
+                    saveButton
                 }
                 }
-            }
-            .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
-            .alert(isPresented: $state.showAlert) {
-                Alert(
-                    title: Text("Unable to Save"),
-                    message: Text("Trio could not communicate with your pump. Changes to your basal profile were not saved."),
-                    dismissButton: .default(Text("Close"))
-                )
-            }
-            .onChange(of: state.items) {
-                state.calcTotal()
-                state.calculateChartData()
-            }
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .navigationTitle("Basal Rates")
-            .navigationBarTitleDisplayMode(.automatic)
-            .toolbar(content: {
-                ToolbarItem(placement: .topBarTrailing) {
-                    Button(action: { state.add() }) {
-                        HStack {
-                            Text("Add Rate")
-                            Image(systemName: "plus")
-                        }
-                    }.disabled(!state.canAdd)
+                .background(appState.trioBackgroundColor(for: colorScheme))
+                .alert(isPresented: $state.showAlert) {
+                    Alert(
+                        title: Text("Unable to Save"),
+                        message: Text("Trio could not communicate with your pump. Changes to your basal profile were not saved."),
+                        dismissButton: .default(Text("Close"))
+                    )
                 }
                 }
-            })
-            .environment(\.editMode, $editMode)
-            .onAppear {
-                configureView()
-                state.validate()
-                state.calculateChartData()
-            }
-        }
-
-        private func pickers(for index: Int) -> some View {
-            Form {
-                Section {
-                    Picker(selection: $state.items[index].rateIndex, label: Text("Rate")) {
-                        ForEach(0 ..< state.rateValues.count, id: \.self) { i in
-                            Text(
-                                (self.rateFormatter.string(from: state.rateValues[i] as NSNumber) ?? "") + " " +
-                                    String(localized: "U/hr")
-                            ).tag(i)
-                        }
-                    }
-                    .onChange(of: state.items[index].rateIndex, { state.calcTotal() })
-                }.listRowBackground(Color.chart)
-
-                Section {
-                    Picker(selection: $state.items[index].timeIndex, label: Text("Time")) {
-                        ForEach(state.availableTimeIndices(index), id: \.self) { i in
-                            Text(
-                                self.dateFormatter
-                                    .string(from: Date(
-                                        timeIntervalSince1970: state
-                                            .timeValues[i]
-                                    ))
-                            ).tag(i)
-                        }
-                    }
-                    .onChange(of: state.items[index].timeIndex, { state.calcTotal() })
-                }.listRowBackground(Color.chart)
-            }
-            .padding(.top)
-            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
-            .navigationTitle("Set Rate")
-            .navigationBarTitleDisplayMode(.automatic)
-        }
-
-        private var list: some View {
-            List {
-                ForEach(state.items.indexed(), id: \.1.id) { index, item in
-                    NavigationLink(destination: pickers(for: index)) {
-                        HStack {
-                            Text("Rate").foregroundColor(.secondary)
-                            Text(
-                                "\(rateFormatter.string(from: state.rateValues[item.rateIndex] as NSNumber) ?? "0") U/hr"
-                            )
-                            Spacer()
-                            Text("starts at").foregroundColor(.secondary)
-                            Text(
-                                "\(dateFormatter.string(from: Date(timeIntervalSince1970: state.timeValues[item.timeIndex])))"
-                            )
-                        }
-                    }
-                    .moveDisabled(true)
+                .navigationTitle("Basal Rates")
+                .navigationBarTitleDisplayMode(.automatic)
+                .onAppear {
+                    configureView()
+                    state.validate()
+                    state.therapyItems = state.getTherapyItems()
+                }
+                .onChange(of: state.therapyItems) { _, newItems in
+                    state.updateFromTherapyItems(newItems)
+                    state.calcTotal()
+                    refreshUI = UUID()
                 }
                 }
-                .onDelete(perform: onDelete)
             }
             }
         }
         }
-
-        private func onDelete(offsets: IndexSet) {
-            state.items.remove(atOffsets: offsets)
-            state.validate()
-            state.calculateChartData()
-        }
     }
     }
 }
 }