EditTempTargetForm.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import Foundation
  2. import SwiftUI
  3. struct EditTempTargetForm: View {
  4. @ObservedObject var tempTarget: TempTargetStored
  5. @Environment(\.presentationMode) var presentationMode
  6. @Environment(\.colorScheme) var colorScheme
  7. @StateObject var state: OverrideConfig.StateModel
  8. @State private var name: String
  9. @State private var target: Decimal
  10. @State private var duration: Decimal
  11. @State private var date: Date
  12. @State private var halfBasalTarget: Decimal
  13. @State private var percentage: Decimal
  14. @State private var hasChanges = false
  15. @State private var showAlert = false
  16. @State private var isUsingSlider = false
  17. init(tempTargetToEdit: TempTargetStored, state: OverrideConfig.StateModel) {
  18. tempTarget = tempTargetToEdit
  19. _state = StateObject(wrappedValue: state)
  20. _name = State(initialValue: tempTargetToEdit.name ?? "")
  21. _target = State(initialValue: tempTargetToEdit.target?.decimalValue ?? 0)
  22. _duration = State(initialValue: tempTargetToEdit.duration?.decimalValue ?? 0)
  23. _date = State(initialValue: tempTargetToEdit.date ?? Date())
  24. _halfBasalTarget = State(initialValue: tempTargetToEdit.halfBasalTarget?.decimalValue ?? 160)
  25. let normalTarget: Decimal = 100
  26. if let hbt = tempTargetToEdit.halfBasalTarget?.decimalValue {
  27. let H = hbt
  28. let N: Decimal = normalTarget
  29. var T = tempTargetToEdit.target?.decimalValue ?? 0
  30. if state.units == .mmolL {
  31. T = T.asMgdL
  32. }
  33. let denominator = H - (2 * N) + T
  34. if denominator != 0 {
  35. let ratio = (H - N) / denominator
  36. _percentage = State(initialValue: ratio * 100)
  37. } else {
  38. _percentage = State(initialValue: 100)
  39. }
  40. } else {
  41. _percentage = State(initialValue: 100)
  42. }
  43. }
  44. var color: LinearGradient {
  45. colorScheme == .dark ? LinearGradient(
  46. gradient: Gradient(colors: [
  47. Color.bgDarkBlue,
  48. Color.bgDarkerDarkBlue
  49. ]),
  50. startPoint: .top,
  51. endPoint: .bottom
  52. ) :
  53. LinearGradient(
  54. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  55. startPoint: .top,
  56. endPoint: .bottom
  57. )
  58. }
  59. private var formatter: NumberFormatter {
  60. let formatter = NumberFormatter()
  61. formatter.numberStyle = .decimal
  62. formatter.maximumFractionDigits = 0
  63. return formatter
  64. }
  65. private var glucoseFormatter: NumberFormatter {
  66. let formatter = NumberFormatter()
  67. formatter.numberStyle = .decimal
  68. if state.units == .mmolL {
  69. formatter.maximumFractionDigits = 1
  70. } else {
  71. formatter.maximumFractionDigits = 0
  72. }
  73. formatter.roundingMode = .halfUp
  74. return formatter
  75. }
  76. var body: some View {
  77. NavigationView {
  78. Form {
  79. editTempTarget()
  80. saveButton
  81. }.scrollContentBackground(.hidden)
  82. .background(color)
  83. .navigationTitle("Edit Temp Target")
  84. .navigationBarTitleDisplayMode(.inline)
  85. .navigationBarItems(leading: Button("Close") {
  86. presentationMode.wrappedValue.dismiss()
  87. })
  88. .onDisappear {
  89. if !hasChanges {
  90. // Reset UI changes
  91. resetValues()
  92. }
  93. }
  94. .alert(isPresented: $state.showInvalidTargetAlert) {
  95. Alert(
  96. title: Text("Invalid Input"),
  97. message: Text("\(state.alertMessage)"),
  98. dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false }
  99. )
  100. }
  101. }
  102. }
  103. @ViewBuilder private func editTempTarget() -> some View {
  104. Section(
  105. header: Text("Configure Temp Target"),
  106. content: {
  107. HStack {
  108. Text("Name")
  109. Spacer()
  110. TextField("Enter Name (optional)", text: $name)
  111. .multilineTextAlignment(.trailing)
  112. }
  113. HStack {
  114. Text("Target")
  115. Spacer()
  116. TextFieldWithToolBar(
  117. text: Binding(
  118. get: { target },
  119. set: {
  120. target = $0
  121. hasChanges = true
  122. }
  123. ),
  124. placeholder: "0",
  125. numberFormatter: glucoseFormatter
  126. )
  127. Text(state.units.rawValue).foregroundColor(.secondary)
  128. }
  129. HStack {
  130. Text("Duration")
  131. Spacer()
  132. TextFieldWithToolBar(
  133. text: Binding(
  134. get: { duration },
  135. set: {
  136. duration = $0
  137. hasChanges = true
  138. }
  139. ),
  140. placeholder: "0",
  141. numberFormatter: formatter
  142. )
  143. Text("minutes").foregroundColor(.secondary)
  144. }
  145. DatePicker("Date", selection: $date)
  146. .onChange(of: date) { _ in hasChanges = true }
  147. }
  148. ).listRowBackground(Color.chart)
  149. if state.computeSliderLow() != state.computeSliderHigh() {
  150. Section {
  151. VStack {
  152. VStack {
  153. Text("\(percentage.formatted(.number.precision(.fractionLength(0)))) % Insulin")
  154. .foregroundColor(isUsingSlider ? .orange : Color.tabBar)
  155. .font(.largeTitle)
  156. Slider(value: Binding(
  157. get: {
  158. Double(truncating: percentage as NSNumber)
  159. },
  160. set: { newValue in
  161. percentage = Decimal(newValue)
  162. hasChanges = true
  163. // Calculate the halfBasalTarget based on the new percentage value
  164. let ratio = Decimal(Int(percentage) / 100)
  165. let normalTarget: Decimal = 100
  166. var target: Decimal = target
  167. if state.units == .mmolL {
  168. target = target.asMgdL
  169. }
  170. if ratio != 1 {
  171. let hbtcalc = ((2 * ratio * normalTarget) - normalTarget - (ratio * target)) / (ratio - 1)
  172. halfBasalTarget = hbtcalc
  173. } else {
  174. halfBasalTarget = normalTarget
  175. }
  176. }
  177. ), in: Double(state.computeSliderLow()) ... Double(state.computeSliderHigh()), step: 5) {}
  178. minimumValueLabel: {
  179. Text("\(state.computeSliderLow(), specifier: "%.0f")%")
  180. }
  181. maximumValueLabel: {
  182. Text("\(state.computeSliderHigh(), specifier: "%.0f")%")
  183. }
  184. onEditingChanged: { editing in
  185. isUsingSlider = editing
  186. state.halfBasalTarget = Decimal(state.computeHalfBasalTarget())
  187. }
  188. Divider()
  189. Text(
  190. state
  191. .units == .mgdL ?
  192. "Half Basal Exercise Target at: \(halfBasalTarget.formatted(.number.precision(.fractionLength(0)))) mg/dl" :
  193. "Half Basal Exercise Target at: \(halfBasalTarget.asMmolL.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))) mmol/L"
  194. )
  195. .foregroundColor(.secondary)
  196. .font(.caption).italic()
  197. }
  198. }
  199. } header: {
  200. Text("% Insulin")
  201. } footer: {
  202. Text("The Slider values are limited to your Autosens Min and Max Settings!")
  203. }.listRowBackground(Color.chart)
  204. }
  205. }
  206. private var saveButton: some View {
  207. HStack {
  208. Spacer()
  209. Button(action: {
  210. if !state.isInputInvalid(target: target) {
  211. saveChanges()
  212. do {
  213. guard let moc = tempTarget.managedObjectContext else { return }
  214. guard moc.hasChanges else { return }
  215. try moc.save()
  216. // Disable previous active Temp Target and update View
  217. if let currentActiveTempTarget = state.currentActiveTempTarget {
  218. Task {
  219. await state.disableAllActiveOverrides(
  220. except: currentActiveTempTarget.objectID,
  221. createOverrideRunEntry: false
  222. )
  223. state.updateLatestTempTargetConfiguration()
  224. }
  225. }
  226. hasChanges = false
  227. presentationMode.wrappedValue.dismiss()
  228. } catch {
  229. debugPrint("Failed to Edit Temp Target")
  230. }
  231. }
  232. }, label: {
  233. Text("Save")
  234. })
  235. .disabled(!hasChanges)
  236. .frame(maxWidth: .infinity, alignment: .center)
  237. .tint(.white)
  238. Spacer()
  239. }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4))
  240. }
  241. private func saveChanges() {
  242. tempTarget.name = name
  243. tempTarget.target = NSDecimalNumber(decimal: target)
  244. tempTarget.duration = NSDecimalNumber(decimal: duration)
  245. tempTarget.date = date
  246. tempTarget.isUploadedToNS = false
  247. tempTarget.halfBasalTarget = NSDecimalNumber(decimal: halfBasalTarget)
  248. }
  249. private func resetValues() {
  250. name = tempTarget.name ?? ""
  251. target = tempTarget.target?.decimalValue ?? 0
  252. duration = tempTarget.duration?.decimalValue ?? 0
  253. date = tempTarget.date ?? Date()
  254. }
  255. }