|
|
@@ -6,41 +6,43 @@ extension BasalProfileEditor {
|
|
|
struct RootView: BaseView {
|
|
|
let resolver: Resolver
|
|
|
@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(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 {
|
|
|
let formatter = NumberFormatter()
|
|
|
formatter.numberStyle = .decimal
|
|
|
-
|
|
|
return formatter
|
|
|
}
|
|
|
|
|
|
- var now = Date()
|
|
|
- var basalScheduleChart: some View {
|
|
|
+ // Chart for visualizing basal profile
|
|
|
+ private var basalProfileChart: some View {
|
|
|
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(
|
|
|
xStart: .value("start", startDate),
|
|
|
xEnd: .value("end", endDate),
|
|
|
- yStart: .value("rate-start", profile.amount),
|
|
|
+ yStart: .value("rate-start", displayValue),
|
|
|
yEnd: .value("rate-end", 0)
|
|
|
).foregroundStyle(
|
|
|
.linearGradient(
|
|
|
@@ -53,30 +55,30 @@ extension BasalProfileEditor {
|
|
|
)
|
|
|
).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)
|
|
|
|
|
|
- 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)
|
|
|
}
|
|
|
}
|
|
|
+ .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: now) ... Calendar.current.startOfDay(for: now)
|
|
|
+ .addingTimeInterval(60 * 60 * 24)
|
|
|
+ )
|
|
|
.chartYAxis {
|
|
|
- AxisMarks(values: .automatic(desiredCount: 2)) { _ in
|
|
|
+ AxisMarks(values: .automatic(desiredCount: 4)) { _ in
|
|
|
AxisValueLabel()
|
|
|
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 {
|
|
|
@@ -116,147 +118,134 @@ extension BasalProfileEditor {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- 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)
|
|
|
- }
|
|
|
+ var fullScheduleWarning: some View {
|
|
|
+ VStack {
|
|
|
+ Text(
|
|
|
+ "Basal profile covers 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space."
|
|
|
+ ).bold()
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
+ .padding()
|
|
|
+ .background(Color.tabBar)
|
|
|
+ .clipShape(
|
|
|
+ .rect(
|
|
|
+ topLeadingRadius: 10,
|
|
|
+ bottomLeadingRadius: 10,
|
|
|
+ bottomTrailingRadius: 10,
|
|
|
+ topTrailingRadius: 10
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- Section(header: Text("Schedule")) {
|
|
|
- if !state.items.isEmpty {
|
|
|
- basalScheduleChart.padding(.vertical)
|
|
|
- }
|
|
|
+ var totalBasalRow: some View {
|
|
|
+ VStack(alignment: .leading, spacing: 0) {
|
|
|
+ HStack {
|
|
|
+ Text("Total")
|
|
|
+ .bold()
|
|
|
|
|
|
- list
|
|
|
- }.listRowBackground(Color.chart)
|
|
|
+ Spacer()
|
|
|
|
|
|
- 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)
|
|
|
-
|
|
|
- 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.")
|
|
|
- }
|
|
|
+ Text("U/day")
|
|
|
+ .foregroundStyle(Color.secondary)
|
|
|
}
|
|
|
- .textCase(nil)
|
|
|
+ .id(refreshUI)
|
|
|
}
|
|
|
}
|
|
|
- .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)
|
|
|
- }
|
|
|
- })
|
|
|
- .environment(\.editMode, $editMode)
|
|
|
- .onAppear {
|
|
|
- configureView()
|
|
|
- state.validate()
|
|
|
- state.calculateChartData()
|
|
|
- }
|
|
|
+ .padding()
|
|
|
+ .background(Color.chart.opacity(0.65))
|
|
|
+ .cornerRadius(10)
|
|
|
+ .padding(.horizontal)
|
|
|
+ .id(bottomID)
|
|
|
}
|
|
|
|
|
|
- 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)
|
|
|
+ var body: some View {
|
|
|
+ ScrollViewReader { proxy in
|
|
|
+ VStack(spacing: 0) {
|
|
|
+ ScrollView {
|
|
|
+ LazyVStack {
|
|
|
+ VStack(alignment: .leading, spacing: 0) {
|
|
|
+ if !state.canAdd {
|
|
|
+ fullScheduleWarning
|
|
|
+ .padding()
|
|
|
+ }
|
|
|
|
|
|
- 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)
|
|
|
- }
|
|
|
+ // Chart visualization
|
|
|
+ if !state.items.isEmpty {
|
|
|
+ basalProfileChart
|
|
|
+ .frame(height: 180)
|
|
|
+ .padding()
|
|
|
+ .background(Color.chart.opacity(0.65))
|
|
|
+ .clipShape(
|
|
|
+ .rect(
|
|
|
+ topLeadingRadius: 10,
|
|
|
+ bottomLeadingRadius: 0,
|
|
|
+ bottomTrailingRadius: 0,
|
|
|
+ topTrailingRadius: 10
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .padding(.horizontal)
|
|
|
+ .padding(.top)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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)
|
|
|
+
|
|
|
+ if !state.items.isEmpty {
|
|
|
+ totalBasalRow
|
|
|
+ }
|
|
|
|
|
|
- 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])))"
|
|
|
- )
|
|
|
+ HStack {
|
|
|
+ Image(systemName: "hand.draw.fill")
|
|
|
+ .padding(.leading)
|
|
|
+
|
|
|
+ Text("Swipe to delete a single entry. Tap on it, to edit its time or value.")
|
|
|
+ .padding(.trailing)
|
|
|
+ }
|
|
|
+ .font(.subheadline)
|
|
|
+ .fontWeight(.light)
|
|
|
+ .foregroundStyle(.secondary)
|
|
|
+ .padding()
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- .moveDisabled(true)
|
|
|
+
|
|
|
+ saveButton
|
|
|
+ }
|
|
|
+ .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"))
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .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()
|
|
|
- }
|
|
|
}
|
|
|
}
|