EditOverrideForm.swift 14 KB

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