EditOverrideForm.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  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. @State private var displayPickerStart: Bool = false
  28. @State private var displayPickerEnd: Bool = false
  29. @State private var displayPickerSmbMinutes: Bool = false
  30. @State private var displayPickerUamMinutes: Bool = false
  31. init(overrideToEdit: OverrideStored, state: OverrideConfig.StateModel) {
  32. override = overrideToEdit
  33. _state = StateObject(wrappedValue: state)
  34. _name = State(initialValue: overrideToEdit.name ?? "")
  35. _percentage = State(initialValue: overrideToEdit.percentage)
  36. _indefinite = State(initialValue: overrideToEdit.indefinite)
  37. _duration = State(initialValue: overrideToEdit.duration?.decimalValue ?? 0)
  38. _target = State(
  39. initialValue: state.units == .mgdL ? overrideToEdit.target?.decimalValue : overrideToEdit.target?
  40. .decimalValue.asMmolL
  41. )
  42. _target_override = State(initialValue: overrideToEdit.target?.decimalValue != 0)
  43. _advancedSettings = State(initialValue: overrideToEdit.advancedSettings)
  44. _smbIsOff = State(initialValue: overrideToEdit.smbIsOff)
  45. _smbIsScheduledOff = State(initialValue: overrideToEdit.smbIsScheduledOff)
  46. _start = State(initialValue: overrideToEdit.start?.decimalValue)
  47. _end = State(initialValue: overrideToEdit.end?.decimalValue)
  48. _isfAndCr = State(initialValue: overrideToEdit.isfAndCr)
  49. _isf = State(initialValue: overrideToEdit.isf)
  50. _cr = State(initialValue: overrideToEdit.cr)
  51. _smbMinutes = State(initialValue: overrideToEdit.smbMinutes?.decimalValue)
  52. _uamMinutes = State(initialValue: overrideToEdit.uamMinutes?.decimalValue)
  53. }
  54. var color: LinearGradient {
  55. colorScheme == .dark ? LinearGradient(
  56. gradient: Gradient(colors: [
  57. Color.bgDarkBlue,
  58. Color.bgDarkerDarkBlue
  59. ]),
  60. startPoint: .top,
  61. endPoint: .bottom
  62. ) :
  63. LinearGradient(
  64. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  65. startPoint: .top,
  66. endPoint: .bottom
  67. )
  68. }
  69. private var formatter: NumberFormatter {
  70. let formatter = NumberFormatter()
  71. formatter.numberStyle = .decimal
  72. formatter.maximumFractionDigits = 0
  73. return formatter
  74. }
  75. private var glucoseFormatter: NumberFormatter {
  76. let formatter = NumberFormatter()
  77. formatter.numberStyle = .decimal
  78. formatter.maximumFractionDigits = 0
  79. if state.units == .mmolL {
  80. formatter.maximumFractionDigits = 1
  81. }
  82. formatter.roundingMode = .halfUp
  83. return formatter
  84. }
  85. var body: some View {
  86. NavigationView {
  87. Form {
  88. editOverride()
  89. saveButton
  90. }.scrollContentBackground(.hidden).background(color)
  91. .navigationTitle("Edit Override")
  92. .navigationBarTitleDisplayMode(.inline)
  93. .navigationBarItems(leading: Button("Close") {
  94. presentationMode.wrappedValue.dismiss()
  95. })
  96. .onDisappear {
  97. if !hasChanges {
  98. // Reset UI changes
  99. resetValues()
  100. }
  101. }
  102. .alert(isPresented: $state.showInvalidTargetAlert) {
  103. Alert(
  104. title: Text("Invalid Input"),
  105. message: Text("\(state.alertMessage)"),
  106. dismissButton: .default(Text("OK")) { state.showInvalidTargetAlert = false }
  107. )
  108. }
  109. }
  110. }
  111. @ViewBuilder private func editOverride() -> some View {
  112. if override.name != nil {
  113. Section {
  114. VStack {
  115. TextField("Name", text: $name)
  116. .onChange(of: name) { _ in hasChanges = true }
  117. }
  118. } header: {
  119. Text("Name")
  120. }.listRowBackground(Color.chart)
  121. }
  122. Section {
  123. VStack {
  124. Spacer()
  125. Text("\(percentage.formatted(.number)) %")
  126. .foregroundColor(
  127. state
  128. .overrideSliderPercentage >= 130 ? .red :
  129. (isEditing ? .orange : Color.tabBar)
  130. )
  131. .font(.largeTitle)
  132. Slider(
  133. value: $percentage,
  134. in: 10 ... 200,
  135. step: 1
  136. ).onChange(of: percentage) { _ in hasChanges = true }
  137. Spacer()
  138. Toggle(isOn: $indefinite) {
  139. Text("Enable Indefinitely")
  140. }.onChange(of: indefinite) { _ in hasChanges = true }
  141. }
  142. if !indefinite {
  143. HStack {
  144. Text("Duration")
  145. TextFieldWithToolBar(
  146. text: Binding(
  147. get: { duration },
  148. set: {
  149. duration = $0
  150. hasChanges = true
  151. }
  152. ),
  153. placeholder: "0",
  154. numberFormatter: formatter
  155. )
  156. Text("minutes").foregroundColor(.secondary)
  157. }
  158. }
  159. HStack {
  160. Toggle(isOn: $target_override) {
  161. Text("Override Override Target")
  162. }.onChange(of: target_override) { _ in
  163. hasChanges = true
  164. }
  165. }
  166. if target_override {
  167. HStack {
  168. Text("Target Glucose")
  169. TextFieldWithToolBar(text: Binding(
  170. get: {
  171. target ?? 0
  172. },
  173. set: {
  174. target = $0
  175. hasChanges = true
  176. }
  177. ), placeholder: "0", numberFormatter: glucoseFormatter)
  178. Text(state.units.rawValue).foregroundColor(.secondary)
  179. }
  180. }
  181. Toggle(isOn: $advancedSettings) {
  182. Text("More Options")
  183. }.onChange(of: advancedSettings) { _ in hasChanges = true }
  184. if advancedSettings {
  185. Toggle(
  186. isOn: Binding(
  187. get: { smbIsOff },
  188. set: { newValue in
  189. smbIsOff = newValue
  190. if newValue {
  191. smbIsScheduledOff = false
  192. }
  193. hasChanges = true
  194. }
  195. )
  196. ) {
  197. Text("Disable SMBs")
  198. }
  199. Toggle(
  200. isOn: Binding(
  201. get: { smbIsScheduledOff },
  202. set: { newValue in
  203. smbIsScheduledOff = newValue
  204. if newValue {
  205. smbIsOff = false
  206. }
  207. hasChanges = true
  208. }
  209. )
  210. ) {
  211. Text("Schedule When SMBs Are Disabled")
  212. }
  213. if smbIsScheduledOff {
  214. // First Hour SMBs Are Disabled
  215. VStack {
  216. HStack {
  217. Text("First Hour SMBs Are Disabled")
  218. Spacer()
  219. // Display current selection based on format
  220. Text(
  221. is24HourFormat() ? format24Hour(Int(truncating: start! as NSNumber)) + ":00" :
  222. convertTo12HourFormat(Int(truncating: start! as NSNumber))
  223. )
  224. .foregroundColor(!displayPickerStart ? .primary : .accentColor)
  225. }
  226. .onTapGesture {
  227. displayPickerStart.toggle() // Toggle the picker visibility
  228. }
  229. // Show picker if toggled
  230. if displayPickerStart {
  231. Picker(selection: Binding(
  232. get: { Int(truncating: start! as NSNumber) },
  233. set: {
  234. start = Decimal($0)
  235. hasChanges = true
  236. }
  237. ), label: Text("")) {
  238. if is24HourFormat() {
  239. ForEach(0 ..< 24, id: \.self) { hour in
  240. Text(format24Hour(hour) + ":00").tag(hour)
  241. }
  242. } else {
  243. ForEach(0 ..< 24, id: \.self) { hour in
  244. Text(convertTo12HourFormat(hour)).tag(hour)
  245. }
  246. }
  247. }
  248. .pickerStyle(WheelPickerStyle()) // Use wheel style
  249. .frame(maxWidth: .infinity)
  250. }
  251. }
  252. .padding(.top)
  253. // First Hour SMBs Are Resumed
  254. VStack {
  255. HStack {
  256. Text("First Hour SMBs Are Resumed")
  257. Spacer()
  258. // Display current selection based on format
  259. Text(
  260. is24HourFormat() ? format24Hour(Int(truncating: end! as NSNumber)) + ":00" :
  261. convertTo12HourFormat(Int(truncating: end! as NSNumber))
  262. )
  263. .foregroundColor(!displayPickerEnd ? .primary : .accentColor)
  264. }
  265. .onTapGesture {
  266. displayPickerEnd.toggle() // Toggle the picker visibility
  267. }
  268. // Show picker if toggled
  269. if displayPickerEnd {
  270. Picker(selection: Binding(
  271. get: { Int(truncating: end! as NSNumber) },
  272. set: {
  273. end = Decimal($0)
  274. hasChanges = true
  275. }
  276. ), label: Text("")) {
  277. if is24HourFormat() {
  278. ForEach(0 ..< 24, id: \.self) { hour in
  279. Text(format24Hour(hour) + ":00").tag(hour)
  280. }
  281. } else {
  282. ForEach(0 ..< 24, id: \.self) { hour in
  283. Text(convertTo12HourFormat(hour)).tag(hour)
  284. }
  285. }
  286. }
  287. .pickerStyle(WheelPickerStyle()) // Use wheel style
  288. .frame(maxWidth: .infinity)
  289. }
  290. }
  291. .padding(.top)
  292. }
  293. Toggle(isOn: $isfAndCr) {
  294. Text("Change ISF and CR")
  295. }.onChange(of: isfAndCr) { _ in hasChanges = true }
  296. if !isfAndCr {
  297. Toggle(isOn: $isf) {
  298. Text("Change ISF")
  299. }.onChange(of: isf) { _ in hasChanges = true }
  300. Toggle(isOn: $cr) {
  301. Text("Change CR")
  302. }.onChange(of: cr) { _ in hasChanges = true }
  303. }
  304. if !smbIsOff {
  305. // SMB Minutes Picker
  306. VStack {
  307. HStack {
  308. Text("Max SMB Minutes")
  309. Spacer()
  310. Text("\(smbMinutes?.formatted(.number) ?? "\(state.defaultSmbMinutes)") min")
  311. .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
  312. }
  313. .onTapGesture {
  314. displayPickerSmbMinutes.toggle()
  315. }
  316. if displayPickerSmbMinutes {
  317. Picker(
  318. selection: Binding(
  319. get: { smbMinutes ?? state.defaultSmbMinutes },
  320. set: {
  321. smbMinutes = $0
  322. hasChanges = true
  323. }
  324. ),
  325. label: Text("")
  326. ) {
  327. ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
  328. Text("\(minute) min").tag(Decimal(minute))
  329. }
  330. }
  331. .pickerStyle(WheelPickerStyle())
  332. .frame(maxWidth: .infinity)
  333. }
  334. }
  335. .padding(.top)
  336. // UAM SMB Minutes Picker
  337. VStack {
  338. HStack {
  339. Text("Max UAM SMB Minutes")
  340. Spacer()
  341. Text("\(uamMinutes?.formatted(.number) ?? "\(state.defaultUamMinutes)") min")
  342. .foregroundColor(!displayPickerUamMinutes ? .primary : .accentColor)
  343. }
  344. .onTapGesture {
  345. displayPickerUamMinutes.toggle()
  346. }
  347. if displayPickerUamMinutes {
  348. Picker(
  349. selection: Binding(
  350. get: { uamMinutes ?? state.defaultUamMinutes },
  351. set: {
  352. uamMinutes = $0
  353. hasChanges = true
  354. }
  355. ),
  356. label: Text("")
  357. ) {
  358. ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
  359. Text("\(minute) min").tag(Decimal(minute))
  360. }
  361. }
  362. .pickerStyle(WheelPickerStyle())
  363. .frame(maxWidth: .infinity)
  364. }
  365. }
  366. .padding(.top)
  367. }
  368. }
  369. }.listRowBackground(Color.chart)
  370. }
  371. private var saveButton: some View {
  372. HStack {
  373. Spacer()
  374. Button(action: {
  375. if !state.isInputInvalid(target: target ?? 0) {
  376. saveChanges()
  377. do {
  378. guard let moc = override.managedObjectContext else { return }
  379. guard moc.hasChanges else { return }
  380. try moc.save()
  381. if let currentActiveOverride = state.currentActiveOverride {
  382. Task {
  383. await state.disableAllActiveOverrides(
  384. except: currentActiveOverride.objectID,
  385. createOverrideRunEntry: false
  386. )
  387. }
  388. }
  389. // Update View
  390. state.updateLatestOverrideConfiguration()
  391. hasChanges = false
  392. presentationMode.wrappedValue.dismiss()
  393. } catch {
  394. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
  395. }
  396. }
  397. }, label: {
  398. Text("Save")
  399. })
  400. .disabled(!hasChanges)
  401. .frame(maxWidth: .infinity, alignment: .center)
  402. .tint(.white)
  403. Spacer()
  404. }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4))
  405. }
  406. private func saveChanges() {
  407. if !override.isPreset, hasChanges, name == (override.name ?? "") {
  408. override.name = "Custom Override"
  409. } else {
  410. override.name = name
  411. }
  412. override.percentage = percentage
  413. override.indefinite = indefinite
  414. override.duration = NSDecimalNumber(decimal: duration)
  415. if target_override {
  416. override.target = target.map {
  417. state.units == .mmolL ? NSDecimalNumber(decimal: $0.asMgdL) : NSDecimalNumber(decimal: $0)
  418. }
  419. } else {
  420. override.target = 0
  421. }
  422. override.advancedSettings = advancedSettings
  423. override.smbIsOff = smbIsOff
  424. override.smbIsScheduledOff = smbIsScheduledOff
  425. override.start = start.map { NSDecimalNumber(decimal: $0) }
  426. override.end = end.map { NSDecimalNumber(decimal: $0) }
  427. override.isfAndCr = isfAndCr
  428. override.isf = isf
  429. override.cr = cr
  430. override.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) }
  431. override.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) }
  432. override.isUploadedToNS = false
  433. }
  434. private func resetValues() {
  435. name = override.name ?? ""
  436. percentage = override.percentage
  437. indefinite = override.indefinite
  438. duration = override.duration?.decimalValue ?? 0
  439. target = override.target?.decimalValue
  440. advancedSettings = override.advancedSettings
  441. smbIsOff = override.smbIsOff
  442. smbIsScheduledOff = override.smbIsScheduledOff
  443. start = override.start?.decimalValue
  444. end = override.end?.decimalValue
  445. isfAndCr = override.isfAndCr
  446. isf = override.isf
  447. cr = override.cr
  448. smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
  449. uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
  450. }
  451. }