EditOverrideForm.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  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. @Environment(AppState.self) var appState
  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)
  88. .background(appState.trioBackgroundColor(for: colorScheme))
  89. .navigationTitle("Edit Override")
  90. .navigationBarTitleDisplayMode(.inline)
  91. .navigationBarItems(leading: Button("Close") {
  92. presentationMode.wrappedValue.dismiss()
  93. })
  94. .onDisappear {
  95. if !hasChanges {
  96. // Reset UI changes
  97. resetValues()
  98. }
  99. }
  100. .alert(isPresented: $state.showInvalidTargetAlert) {
  101. Alert(
  102. title: Text("Invalid Input"),
  103. message: Text("\(state.alertMessage)"),
  104. dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false }
  105. )
  106. }
  107. }
  108. }
  109. @ViewBuilder private func editOverride() -> some View {
  110. if override.name != nil {
  111. Section {
  112. VStack {
  113. TextField("Name", text: $name)
  114. .onChange(of: name) { _ in hasChanges = true }
  115. }
  116. } header: {
  117. Text("Name")
  118. }.listRowBackground(Color.chart)
  119. }
  120. Section {
  121. VStack {
  122. Spacer()
  123. Text("\(percentage.formatted(.number)) %")
  124. .foregroundColor(
  125. state
  126. .overrideSliderPercentage >= 130 ? .red :
  127. (isEditing ? .orange : Color.tabBar)
  128. )
  129. .font(.largeTitle)
  130. Slider(
  131. value: $percentage,
  132. in: 10 ... 200,
  133. step: 1
  134. ).onChange(of: percentage) { _ in hasChanges = true }
  135. Spacer()
  136. Toggle(isOn: $indefinite) {
  137. Text("Enable indefinitely")
  138. }.onChange(of: indefinite) { _ in hasChanges = true }
  139. }
  140. if !indefinite {
  141. HStack {
  142. Text("Duration")
  143. TextFieldWithToolBar(
  144. text: Binding(
  145. get: { duration },
  146. set: {
  147. duration = $0
  148. hasChanges = true
  149. }
  150. ),
  151. placeholder: "0",
  152. numberFormatter: formatter
  153. )
  154. Text("minutes").foregroundColor(.secondary)
  155. }
  156. }
  157. HStack {
  158. Toggle(isOn: $target_override) {
  159. Text("Override Override Target")
  160. }.onChange(of: target_override) { _ in
  161. hasChanges = true
  162. }
  163. }
  164. if target_override {
  165. HStack {
  166. Text("Target Glucose")
  167. TextFieldWithToolBar(text: Binding(
  168. get: {
  169. target ?? 0
  170. },
  171. set: {
  172. target = $0
  173. hasChanges = true
  174. }
  175. ), placeholder: "0", numberFormatter: glucoseFormatter)
  176. Text(state.units.rawValue).foregroundColor(.secondary)
  177. }
  178. }
  179. Toggle(isOn: $advancedSettings) {
  180. Text("More options")
  181. }.onChange(of: advancedSettings) { _ in hasChanges = true }
  182. if advancedSettings {
  183. Toggle(isOn: $smbIsOff) {
  184. Text("Disable SMBs")
  185. }.onChange(of: smbIsOff) { _ in hasChanges = true }
  186. Toggle(isOn: $smbIsAlwaysOff) {
  187. Text("Schedule when SMBs are Off")
  188. }.onChange(of: smbIsAlwaysOff) { _ in hasChanges = true }
  189. if smbIsAlwaysOff {
  190. HStack {
  191. Text("First Hour SMBs are Off (24 hours)")
  192. TextFieldWithToolBar(
  193. text: Binding(
  194. get: { start ?? 0 },
  195. set: {
  196. start = $0
  197. hasChanges = true
  198. }
  199. ),
  200. placeholder: "0",
  201. numberFormatter: formatter
  202. )
  203. Text("hour").foregroundColor(.secondary)
  204. }
  205. HStack {
  206. Text("Last Hour SMBs are Off (24 hours)")
  207. TextFieldWithToolBar(
  208. text: Binding(
  209. get: { end ?? 23 },
  210. set: {
  211. end = $0
  212. hasChanges = true
  213. }
  214. ),
  215. placeholder: "0",
  216. numberFormatter: formatter
  217. )
  218. Text("hour").foregroundColor(.secondary)
  219. }
  220. }
  221. Toggle(isOn: $isfAndCr) {
  222. Text("Change ISF and CR")
  223. }.onChange(of: isfAndCr) { _ in hasChanges = true }
  224. if !isfAndCr {
  225. Toggle(isOn: $isf) {
  226. Text("Change ISF")
  227. }.onChange(of: isf) { _ in hasChanges = true }
  228. Toggle(isOn: $cr) {
  229. Text("Change CR")
  230. }.onChange(of: cr) { _ in hasChanges = true }
  231. }
  232. HStack {
  233. Text("SMB Minutes")
  234. TextFieldWithToolBar(
  235. text: Binding(
  236. get: { smbMinutes ?? state.defaultSmbMinutes },
  237. set: {
  238. smbMinutes = $0
  239. hasChanges = true
  240. }
  241. ),
  242. placeholder: "0",
  243. numberFormatter: formatter
  244. )
  245. Text("minutes").foregroundColor(.secondary)
  246. }
  247. HStack {
  248. Text("UAM SMB Minutes")
  249. TextFieldWithToolBar(
  250. text: Binding(
  251. get: { uamMinutes ?? state.defaultUamMinutes },
  252. set: {
  253. uamMinutes = $0
  254. hasChanges = true
  255. }
  256. ),
  257. placeholder: "0",
  258. numberFormatter: formatter
  259. )
  260. Text("minutes").foregroundColor(.secondary)
  261. }
  262. }
  263. }.listRowBackground(Color.chart)
  264. }
  265. private var saveButton: some View {
  266. HStack {
  267. Spacer()
  268. Button(action: {
  269. if !state.isInputInvalid(target: target ?? 0) {
  270. saveChanges()
  271. do {
  272. guard let moc = override.managedObjectContext else { return }
  273. guard moc.hasChanges else { return }
  274. try moc.save()
  275. Task {
  276. await state.nightscoutManager.uploadProfiles()
  277. }
  278. if let currentActiveOverride = state.currentActiveOverride {
  279. Task {
  280. await state.disableAllActiveOverrides(
  281. except: currentActiveOverride.objectID,
  282. createOverrideRunEntry: false
  283. )
  284. }
  285. }
  286. // Update View
  287. state.updateLatestOverrideConfiguration()
  288. hasChanges = false
  289. presentationMode.wrappedValue.dismiss()
  290. } catch {
  291. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
  292. }
  293. }
  294. }, label: {
  295. Text("Save")
  296. })
  297. .disabled(!hasChanges)
  298. .frame(maxWidth: .infinity, alignment: .center)
  299. .tint(.white)
  300. Spacer()
  301. }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4))
  302. }
  303. private func saveChanges() {
  304. if !override.isPreset, hasChanges, name == (override.name ?? "") {
  305. override.name = "Custom Override"
  306. } else {
  307. override.name = name
  308. }
  309. override.percentage = percentage
  310. override.indefinite = indefinite
  311. override.duration = NSDecimalNumber(decimal: duration)
  312. if target_override {
  313. override.target = target.map {
  314. state.units == .mmolL ? NSDecimalNumber(decimal: $0.asMgdL) : NSDecimalNumber(decimal: $0)
  315. }
  316. } else {
  317. override.target = 0
  318. }
  319. override.advancedSettings = advancedSettings
  320. override.smbIsOff = smbIsOff
  321. override.smbIsAlwaysOff = smbIsAlwaysOff
  322. override.start = start.map { NSDecimalNumber(decimal: $0) }
  323. override.end = end.map { NSDecimalNumber(decimal: $0) }
  324. override.isfAndCr = isfAndCr
  325. override.isf = isf
  326. override.cr = cr
  327. override.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) }
  328. override.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) }
  329. override.isUploadedToNS = false
  330. }
  331. private func resetValues() {
  332. name = override.name ?? ""
  333. percentage = override.percentage
  334. indefinite = override.indefinite
  335. duration = override.duration?.decimalValue ?? 0
  336. target = override.target?.decimalValue
  337. advancedSettings = override.advancedSettings
  338. smbIsOff = override.smbIsOff
  339. smbIsAlwaysOff = override.smbIsAlwaysOff
  340. start = override.start?.decimalValue
  341. end = override.end?.decimalValue
  342. isfAndCr = override.isfAndCr
  343. isf = override.isf
  344. cr = override.cr
  345. smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
  346. uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
  347. }
  348. }