EditOverrideForm.swift 21 KB

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