EditTempTargetForm.swift 12 KB

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