EditOverrideForm.swift 23 KB

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