OverrideRootView.swift 23 KB

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