OverrideRootView.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. import CoreData
  2. import SwiftUI
  3. import Swinject
  4. extension OverrideConfig {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. @StateObject var state = StateModel()
  8. @State private var isEditing = false
  9. @State private var showOverrideCreationSheet = false
  10. @State private var showTempTargetCreationSheet = false
  11. @State private var showingDetail = false
  12. @State private var showCheckmark: Bool = false
  13. @State private var selectedPresetID: String?
  14. @State private var selectedTempTargetPresetID: String?
  15. @State private var selectedOverride: OverrideStored?
  16. @State private var selectedTempTarget: TempTargetStored?
  17. // temp targets
  18. @State private var isRemoveAlertPresented = false
  19. @State private var removeAlert: Alert?
  20. @State private var isEditingTT = false
  21. @Environment(\.managedObjectContext) var moc
  22. @Environment(\.colorScheme) var colorScheme
  23. var color: LinearGradient {
  24. colorScheme == .dark ? LinearGradient(
  25. gradient: Gradient(colors: [
  26. Color.bgDarkBlue,
  27. Color.bgDarkerDarkBlue
  28. ]),
  29. startPoint: .top,
  30. endPoint: .bottom
  31. )
  32. :
  33. LinearGradient(
  34. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  35. startPoint: .top,
  36. endPoint: .bottom
  37. )
  38. }
  39. private var formatter: NumberFormatter {
  40. let formatter = NumberFormatter()
  41. formatter.numberStyle = .decimal
  42. formatter.maximumFractionDigits = 0
  43. return formatter
  44. }
  45. private var glucoseFormatter: NumberFormatter {
  46. let formatter = NumberFormatter()
  47. formatter.numberStyle = .decimal
  48. formatter.maximumFractionDigits = 0
  49. if state.units == .mmolL {
  50. formatter.maximumFractionDigits = 1
  51. }
  52. formatter.roundingMode = .halfUp
  53. return formatter
  54. }
  55. var body: some View {
  56. VStack {
  57. Picker("Tab", selection: $state.selectedTab) {
  58. ForEach(Tab.allCases) { tab in
  59. Text(NSLocalizedString(tab.name, comment: "")).tag(tab)
  60. }
  61. }
  62. .pickerStyle(.segmented).padding(.horizontal, 10)
  63. Form {
  64. switch state.selectedTab {
  65. case .overrides: overrides()
  66. case .tempTargets: tempTargets() }
  67. }.scrollContentBackground(.hidden).background(color)
  68. .onAppear(perform: configureView)
  69. .navigationBarTitle("Adjustments")
  70. .navigationBarTitleDisplayMode(.large)
  71. .toolbar {
  72. ToolbarItem(placement: .topBarTrailing) {
  73. switch state.selectedTab {
  74. case .overrides:
  75. Button(action: {
  76. showOverrideCreationSheet = true
  77. }, label: {
  78. HStack {
  79. Text("Add Override")
  80. Image(systemName: "plus")
  81. }
  82. })
  83. case .tempTargets:
  84. Button(action: {
  85. showTempTargetCreationSheet = true
  86. }, label: {
  87. HStack {
  88. Text("Add Temp Target")
  89. Image(systemName: "plus")
  90. }
  91. })
  92. }
  93. }
  94. }
  95. .sheet(isPresented: $state.showOverrideEditSheet, onDismiss: {
  96. Task {
  97. await state.resetStateVariables()
  98. state.showOverrideEditSheet = false
  99. }
  100. }) {
  101. if let override = selectedOverride {
  102. EditOverrideForm(overrideToEdit: override, state: state)
  103. }
  104. }
  105. .sheet(isPresented: $showOverrideCreationSheet, onDismiss: {
  106. Task {
  107. await state.resetStateVariables()
  108. showOverrideCreationSheet = false
  109. }
  110. }) {
  111. AddOverrideForm(state: state)
  112. }
  113. .sheet(isPresented: $showTempTargetCreationSheet, onDismiss: {
  114. Task {
  115. await state.resetStateVariables()
  116. showTempTargetCreationSheet = false
  117. }
  118. }) {
  119. AddTempTargetForm(state: state)
  120. }
  121. .sheet(isPresented: $state.showTempTargetEditSheet, onDismiss: {
  122. Task {
  123. await state.resetTempTargetState()
  124. state.showTempTargetEditSheet = false
  125. }
  126. }) {
  127. if let tempTarget = selectedTempTarget {
  128. EditTempTargetForm(tempTargetToEdit: tempTarget, state: state)
  129. }
  130. }
  131. }.background(color)
  132. }
  133. @ViewBuilder func overrides() -> some View {
  134. if state.overridePresets.isNotEmpty {
  135. overridePresets
  136. } else {
  137. defaultText
  138. }
  139. if state.isEnabled, state.activeOverrideName.isNotEmpty {
  140. currentActiveAdjustment
  141. }
  142. if state.overridePresets.isNotEmpty || state.currentActiveOverride != nil {
  143. cancelAdjustmentButton
  144. }
  145. }
  146. @ViewBuilder func tempTargets() -> some View {
  147. if state.tempTargetPresets.isNotEmpty {
  148. tempTargetPresets
  149. } else {
  150. defaultText
  151. }
  152. if state.isTempTargetEnabled, state.activeTempTargetName.isNotEmpty {
  153. currentActiveAdjustment
  154. }
  155. if state.tempTargetPresets.isNotEmpty || state.currentActiveTempTarget != nil {
  156. cancelAdjustmentButton
  157. }
  158. }
  159. private var defaultText: some View {
  160. switch state.selectedTab {
  161. case .overrides:
  162. Section {} header: {
  163. Text("Add Preset or Override by tapping 'Add Override +' in the top right-hand corner of the screen.")
  164. .textCase(nil)
  165. .foregroundStyle(.secondary)
  166. }
  167. case .tempTargets:
  168. Section {} header: {
  169. Text(
  170. "Add Preset or Temp Target by tapping 'Add Temp Target +' in the top right-hand corner of the screen."
  171. )
  172. .textCase(nil)
  173. .foregroundStyle(.secondary)
  174. }
  175. }
  176. }
  177. private var overridePresets: some View {
  178. Section {
  179. ForEach(state.overridePresets) { preset in
  180. overridesView(for: preset)
  181. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  182. Button(role: .none) {
  183. Task {
  184. await state.invokeOverridePresetDeletion(preset.objectID)
  185. }
  186. } label: {
  187. Label("Delete", systemImage: "trash")
  188. .tint(.red)
  189. }
  190. Button(action: {
  191. // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
  192. selectedOverride = preset
  193. state.showOverrideEditSheet = true
  194. }, label: {
  195. Label("Edit", systemImage: "pencil")
  196. .tint(.blue)
  197. })
  198. }
  199. }
  200. .onMove(perform: state.reorderOverride)
  201. .listRowBackground(Color.chart)
  202. } header: {
  203. Text("Presets")
  204. } footer: {
  205. HStack {
  206. Image(systemName: "hand.draw.fill")
  207. Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
  208. }
  209. }
  210. }
  211. private var tempTargetPresets: some View {
  212. Section {
  213. ForEach(state.tempTargetPresets) { preset in
  214. tempTargetView(for: preset)
  215. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  216. Button(role: .none) {
  217. Task {
  218. await state.invokeTempTargetPresetDeletion(preset.objectID)
  219. }
  220. } label: {
  221. Label("Delete", systemImage: "trash")
  222. .tint(.red)
  223. }
  224. Button(action: {
  225. // Set the selected Temp Target to the chosen Preset and pass it to the Edit Sheet
  226. selectedTempTarget = preset
  227. state.showTempTargetEditSheet = true
  228. }, label: {
  229. Label("Edit", systemImage: "pencil")
  230. .tint(.blue)
  231. })
  232. }
  233. }
  234. // .onMove(perform: state.reorderOverride)
  235. .listRowBackground(Color.chart)
  236. } header: {
  237. Text("Presets")
  238. } footer: {
  239. HStack {
  240. Image(systemName: "hand.draw.fill")
  241. Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
  242. }
  243. }
  244. }
  245. private var currentActiveAdjustment: some View {
  246. switch state.selectedTab {
  247. case .overrides:
  248. Section {
  249. HStack {
  250. Text("\(state.activeOverrideName) is running")
  251. Spacer()
  252. Image(systemName: "square.and.pencil")
  253. .foregroundStyle(Color.blue)
  254. }
  255. .contentShape(Rectangle())
  256. .onTapGesture {
  257. Task {
  258. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  259. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  260. await state.duplicateOverridePresetAndCancelPreviousOverride()
  261. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  262. selectedOverride = state.currentActiveOverride
  263. /// Now we can show the Edit sheet
  264. state.showOverrideEditSheet = true
  265. }
  266. }
  267. }
  268. .listRowBackground(Color.blue.opacity(0.2))
  269. case .tempTargets:
  270. Section {
  271. HStack {
  272. Text("\(state.activeTempTargetName) is running")
  273. Spacer()
  274. Image(systemName: "square.and.pencil")
  275. .foregroundStyle(Color.blue)
  276. }
  277. .contentShape(Rectangle())
  278. .onTapGesture {
  279. Task {
  280. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  281. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  282. await state.duplicateTempTargetPresetAndCancelPreviousTempTarget()
  283. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  284. selectedTempTarget = state.currentActiveTempTarget
  285. /// Now we can show the Edit sheet
  286. state.showTempTargetEditSheet = true
  287. }
  288. }
  289. }
  290. .listRowBackground(Color.blue.opacity(0.2))
  291. }
  292. }
  293. private var cancelAdjustmentButton: some View {
  294. switch state.selectedTab {
  295. case .overrides:
  296. Button(action: {
  297. Task {
  298. // Save cancelled Override in OverrideRunStored Entity
  299. // Cancel ALL active Override
  300. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  301. }
  302. }, label: {
  303. Text("Cancel Override")
  304. })
  305. .frame(maxWidth: .infinity, alignment: .center)
  306. .disabled(!state.isEnabled)
  307. .listRowBackground(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
  308. .tint(.white)
  309. case .tempTargets:
  310. Button(action: {
  311. Task {
  312. // Save cancelled Temp Targets in TempTargetRunStored Entity
  313. // Cancel ALL active Temp Targets
  314. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  315. }
  316. }, label: {
  317. Text("Cancel Temp Target")
  318. })
  319. .frame(maxWidth: .infinity, alignment: .center)
  320. .disabled(!state.isTempTargetEnabled)
  321. .listRowBackground(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  322. .tint(.white)
  323. }
  324. }
  325. private func tempTargetView(for preset: TempTargetStored) -> some View {
  326. var target = preset.target
  327. // if state.units == .mmolL {
  328. // target.asMmolL
  329. // }
  330. let isSelected = preset.id?.uuidString == selectedTempTargetPresetID
  331. return ZStack(alignment: .trailing, content: {
  332. HStack {
  333. VStack {
  334. HStack {
  335. Text(preset.name ?? "")
  336. Spacer()
  337. }
  338. HStack(spacing: 2) {
  339. Text(
  340. "\(formatter.string(from: (target ?? 0) as NSNumber)!)"
  341. )
  342. .foregroundColor(.secondary)
  343. .font(.caption)
  344. Text(state.units.rawValue)
  345. .foregroundColor(.secondary)
  346. .font(.caption)
  347. Text("for")
  348. .foregroundColor(.secondary)
  349. .font(.caption)
  350. Text("\(formatter.string(from: (preset.duration ?? 0) as NSNumber)!)")
  351. .foregroundColor(.secondary)
  352. .font(.caption)
  353. Text("min")
  354. .foregroundColor(.secondary)
  355. .font(.caption)
  356. Spacer()
  357. }.padding(.top, 2)
  358. }
  359. .contentShape(Rectangle())
  360. .onTapGesture {
  361. Task {
  362. let objectID = preset.objectID
  363. await state.enactTempTargetPreset(withID: objectID)
  364. // state.hideModal() // is this needed???
  365. selectedTempTargetPresetID = preset.id?.uuidString
  366. // TODO: - add checkmark here when preset was selected
  367. }
  368. }
  369. Image(systemName: "xmark.circle").foregroundColor(showCheckmark && isSelected ? Color.clear : Color.secondary)
  370. .contentShape(Rectangle())
  371. .padding(.vertical)
  372. .onTapGesture {
  373. removeAlert = Alert(
  374. title: Text("Are you sure?"),
  375. message: Text("Delete preset \"\(preset.name ?? "")\""),
  376. primaryButton: .destructive(Text("Delete"), action: {
  377. // TODO: add deletion for Presets
  378. // state.removePreset(id: preset.id)
  379. }),
  380. secondaryButton: .cancel()
  381. )
  382. isRemoveAlertPresented = true
  383. }
  384. .alert(isPresented: $isRemoveAlertPresented) {
  385. removeAlert!
  386. }
  387. }
  388. if showCheckmark && isSelected {
  389. // show checkmark to indicate if the preset was actually pressed
  390. Image(systemName: "checkmark.circle.fill")
  391. .imageScale(.large)
  392. .fontWeight(.bold)
  393. .foregroundStyle(Color.green)
  394. }
  395. })
  396. }
  397. @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
  398. let target = (state.units == .mgdL ? preset.target : preset.target?.decimalValue.asMmolL as NSDecimalNumber?) ?? 0
  399. let duration = (preset.duration ?? 0) as Decimal
  400. let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
  401. let percent = preset.percentage / 100
  402. let perpetual = preset.indefinite
  403. let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
  404. let scheduledSMBstring = (preset.smbIsOff && preset.smbIsAlwaysOff) ? "Scheduled SMBs" : ""
  405. let smbString = (preset.smbIsOff && scheduledSMBstring == "") ? "SMBs are off" : ""
  406. let targetString = target != 0 ? target.description : ""
  407. let maxMinutesSMB = (preset.smbMinutes as Decimal?) != nil ? (preset.smbMinutes ?? 0) as Decimal : 0
  408. let maxMinutesUAM = (preset.uamMinutes as Decimal?) != nil ? (preset.uamMinutes ?? 0) as Decimal : 0
  409. let isfString = preset.isf ? "ISF" : ""
  410. let crString = preset.cr ? "CR" : ""
  411. let dash = crString != "" ? "/" : ""
  412. let isfAndCRstring = isfString + dash + crString
  413. let isSelected = preset.id == selectedPresetID
  414. if name != "" {
  415. ZStack(alignment: .trailing, content: {
  416. HStack {
  417. VStack {
  418. HStack {
  419. Text(name)
  420. Spacer()
  421. }
  422. HStack(spacing: 5) {
  423. Text(percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
  424. if targetString != "" {
  425. Text(targetString)
  426. Text(targetString != "" ? state.units.rawValue : "")
  427. }
  428. if durationString != "" { Text(durationString + (perpetual ? "" : "min")) }
  429. if smbString != "" { Text(smbString).foregroundColor(.secondary).font(.caption) }
  430. if scheduledSMBstring != "" { Text(scheduledSMBstring) }
  431. if preset.advancedSettings {
  432. Text(maxMinutesSMB == 0 ? "" : maxMinutesSMB.formatted() + " SMB")
  433. Text(maxMinutesUAM == 0 ? "" : maxMinutesUAM.formatted() + " UAM")
  434. Text(isfAndCRstring)
  435. }
  436. Spacer()
  437. }
  438. .padding(.top, 2)
  439. .foregroundColor(.secondary)
  440. .font(.caption)
  441. }
  442. .contentShape(Rectangle())
  443. .onTapGesture {
  444. Task {
  445. let objectID = preset.objectID
  446. await state.enactOverridePreset(withID: objectID)
  447. state.hideModal()
  448. showCheckmark.toggle()
  449. selectedPresetID = preset.id
  450. // deactivate showCheckmark after 3 seconds
  451. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  452. showCheckmark = false
  453. }
  454. }
  455. }
  456. }
  457. // show checkmark to indicate if the preset was actually pressed
  458. if showCheckmark && isSelected {
  459. Image(systemName: "checkmark.circle.fill")
  460. .imageScale(.large)
  461. .fontWeight(.bold)
  462. .foregroundStyle(Color.green)
  463. } else {
  464. Image(systemName: "line.3.horizontal")
  465. .imageScale(.medium)
  466. .foregroundStyle(.secondary)
  467. }
  468. })
  469. }
  470. }
  471. }
  472. }