EditOverrideForm.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. import Foundation
  2. import SwiftUI
  3. struct EditOverrideForm: View {
  4. @ObservedObject var override: OverrideStored
  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 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 smbIsScheduledOff: 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 = StateObject(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. _smbIsScheduledOff = State(initialValue: overrideToEdit.smbIsScheduledOff)
  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(
  182. isOn: Binding(
  183. get: { smbIsOff },
  184. set: { newValue in
  185. smbIsOff = newValue
  186. if newValue {
  187. smbIsScheduledOff = false
  188. }
  189. hasChanges = true
  190. }
  191. )
  192. ) {
  193. Text("Disable SMBs")
  194. }
  195. Toggle(
  196. isOn: Binding(
  197. get: { smbIsScheduledOff },
  198. set: { newValue in
  199. smbIsScheduledOff = newValue
  200. if newValue {
  201. smbIsOff = false
  202. }
  203. hasChanges = true
  204. }
  205. )
  206. ) {
  207. Text("Schedule When SMBs Are Disabled")
  208. }
  209. if smbIsScheduledOff {
  210. HStack {
  211. Text("First Hour SMBs Are Disabled (24 hours)")
  212. TextFieldWithToolBar(
  213. text: Binding(
  214. get: { start ?? 0 },
  215. set: {
  216. start = $0
  217. hasChanges = true
  218. }
  219. ),
  220. placeholder: "0",
  221. numberFormatter: formatter
  222. )
  223. Text("hour").foregroundColor(.secondary)
  224. }
  225. HStack {
  226. Text("First Hour SMBs Are Resumed (24 hours)")
  227. TextFieldWithToolBar(
  228. text: Binding(
  229. get: { end ?? 0 },
  230. set: {
  231. end = $0
  232. hasChanges = true
  233. }
  234. ),
  235. placeholder: "0",
  236. numberFormatter: formatter
  237. )
  238. Text("hour").foregroundColor(.secondary)
  239. }
  240. }
  241. Toggle(isOn: $isfAndCr) {
  242. Text("Change ISF and CR")
  243. }.onChange(of: isfAndCr) { _ in hasChanges = true }
  244. if !isfAndCr {
  245. Toggle(isOn: $isf) {
  246. Text("Change ISF")
  247. }.onChange(of: isf) { _ in hasChanges = true }
  248. Toggle(isOn: $cr) {
  249. Text("Change CR")
  250. }.onChange(of: cr) { _ in hasChanges = true }
  251. }
  252. if !smbIsOff {
  253. HStack {
  254. Text("SMB Minutes")
  255. TextFieldWithToolBar(
  256. text: Binding(
  257. get: { smbMinutes ?? state.defaultSmbMinutes },
  258. set: {
  259. smbMinutes = $0
  260. hasChanges = true
  261. }
  262. ),
  263. placeholder: "0",
  264. numberFormatter: formatter
  265. )
  266. Text("minutes").foregroundColor(.secondary)
  267. }
  268. HStack {
  269. Text("UAM SMB Minutes")
  270. TextFieldWithToolBar(
  271. text: Binding(
  272. get: { uamMinutes ?? state.defaultUamMinutes },
  273. set: {
  274. uamMinutes = $0
  275. hasChanges = true
  276. }
  277. ),
  278. placeholder: "0",
  279. numberFormatter: formatter
  280. )
  281. Text("minutes").foregroundColor(.secondary)
  282. }
  283. }
  284. }
  285. }.listRowBackground(Color.chart)
  286. }
  287. private var saveButton: some View {
  288. HStack {
  289. Spacer()
  290. Button(action: {
  291. if !state.isInputInvalid(target: target ?? 0) {
  292. saveChanges()
  293. do {
  294. guard let moc = override.managedObjectContext else { return }
  295. guard moc.hasChanges else { return }
  296. try moc.save()
  297. if let currentActiveOverride = state.currentActiveOverride {
  298. Task {
  299. await state.disableAllActiveOverrides(
  300. except: currentActiveOverride.objectID,
  301. createOverrideRunEntry: false
  302. )
  303. }
  304. }
  305. // Update View
  306. state.updateLatestOverrideConfiguration()
  307. hasChanges = false
  308. presentationMode.wrappedValue.dismiss()
  309. } catch {
  310. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
  311. }
  312. }
  313. }, label: {
  314. Text("Save")
  315. })
  316. .disabled(!hasChanges)
  317. .frame(maxWidth: .infinity, alignment: .center)
  318. .tint(.white)
  319. Spacer()
  320. }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4))
  321. }
  322. private func saveChanges() {
  323. if !override.isPreset, hasChanges, name == (override.name ?? "") {
  324. override.name = "Custom Override"
  325. } else {
  326. override.name = name
  327. }
  328. override.percentage = percentage
  329. override.indefinite = indefinite
  330. override.duration = NSDecimalNumber(decimal: duration)
  331. if target_override {
  332. override.target = target.map {
  333. state.units == .mmolL ? NSDecimalNumber(decimal: $0.asMgdL) : NSDecimalNumber(decimal: $0)
  334. }
  335. } else {
  336. override.target = 0
  337. }
  338. override.advancedSettings = advancedSettings
  339. override.smbIsOff = smbIsOff
  340. override.smbIsScheduledOff = smbIsScheduledOff
  341. override.start = start.map { NSDecimalNumber(decimal: $0) }
  342. override.end = end.map { NSDecimalNumber(decimal: $0) }
  343. override.isfAndCr = isfAndCr
  344. override.isf = isf
  345. override.cr = cr
  346. override.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) }
  347. override.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) }
  348. override.isUploadedToNS = false
  349. }
  350. private func resetValues() {
  351. name = override.name ?? ""
  352. percentage = override.percentage
  353. indefinite = override.indefinite
  354. duration = override.duration?.decimalValue ?? 0
  355. target = override.target?.decimalValue
  356. advancedSettings = override.advancedSettings
  357. smbIsOff = override.smbIsOff
  358. smbIsScheduledOff = override.smbIsScheduledOff
  359. start = override.start?.decimalValue
  360. end = override.end?.decimalValue
  361. isfAndCr = override.isfAndCr
  362. isf = override.isf
  363. cr = override.cr
  364. smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
  365. uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
  366. }
  367. }