EditOverrideForm.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  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. HStack {
  125. Spacer()
  126. // Decrement button
  127. Button(action: {
  128. if percentage > 10 {
  129. percentage -= 1
  130. }
  131. }) {
  132. Image(systemName: "minus.circle.fill")
  133. .font(.title)
  134. .foregroundColor(percentage > 10 ? .accentColor : .loopGray)
  135. }
  136. .buttonStyle(PlainButtonStyle())
  137. Spacer()
  138. Text("\(percentage.formatted(.number)) %")
  139. .foregroundColor(.accentColor)
  140. .font(.largeTitle)
  141. Spacer()
  142. // Increment button
  143. Button(action: {
  144. if percentage < 200 {
  145. percentage += 1
  146. }
  147. }) {
  148. Image(systemName: "plus.circle.fill")
  149. .font(.title)
  150. .foregroundColor(percentage < 200 ? .accentColor : .loopGray)
  151. }
  152. .buttonStyle(PlainButtonStyle())
  153. Spacer()
  154. }
  155. .padding()
  156. Slider(
  157. value: $percentage,
  158. in: 10 ... 200,
  159. step: 1
  160. ).onChange(of: percentage) { _ in hasChanges = true }
  161. Toggle(isOn: $isfAndCr) {
  162. Text("Change ISF and CR")
  163. }.onChange(of: isfAndCr) { _ in hasChanges = true }
  164. if !isfAndCr {
  165. Toggle(isOn: $isf) {
  166. Text("Change ISF")
  167. }.onChange(of: isf) { _ in hasChanges = true }
  168. Toggle(isOn: $cr) {
  169. Text("Change CR")
  170. }.onChange(of: cr) { _ in hasChanges = true }
  171. }
  172. }
  173. VStack {
  174. Toggle(isOn: $indefinite) {
  175. Text("Enable Indefinitely")
  176. }.onChange(of: indefinite) { _ in hasChanges = true }
  177. if !indefinite {
  178. HStack {
  179. Text("Duration")
  180. TextFieldWithToolBar(
  181. text: Binding(
  182. get: { duration },
  183. set: {
  184. duration = $0
  185. hasChanges = true
  186. }
  187. ),
  188. placeholder: "0",
  189. numberFormatter: formatter
  190. )
  191. Text("min").foregroundColor(.secondary)
  192. }
  193. }
  194. }
  195. VStack {
  196. Toggle(isOn: $target_override) {
  197. Text("Override Override Target")
  198. }.onChange(of: target_override) { _ in
  199. hasChanges = true
  200. }
  201. if target_override {
  202. HStack {
  203. Text("Target Glucose")
  204. TextFieldWithToolBar(text: Binding(
  205. get: {
  206. target ?? 0
  207. },
  208. set: {
  209. target = $0
  210. hasChanges = true
  211. }
  212. ), placeholder: "0", numberFormatter: glucoseFormatter)
  213. Text(state.units.rawValue).foregroundColor(.secondary)
  214. }
  215. }
  216. }
  217. Toggle(isOn: $advancedSettings) {
  218. Text("More Options")
  219. }.onChange(of: advancedSettings) { _ in hasChanges = true }
  220. if advancedSettings {
  221. VStack {
  222. Toggle(
  223. isOn: Binding(
  224. get: { smbIsOff },
  225. set: { newValue in
  226. smbIsOff = newValue
  227. if newValue {
  228. smbIsScheduledOff = false
  229. }
  230. hasChanges = true
  231. }
  232. )
  233. ) {
  234. Text("Disable SMBs")
  235. }
  236. Divider()
  237. Toggle(
  238. isOn: Binding(
  239. get: { smbIsScheduledOff },
  240. set: { newValue in
  241. smbIsScheduledOff = newValue
  242. if newValue {
  243. smbIsOff = false
  244. }
  245. hasChanges = true
  246. }
  247. )
  248. ) {
  249. Text("Schedule When SMBs Are Disabled")
  250. }
  251. if smbIsScheduledOff {
  252. // First Hour SMBs Are Disabled
  253. VStack {
  254. HStack {
  255. Text("From")
  256. Spacer()
  257. Text(
  258. is24HourFormat() ? format24Hour(Int(truncating: start! as NSNumber)) + ":00" :
  259. convertTo12HourFormat(Int(truncating: start! as NSNumber))
  260. )
  261. .foregroundColor(!displayPickerStart ? .primary : .accentColor)
  262. }
  263. .onTapGesture {
  264. displayPickerStart.toggle()
  265. }
  266. if displayPickerStart {
  267. Picker(selection: Binding(
  268. get: { Int(truncating: start! as NSNumber) },
  269. set: {
  270. start = Decimal($0)
  271. hasChanges = true
  272. }
  273. ), label: Text("")) {
  274. if is24HourFormat() {
  275. ForEach(0 ..< 24, id: \.self) { hour in
  276. Text(format24Hour(hour) + ":00").tag(hour)
  277. }
  278. } else {
  279. ForEach(0 ..< 24, id: \.self) { hour in
  280. Text(convertTo12HourFormat(hour)).tag(hour)
  281. }
  282. }
  283. }
  284. .pickerStyle(WheelPickerStyle())
  285. .frame(maxWidth: .infinity)
  286. }
  287. }
  288. .padding(.top)
  289. // First Hour SMBs Are Resumed
  290. VStack {
  291. HStack {
  292. Text("To")
  293. Spacer()
  294. Text(
  295. is24HourFormat() ? format24Hour(Int(truncating: end! as NSNumber)) + ":00" :
  296. convertTo12HourFormat(Int(truncating: end! as NSNumber))
  297. )
  298. .foregroundColor(!displayPickerEnd ? .primary : .accentColor)
  299. }
  300. .onTapGesture {
  301. displayPickerEnd.toggle()
  302. }
  303. if displayPickerEnd {
  304. Picker(selection: Binding(
  305. get: { Int(truncating: end! as NSNumber) },
  306. set: {
  307. end = Decimal($0)
  308. hasChanges = true
  309. }
  310. ), label: Text("")) {
  311. if is24HourFormat() {
  312. ForEach(0 ..< 24, id: \.self) { hour in
  313. Text(format24Hour(hour) + ":00").tag(hour)
  314. }
  315. } else {
  316. ForEach(0 ..< 24, id: \.self) { hour in
  317. Text(convertTo12HourFormat(hour)).tag(hour)
  318. }
  319. }
  320. }
  321. .pickerStyle(WheelPickerStyle())
  322. .frame(maxWidth: .infinity)
  323. }
  324. }
  325. .padding(.top)
  326. }
  327. }
  328. if !smbIsOff {
  329. VStack {
  330. // SMB Minutes Picker
  331. VStack {
  332. HStack {
  333. Text("Max SMB Minutes")
  334. Spacer()
  335. Text("\(smbMinutes?.formatted(.number) ?? "\(state.defaultSmbMinutes)") min")
  336. .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
  337. }
  338. .onTapGesture {
  339. displayPickerSmbMinutes.toggle()
  340. }
  341. if displayPickerSmbMinutes {
  342. Picker(
  343. selection: Binding(
  344. get: { smbMinutes ?? state.defaultSmbMinutes },
  345. set: {
  346. smbMinutes = $0
  347. hasChanges = true
  348. }
  349. ),
  350. label: Text("")
  351. ) {
  352. ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
  353. Text("\(minute) min").tag(Decimal(minute))
  354. }
  355. }
  356. .pickerStyle(WheelPickerStyle())
  357. .frame(maxWidth: .infinity)
  358. }
  359. }
  360. .padding(.top)
  361. // UAM SMB Minutes Picker
  362. VStack {
  363. HStack {
  364. Text("Max UAM SMB Minutes")
  365. Spacer()
  366. Text("\(uamMinutes?.formatted(.number) ?? "\(state.defaultUamMinutes)") min")
  367. .foregroundColor(!displayPickerUamMinutes ? .primary : .accentColor)
  368. }
  369. .onTapGesture {
  370. displayPickerUamMinutes.toggle()
  371. }
  372. if displayPickerUamMinutes {
  373. Picker(
  374. selection: Binding(
  375. get: { uamMinutes ?? state.defaultUamMinutes },
  376. set: {
  377. uamMinutes = $0
  378. hasChanges = true
  379. }
  380. ),
  381. label: Text("")
  382. ) {
  383. ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
  384. Text("\(minute) min").tag(Decimal(minute))
  385. }
  386. }
  387. .pickerStyle(WheelPickerStyle())
  388. .frame(maxWidth: .infinity)
  389. }
  390. }
  391. .padding(.top)
  392. }
  393. }
  394. }
  395. }.listRowBackground(Color.chart)
  396. }
  397. private var saveButton: some View {
  398. HStack {
  399. Spacer()
  400. Button(action: {
  401. if !state.isInputInvalid(target: target ?? 0) {
  402. saveChanges()
  403. do {
  404. guard let moc = override.managedObjectContext else { return }
  405. guard moc.hasChanges else { return }
  406. try moc.save()
  407. if let currentActiveOverride = state.currentActiveOverride {
  408. Task {
  409. await state.disableAllActiveOverrides(
  410. except: currentActiveOverride.objectID,
  411. createOverrideRunEntry: false
  412. )
  413. }
  414. }
  415. // Update View
  416. state.updateLatestOverrideConfiguration()
  417. hasChanges = false
  418. presentationMode.wrappedValue.dismiss()
  419. } catch {
  420. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
  421. }
  422. }
  423. }, label: {
  424. Text("Save")
  425. })
  426. .disabled(!hasChanges || (!indefinite && duration == 0))
  427. .frame(maxWidth: .infinity, alignment: .center)
  428. .tint(.white)
  429. Spacer()
  430. }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4))
  431. }
  432. private func saveChanges() {
  433. if !override.isPreset, hasChanges, name == (override.name ?? "") {
  434. override.name = "Custom Override"
  435. } else {
  436. override.name = name
  437. }
  438. override.percentage = percentage
  439. override.indefinite = indefinite
  440. override.duration = NSDecimalNumber(decimal: duration)
  441. if target_override {
  442. override.target = target.map {
  443. state.units == .mmolL ? NSDecimalNumber(decimal: $0.asMgdL) : NSDecimalNumber(decimal: $0)
  444. }
  445. } else {
  446. override.target = 0
  447. }
  448. override.advancedSettings = advancedSettings
  449. override.smbIsOff = smbIsOff
  450. override.smbIsScheduledOff = smbIsScheduledOff
  451. override.start = start.map { NSDecimalNumber(decimal: $0) }
  452. override.end = end.map { NSDecimalNumber(decimal: $0) }
  453. override.isfAndCr = isfAndCr
  454. override.isf = isf
  455. override.cr = cr
  456. override.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) }
  457. override.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) }
  458. override.isUploadedToNS = false
  459. }
  460. private func resetValues() {
  461. name = override.name ?? ""
  462. percentage = override.percentage
  463. indefinite = override.indefinite
  464. duration = override.duration?.decimalValue ?? 0
  465. target = override.target?.decimalValue
  466. advancedSettings = override.advancedSettings
  467. smbIsOff = override.smbIsOff
  468. smbIsScheduledOff = override.smbIsScheduledOff
  469. start = override.start?.decimalValue
  470. end = override.end?.decimalValue
  471. isfAndCr = override.isfAndCr
  472. isf = override.isf
  473. cr = override.cr
  474. smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
  475. uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
  476. }
  477. }