EditOverrideForm.swift 23 KB

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