EditOverrideForm.swift 24 KB

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