EditOverrideForm.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import Foundation
  2. import SwiftUI
  3. struct EditOverrideForm: View {
  4. var override: OverrideStored
  5. @Environment(\.presentationMode) var presentationMode
  6. @Environment(\.colorScheme) var colorScheme
  7. @Bindable var state: OverrideConfig.StateModel
  8. @State private var name: String
  9. @State private var percentage: Double
  10. @State private var indefinite: Bool
  11. @State private var duration: Decimal
  12. @State private var target: Decimal?
  13. @State private var advancedSettings: Bool
  14. @State private var smbIsOff: Bool
  15. @State private var smbIsAlwaysOff: Bool
  16. @State private var start: Decimal?
  17. @State private var end: Decimal?
  18. @State private var isfAndCr: Bool
  19. @State private var isf: Bool
  20. @State private var cr: Bool
  21. @State private var smbMinutes: Decimal?
  22. @State private var uamMinutes: Decimal?
  23. @State private var hasChanges = false
  24. @State private var isEditing = false
  25. @State private var target_override = false
  26. @State private var showAlert = false
  27. init(overrideToEdit: OverrideStored, state: OverrideConfig.StateModel) {
  28. override = overrideToEdit
  29. _state = Bindable(wrappedValue: state)
  30. _name = State(initialValue: overrideToEdit.name ?? "")
  31. _percentage = State(initialValue: overrideToEdit.percentage)
  32. _indefinite = State(initialValue: overrideToEdit.indefinite)
  33. _duration = State(initialValue: overrideToEdit.duration?.decimalValue ?? 0)
  34. _target = State(
  35. initialValue: state.units == .mgdL ? overrideToEdit.target?.decimalValue : overrideToEdit.target?
  36. .decimalValue.asMmolL
  37. )
  38. _target_override = State(initialValue: overrideToEdit.target?.decimalValue != 0)
  39. _advancedSettings = State(initialValue: overrideToEdit.advancedSettings)
  40. _smbIsOff = State(initialValue: overrideToEdit.smbIsOff)
  41. _smbIsAlwaysOff = State(initialValue: overrideToEdit.smbIsAlwaysOff)
  42. _start = State(initialValue: overrideToEdit.start?.decimalValue)
  43. _end = State(initialValue: overrideToEdit.end?.decimalValue)
  44. _isfAndCr = State(initialValue: overrideToEdit.isfAndCr)
  45. _isf = State(initialValue: overrideToEdit.isf)
  46. _cr = State(initialValue: overrideToEdit.cr)
  47. _smbMinutes = State(initialValue: overrideToEdit.smbMinutes?.decimalValue)
  48. _uamMinutes = State(initialValue: overrideToEdit.uamMinutes?.decimalValue)
  49. }
  50. var color: LinearGradient {
  51. colorScheme == .dark ? LinearGradient(
  52. gradient: Gradient(colors: [
  53. Color.bgDarkBlue,
  54. Color.bgDarkerDarkBlue
  55. ]),
  56. startPoint: .top,
  57. endPoint: .bottom
  58. ) :
  59. LinearGradient(
  60. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  61. startPoint: .top,
  62. endPoint: .bottom
  63. )
  64. }
  65. private var formatter: NumberFormatter {
  66. let formatter = NumberFormatter()
  67. formatter.numberStyle = .decimal
  68. formatter.maximumFractionDigits = 0
  69. return formatter
  70. }
  71. private var glucoseFormatter: NumberFormatter {
  72. let formatter = NumberFormatter()
  73. formatter.numberStyle = .decimal
  74. formatter.maximumFractionDigits = 0
  75. if state.units == .mmolL {
  76. formatter.maximumFractionDigits = 1
  77. }
  78. formatter.roundingMode = .halfUp
  79. return formatter
  80. }
  81. var body: some View {
  82. NavigationView {
  83. Form {
  84. editOverride()
  85. saveButton
  86. }.scrollContentBackground(.hidden).background(color)
  87. .navigationTitle("Edit Override")
  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 editOverride() -> some View {
  108. if override.name != nil {
  109. Section {
  110. VStack {
  111. TextField("Name", text: $name)
  112. .onChange(of: name) { _ in hasChanges = true }
  113. }
  114. } header: {
  115. Text("Name")
  116. }.listRowBackground(Color.chart)
  117. }
  118. Section {
  119. VStack {
  120. Spacer()
  121. Text("\(percentage.formatted(.number)) %")
  122. .foregroundColor(
  123. state
  124. .overrideSliderPercentage >= 130 ? .red :
  125. (isEditing ? .orange : Color.tabBar)
  126. )
  127. .font(.largeTitle)
  128. Slider(
  129. value: $percentage,
  130. in: 10 ... 200,
  131. step: 1
  132. ).onChange(of: percentage) { _ in hasChanges = true }
  133. Spacer()
  134. Toggle(isOn: $indefinite) {
  135. Text("Enable indefinitely")
  136. }.onChange(of: indefinite) { _ in hasChanges = true }
  137. }
  138. if !indefinite {
  139. HStack {
  140. Text("Duration")
  141. TextFieldWithToolBar(
  142. text: Binding(
  143. get: { duration },
  144. set: {
  145. duration = $0
  146. hasChanges = true
  147. }
  148. ),
  149. placeholder: "0",
  150. numberFormatter: formatter
  151. )
  152. Text("minutes").foregroundColor(.secondary)
  153. }
  154. }
  155. HStack {
  156. Toggle(isOn: $target_override) {
  157. Text("Override Override Target")
  158. }.onChange(of: target_override) { _ in
  159. hasChanges = true
  160. }
  161. }
  162. if target_override {
  163. HStack {
  164. Text("Target Glucose")
  165. TextFieldWithToolBar(text: Binding(
  166. get: {
  167. target ?? 0
  168. },
  169. set: {
  170. target = $0
  171. hasChanges = true
  172. }
  173. ), placeholder: "0", numberFormatter: glucoseFormatter)
  174. Text(state.units.rawValue).foregroundColor(.secondary)
  175. }
  176. }
  177. Toggle(isOn: $advancedSettings) {
  178. Text("More options")
  179. }.onChange(of: advancedSettings) { _ in hasChanges = true }
  180. if advancedSettings {
  181. Toggle(isOn: $smbIsOff) {
  182. Text("Disable SMBs")
  183. }.onChange(of: smbIsOff) { _ in hasChanges = true }
  184. Toggle(isOn: $smbIsAlwaysOff) {
  185. Text("Schedule when SMBs are Off")
  186. }.onChange(of: smbIsAlwaysOff) { _ in hasChanges = true }
  187. if smbIsAlwaysOff {
  188. HStack {
  189. Text("First Hour SMBs are Off (24 hours)")
  190. TextFieldWithToolBar(
  191. text: Binding(
  192. get: { start ?? 0 },
  193. set: {
  194. start = $0
  195. hasChanges = true
  196. }
  197. ),
  198. placeholder: "0",
  199. numberFormatter: formatter
  200. )
  201. Text("hour").foregroundColor(.secondary)
  202. }
  203. HStack {
  204. Text("Last Hour SMBs are Off (24 hours)")
  205. TextFieldWithToolBar(
  206. text: Binding(
  207. get: { end ?? 23 },
  208. set: {
  209. end = $0
  210. hasChanges = true
  211. }
  212. ),
  213. placeholder: "0",
  214. numberFormatter: formatter
  215. )
  216. Text("hour").foregroundColor(.secondary)
  217. }
  218. }
  219. Toggle(isOn: $isfAndCr) {
  220. Text("Change ISF and CR")
  221. }.onChange(of: isfAndCr) { _ in hasChanges = true }
  222. if !isfAndCr {
  223. Toggle(isOn: $isf) {
  224. Text("Change ISF")
  225. }.onChange(of: isf) { _ in hasChanges = true }
  226. Toggle(isOn: $cr) {
  227. Text("Change CR")
  228. }.onChange(of: cr) { _ in hasChanges = true }
  229. }
  230. HStack {
  231. Text("SMB Minutes")
  232. TextFieldWithToolBar(
  233. text: Binding(
  234. get: { smbMinutes ?? state.defaultSmbMinutes },
  235. set: {
  236. smbMinutes = $0
  237. hasChanges = true
  238. }
  239. ),
  240. placeholder: "0",
  241. numberFormatter: formatter
  242. )
  243. Text("minutes").foregroundColor(.secondary)
  244. }
  245. HStack {
  246. Text("UAM SMB Minutes")
  247. TextFieldWithToolBar(
  248. text: Binding(
  249. get: { uamMinutes ?? state.defaultUamMinutes },
  250. set: {
  251. uamMinutes = $0
  252. hasChanges = true
  253. }
  254. ),
  255. placeholder: "0",
  256. numberFormatter: formatter
  257. )
  258. Text("minutes").foregroundColor(.secondary)
  259. }
  260. }
  261. }.listRowBackground(Color.chart)
  262. }
  263. private var saveButton: some View {
  264. HStack {
  265. Spacer()
  266. Button(action: {
  267. if !state.isInputInvalid(target: target ?? 0) {
  268. saveChanges()
  269. do {
  270. guard let moc = override.managedObjectContext else { return }
  271. guard moc.hasChanges else { return }
  272. try moc.save()
  273. Task {
  274. await state.nightscoutManager.uploadProfiles()
  275. }
  276. if let currentActiveOverride = state.currentActiveOverride {
  277. Task {
  278. await state.disableAllActiveOverrides(
  279. except: currentActiveOverride.objectID,
  280. createOverrideRunEntry: false
  281. )
  282. }
  283. }
  284. // Update View
  285. state.updateLatestOverrideConfiguration()
  286. hasChanges = false
  287. presentationMode.wrappedValue.dismiss()
  288. } catch {
  289. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
  290. }
  291. }
  292. }, label: {
  293. Text("Save")
  294. })
  295. .disabled(!hasChanges)
  296. .frame(maxWidth: .infinity, alignment: .center)
  297. .tint(.white)
  298. Spacer()
  299. }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4))
  300. }
  301. private func saveChanges() {
  302. if !override.isPreset, hasChanges, name == (override.name ?? "") {
  303. override.name = "Custom Override"
  304. } else {
  305. override.name = name
  306. }
  307. override.percentage = percentage
  308. override.indefinite = indefinite
  309. override.duration = NSDecimalNumber(decimal: duration)
  310. if target_override {
  311. override.target = target.map {
  312. state.units == .mmolL ? NSDecimalNumber(decimal: $0.asMgdL) : NSDecimalNumber(decimal: $0)
  313. }
  314. } else {
  315. override.target = 0
  316. }
  317. override.advancedSettings = advancedSettings
  318. override.smbIsOff = smbIsOff
  319. override.smbIsAlwaysOff = smbIsAlwaysOff
  320. override.start = start.map { NSDecimalNumber(decimal: $0) }
  321. override.end = end.map { NSDecimalNumber(decimal: $0) }
  322. override.isfAndCr = isfAndCr
  323. override.isf = isf
  324. override.cr = cr
  325. override.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) }
  326. override.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) }
  327. override.isUploadedToNS = false
  328. }
  329. private func resetValues() {
  330. name = override.name ?? ""
  331. percentage = override.percentage
  332. indefinite = override.indefinite
  333. duration = override.duration?.decimalValue ?? 0
  334. target = override.target?.decimalValue
  335. advancedSettings = override.advancedSettings
  336. smbIsOff = override.smbIsOff
  337. smbIsAlwaysOff = override.smbIsAlwaysOff
  338. start = override.start?.decimalValue
  339. end = override.end?.decimalValue
  340. isfAndCr = override.isfAndCr
  341. isf = override.isf
  342. cr = override.cr
  343. smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
  344. uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
  345. }
  346. }