OverrideRootView.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  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: OverrideStored?
  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. }.background(color)
  122. }
  123. @ViewBuilder func overrides() -> some View {
  124. if state.overridePresets.isNotEmpty {
  125. overridePresets
  126. } else {
  127. defaultText
  128. }
  129. if state.isEnabled, state.activeOverrideName.isNotEmpty {
  130. currentActiveOverride
  131. }
  132. if state.overridePresets.isNotEmpty || state.currentActiveOverride != nil {
  133. cancelOverrideButton
  134. }
  135. }
  136. @ViewBuilder func tempTargets() -> some View {
  137. if state.tempTargetPresets.isNotEmpty {
  138. tempTargetPresets
  139. } else {
  140. defaultText
  141. }
  142. //
  143. // if state.isEnabled, state.activeOverrideName.isNotEmpty {
  144. // currentActiveOverride
  145. // }
  146. //
  147. // if state.overridePresets.isNotEmpty || state.currentActiveOverride != nil {
  148. // cancelOverrideButton
  149. // }
  150. }
  151. private var defaultText: some View {
  152. switch state.selectedTab {
  153. case .overrides:
  154. Section {} header: {
  155. Text("Add Preset or Override by tapping 'Add Override +' in the top right-hand corner of the screen.")
  156. .textCase(nil)
  157. .foregroundStyle(.secondary)
  158. }
  159. case .tempTargets:
  160. Section {} header: {
  161. Text(
  162. "Add Preset or Temp Target by tapping 'Add Temp Target +' in the top right-hand corner of the screen."
  163. )
  164. .textCase(nil)
  165. .foregroundStyle(.secondary)
  166. }
  167. }
  168. }
  169. private var overridePresets: some View {
  170. Section {
  171. ForEach(state.overridePresets) { preset in
  172. overridesView(for: preset)
  173. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  174. Button(role: .none) {
  175. Task {
  176. await state.invokeOverridePresetDeletion(preset.objectID)
  177. }
  178. } label: {
  179. Label("Delete", systemImage: "trash")
  180. .tint(.red)
  181. }
  182. Button(action: {
  183. // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
  184. selectedOverride = preset
  185. state.showOverrideEditSheet = true
  186. }, label: {
  187. Label("Edit", systemImage: "pencil")
  188. .tint(.blue)
  189. })
  190. }
  191. }
  192. .onMove(perform: state.reorderOverride)
  193. .listRowBackground(Color.chart)
  194. } header: {
  195. Text("Presets")
  196. } footer: {
  197. HStack {
  198. Image(systemName: "hand.draw.fill")
  199. Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
  200. }
  201. }
  202. }
  203. private var tempTargetPresets: some View {
  204. Section {
  205. ForEach(state.tempTargetPresets) { preset in
  206. tempTargetView(for: preset)
  207. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  208. Button(role: .none) {
  209. Task {
  210. // await state.invokeOverridePresetDeletion(preset.objectID)
  211. }
  212. } label: {
  213. Label("Delete", systemImage: "trash")
  214. .tint(.red)
  215. }
  216. Button(action: {
  217. // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
  218. // selectedOverride = preset
  219. // state.showOverrideEditSheet = true
  220. }, label: {
  221. Label("Edit", systemImage: "pencil")
  222. .tint(.blue)
  223. })
  224. }
  225. }
  226. .onMove(perform: state.reorderOverride)
  227. .listRowBackground(Color.chart)
  228. } header: {
  229. Text("Presets")
  230. } footer: {
  231. HStack {
  232. Image(systemName: "hand.draw.fill")
  233. Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
  234. }
  235. }
  236. }
  237. private var currentActiveOverride: some View {
  238. Section {
  239. HStack {
  240. Text("\(state.activeOverrideName) is running")
  241. Spacer()
  242. Image(systemName: "square.and.pencil")
  243. .foregroundStyle(Color.blue)
  244. }
  245. .contentShape(Rectangle())
  246. .onTapGesture {
  247. Task {
  248. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  249. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  250. await state.duplicateOverridePresetAndCancelPreviousOverride()
  251. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  252. selectedOverride = state.currentActiveOverride
  253. /// Now we can show the Edit sheet
  254. state.showOverrideEditSheet = true
  255. }
  256. }
  257. }
  258. .listRowBackground(Color.blue.opacity(0.2))
  259. }
  260. private var cancelOverrideButton: some View {
  261. switch state.selectedTab {
  262. case .overrides:
  263. Button(action: {
  264. Task {
  265. // Save cancelled Override in OverrideRunStored Entity
  266. // Cancel ALL active Override
  267. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  268. }
  269. }, label: {
  270. Text("Cancel Override")
  271. })
  272. .frame(maxWidth: .infinity, alignment: .center)
  273. .disabled(!state.isEnabled)
  274. .listRowBackground(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
  275. .tint(.white)
  276. case .tempTargets:
  277. Button(action: {
  278. Task {
  279. // Save cancelled Temp Targets in TempTargetRunStored Entity
  280. // Cancel ALL active Temp Targets
  281. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  282. }
  283. }, label: {
  284. Text("Cancel Temp Target")
  285. })
  286. .frame(maxWidth: .infinity, alignment: .center)
  287. .disabled(!state.isTempTargetEnabled)
  288. .listRowBackground(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  289. .tint(.white)
  290. }
  291. }
  292. private func tempTargetView(for preset: TempTargetStored) -> some View {
  293. var target = preset.target
  294. // if state.units == .mmolL {
  295. // target.asMmolL
  296. // }
  297. let isSelected = preset.id?.uuidString == selectedTempTargetPresetID
  298. return ZStack(alignment: .trailing, content: {
  299. HStack {
  300. VStack {
  301. HStack {
  302. Text(preset.name ?? "")
  303. Spacer()
  304. }
  305. HStack(spacing: 2) {
  306. Text(
  307. "\(formatter.string(from: (target ?? 0) as NSNumber)!)"
  308. )
  309. .foregroundColor(.secondary)
  310. .font(.caption)
  311. Text(state.units.rawValue)
  312. .foregroundColor(.secondary)
  313. .font(.caption)
  314. Text("for")
  315. .foregroundColor(.secondary)
  316. .font(.caption)
  317. Text("\(formatter.string(from: (preset.duration ?? 0) as NSNumber)!)")
  318. .foregroundColor(.secondary)
  319. .font(.caption)
  320. Text("min")
  321. .foregroundColor(.secondary)
  322. .font(.caption)
  323. Spacer()
  324. }.padding(.top, 2)
  325. }
  326. .contentShape(Rectangle())
  327. .onTapGesture {
  328. // TODO: - adapt to Temp Targets
  329. // state.enactPreset(id: preset.id)
  330. // selectedPresetID = preset.id
  331. // showCheckmark.toggle()
  332. //
  333. // // deactivate showCheckmark after 3 seconds
  334. // DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  335. // showCheckmark = false
  336. // }
  337. }
  338. Image(systemName: "xmark.circle").foregroundColor(showCheckmark && isSelected ? Color.clear : Color.secondary)
  339. .contentShape(Rectangle())
  340. .padding(.vertical)
  341. .onTapGesture {
  342. removeAlert = Alert(
  343. title: Text("Are you sure?"),
  344. message: Text("Delete preset \"\(preset.name ?? "")\""),
  345. primaryButton: .destructive(Text("Delete"), action: {
  346. // TODO: add deletion for Presets
  347. // state.removePreset(id: preset.id)
  348. }),
  349. secondaryButton: .cancel()
  350. )
  351. isRemoveAlertPresented = true
  352. }
  353. .alert(isPresented: $isRemoveAlertPresented) {
  354. removeAlert!
  355. }
  356. }
  357. if showCheckmark && isSelected {
  358. // show checkmark to indicate if the preset was actually pressed
  359. Image(systemName: "checkmark.circle.fill")
  360. .imageScale(.large)
  361. .fontWeight(.bold)
  362. .foregroundStyle(Color.green)
  363. }
  364. })
  365. }
  366. @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
  367. let target = (state.units == .mgdL ? preset.target : preset.target?.decimalValue.asMmolL as NSDecimalNumber?) ?? 0
  368. let duration = (preset.duration ?? 0) as Decimal
  369. let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
  370. let percent = preset.percentage / 100
  371. let perpetual = preset.indefinite
  372. let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
  373. let scheduledSMBstring = (preset.smbIsOff && preset.smbIsAlwaysOff) ? "Scheduled SMBs" : ""
  374. let smbString = (preset.smbIsOff && scheduledSMBstring == "") ? "SMBs are off" : ""
  375. let targetString = target != 0 ? target.description : ""
  376. let maxMinutesSMB = (preset.smbMinutes as Decimal?) != nil ? (preset.smbMinutes ?? 0) as Decimal : 0
  377. let maxMinutesUAM = (preset.uamMinutes as Decimal?) != nil ? (preset.uamMinutes ?? 0) as Decimal : 0
  378. let isfString = preset.isf ? "ISF" : ""
  379. let crString = preset.cr ? "CR" : ""
  380. let dash = crString != "" ? "/" : ""
  381. let isfAndCRstring = isfString + dash + crString
  382. let isSelected = preset.id == selectedPresetID
  383. if name != "" {
  384. ZStack(alignment: .trailing, content: {
  385. HStack {
  386. VStack {
  387. HStack {
  388. Text(name)
  389. Spacer()
  390. }
  391. HStack(spacing: 5) {
  392. Text(percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
  393. if targetString != "" {
  394. Text(targetString)
  395. Text(targetString != "" ? state.units.rawValue : "")
  396. }
  397. if durationString != "" { Text(durationString + (perpetual ? "" : "min")) }
  398. if smbString != "" { Text(smbString).foregroundColor(.secondary).font(.caption) }
  399. if scheduledSMBstring != "" { Text(scheduledSMBstring) }
  400. if preset.advancedSettings {
  401. Text(maxMinutesSMB == 0 ? "" : maxMinutesSMB.formatted() + " SMB")
  402. Text(maxMinutesUAM == 0 ? "" : maxMinutesUAM.formatted() + " UAM")
  403. Text(isfAndCRstring)
  404. }
  405. Spacer()
  406. }
  407. .padding(.top, 2)
  408. .foregroundColor(.secondary)
  409. .font(.caption)
  410. }
  411. .contentShape(Rectangle())
  412. .onTapGesture {
  413. Task {
  414. let objectID = preset.objectID
  415. await state.enactOverridePreset(withID: objectID)
  416. state.hideModal()
  417. showCheckmark.toggle()
  418. selectedPresetID = preset.id
  419. // deactivate showCheckmark after 3 seconds
  420. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  421. showCheckmark = false
  422. }
  423. }
  424. }
  425. }
  426. // show checkmark to indicate if the preset was actually pressed
  427. if showCheckmark && isSelected {
  428. Image(systemName: "checkmark.circle.fill")
  429. .imageScale(.large)
  430. .fontWeight(.bold)
  431. .foregroundStyle(Color.green)
  432. } else {
  433. Image(systemName: "line.3.horizontal")
  434. .imageScale(.medium)
  435. .foregroundStyle(.secondary)
  436. }
  437. })
  438. }
  439. }
  440. }
  441. }