AdjustmentsRootView.swift 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864
  1. import CoreData
  2. import SwiftUI
  3. import Swinject
  4. extension OverrideConfig {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. @State 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 isConfirmDeletePresented = false
  19. @State private var isPromptPresented = false
  20. @State private var isRemoveAlertPresented = false
  21. @State private var removeAlert: Alert?
  22. @State private var isEditingTT = false
  23. @Environment(\.managedObjectContext) var moc
  24. @Environment(\.colorScheme) var colorScheme
  25. var color: LinearGradient {
  26. colorScheme == .dark ? LinearGradient(
  27. gradient: Gradient(colors: [
  28. Color.bgDarkBlue,
  29. Color.bgDarkerDarkBlue
  30. ]),
  31. startPoint: .top,
  32. endPoint: .bottom
  33. )
  34. :
  35. LinearGradient(
  36. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  37. startPoint: .top,
  38. endPoint: .bottom
  39. )
  40. }
  41. private var formatter: NumberFormatter {
  42. let formatter = NumberFormatter()
  43. formatter.numberStyle = .decimal
  44. formatter.maximumFractionDigits = 0
  45. return formatter
  46. }
  47. private var glucoseFormatter: NumberFormatter {
  48. let formatter = NumberFormatter()
  49. formatter.numberStyle = .decimal
  50. formatter.maximumFractionDigits = 0
  51. if state.units == .mmolL {
  52. formatter.maximumFractionDigits = 1
  53. }
  54. formatter.roundingMode = .halfUp
  55. return formatter
  56. }
  57. private func formattedGlucose(glucose: Decimal) -> String {
  58. let formattedValue: String
  59. if state.units == .mgdL {
  60. formattedValue = glucoseFormatter.string(from: glucose as NSDecimalNumber) ?? "\(glucose)"
  61. } else {
  62. formattedValue = glucose.formattedAsMmolL
  63. }
  64. return "\(formattedValue) \(state.units.rawValue)"
  65. }
  66. var body: some View {
  67. VStack {
  68. HStack(spacing: 6) {
  69. HStack {
  70. Spacer()
  71. Image(systemName: "clock.arrow.2.circlepath")
  72. .font(.system(size: 20))
  73. .foregroundStyle(Color.primary, Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569))
  74. Text(OverrideConfig.Tab.overrides.name)
  75. .font(.subheadline)
  76. .lineLimit(1)
  77. .minimumScaleFactor(0.8)
  78. Spacer()
  79. }
  80. .padding(.vertical, 6)
  81. .background(state.selectedTab == .overrides ? Color.loopGray.opacity(0.4) : Color.clear)
  82. .cornerRadius(8)
  83. .onTapGesture {
  84. withAnimation {
  85. state.selectedTab = .overrides
  86. }
  87. }
  88. HStack {
  89. Spacer()
  90. Image(systemName: "target")
  91. .font(.system(size: 20))
  92. .foregroundStyle(Color.loopGreen)
  93. Text(OverrideConfig.Tab.tempTargets.name)
  94. .font(.subheadline)
  95. .lineLimit(1)
  96. .minimumScaleFactor(0.8)
  97. Spacer()
  98. }
  99. .padding(.vertical, 6)
  100. .background(state.selectedTab == .tempTargets ? Color.loopGray.opacity(0.4) : Color.clear)
  101. .cornerRadius(8)
  102. .onTapGesture {
  103. withAnimation {
  104. state.selectedTab = .tempTargets
  105. }
  106. }
  107. }
  108. .background(Color.gray.opacity(0.2))
  109. .cornerRadius(8)
  110. .padding(.horizontal)
  111. List {
  112. switch state.selectedTab {
  113. case .overrides: overrides()
  114. case .tempTargets: tempTargets() }
  115. }
  116. .listSectionSpacing(10)
  117. .safeAreaInset(edge: .bottom, spacing: 30) { stickyStopButton }
  118. .scrollContentBackground(.hidden).background(color)
  119. .onAppear(perform: configureView)
  120. .navigationBarTitle("Adjustments")
  121. .navigationBarTitleDisplayMode(.large)
  122. .toolbar {
  123. ToolbarItem(placement: .topBarTrailing) {
  124. switch state.selectedTab {
  125. case .overrides:
  126. Button(action: {
  127. showOverrideCreationSheet = true
  128. }, label: {
  129. HStack {
  130. Text("Add Override")
  131. Image(systemName: "plus")
  132. }
  133. })
  134. case .tempTargets:
  135. Button(action: {
  136. showTempTargetCreationSheet = true
  137. }, label: {
  138. HStack {
  139. Text("Add Temp Target")
  140. Image(systemName: "plus")
  141. }
  142. })
  143. }
  144. }
  145. }
  146. .sheet(isPresented: $state.showOverrideEditSheet, onDismiss: {
  147. Task {
  148. await state.resetStateVariables()
  149. state.showOverrideEditSheet = false
  150. }
  151. }) {
  152. if let override = selectedOverride {
  153. EditOverrideForm(overrideToEdit: override, state: state)
  154. }
  155. }
  156. .sheet(isPresented: $showOverrideCreationSheet, onDismiss: {
  157. Task {
  158. await state.resetStateVariables()
  159. showOverrideCreationSheet = false
  160. }
  161. }) {
  162. AddOverrideForm(state: state)
  163. }
  164. .sheet(isPresented: $showTempTargetCreationSheet, onDismiss: {
  165. Task {
  166. await state.resetTempTargetState()
  167. showTempTargetCreationSheet = false
  168. }
  169. }) {
  170. AddTempTargetForm(state: state)
  171. }
  172. .sheet(isPresented: $state.showTempTargetEditSheet, onDismiss: {
  173. Task {
  174. await state.resetTempTargetState()
  175. state.showTempTargetEditSheet = false
  176. }
  177. }) {
  178. if let tempTarget = selectedTempTarget {
  179. EditTempTargetForm(tempTargetToEdit: tempTarget, state: state)
  180. }
  181. }
  182. }.background(color)
  183. }
  184. @ViewBuilder func overrides() -> some View {
  185. if state.isEnabled, state.activeOverrideName.isNotEmpty {
  186. currentActiveAdjustment
  187. }
  188. if state.overridePresets.isNotEmpty {
  189. overridePresets
  190. } else {
  191. defaultText
  192. }
  193. }
  194. @ViewBuilder func tempTargets() -> some View {
  195. if state.isTempTargetEnabled, state.activeTempTargetName.isNotEmpty {
  196. currentActiveAdjustment
  197. }
  198. if state.scheduledTempTargets.isNotEmpty {
  199. scheduledTempTargets
  200. }
  201. if state.tempTargetPresets.isNotEmpty {
  202. tempTargetPresets
  203. } else {
  204. defaultText
  205. }
  206. }
  207. private var defaultText: some View {
  208. switch state.selectedTab {
  209. case .overrides:
  210. Section {} header: {
  211. Text("Add Preset or Override by tapping 'Add Override +' in the top right-hand corner of the screen.")
  212. .textCase(nil)
  213. .foregroundStyle(.secondary)
  214. }
  215. case .tempTargets:
  216. Section {} header: {
  217. Text(
  218. "Add Preset or Temp Target by tapping 'Add Temp Target +' in the top right-hand corner of the screen."
  219. )
  220. .textCase(nil)
  221. .foregroundStyle(.secondary)
  222. }
  223. }
  224. }
  225. private var overridePresets: some View {
  226. Section {
  227. ForEach(state.overridePresets) { preset in
  228. overridesView(for: preset)
  229. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  230. Button(role: .none) {
  231. selectedOverride = preset
  232. isConfirmDeletePresented = true
  233. } label: {
  234. Label("Delete", systemImage: "trash")
  235. .tint(.red)
  236. }
  237. Button(action: {
  238. // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
  239. selectedOverride = preset
  240. state.showOverrideEditSheet = true
  241. }, label: {
  242. Label("Edit", systemImage: "pencil")
  243. .tint(.blue)
  244. })
  245. }
  246. }
  247. .onMove(perform: state.reorderOverride)
  248. .confirmationDialog(
  249. "Delete the Override Preset \"\(selectedOverride?.name ?? "")\"?",
  250. isPresented: $isConfirmDeletePresented,
  251. titleVisibility: .visible
  252. ) {
  253. if let itemToDelete = selectedOverride {
  254. Button(
  255. state.currentActiveOverride == selectedOverride ? "Stop and Delete" : "Delete",
  256. role: .destructive
  257. ) {
  258. if state.currentActiveOverride == selectedOverride {
  259. Task {
  260. // Save cancelled Override in OverrideRunStored Entity
  261. // Cancel ALL active Override
  262. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  263. }
  264. }
  265. // Perform the delete action
  266. Task {
  267. await state.invokeOverridePresetDeletion(itemToDelete.objectID)
  268. }
  269. // Reset the selected item after deletion
  270. selectedOverride = nil
  271. }
  272. }
  273. Button("Cancel", role: .cancel) {
  274. // Dismiss the dialog without action
  275. selectedOverride = nil
  276. }
  277. } message: {
  278. if state.currentActiveOverride == selectedOverride {
  279. Text(
  280. state
  281. .currentActiveOverride == selectedOverride ?
  282. "This override preset is currently running. Deleting will stop it." : ""
  283. )
  284. }
  285. }
  286. .listRowBackground(Color.chart)
  287. } header: {
  288. Text("Presets")
  289. } footer: {
  290. HStack {
  291. Image(systemName: "hand.draw.fill")
  292. Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
  293. }
  294. }
  295. }
  296. private var scheduledTempTargets: some View {
  297. Section {
  298. ForEach(state.scheduledTempTargets) { tt in
  299. tempTargetView(for: tt)
  300. }
  301. .listRowBackground(Color.chart)
  302. } header: {
  303. Text("Scheduled Temp Targets")
  304. }
  305. }
  306. private var tempTargetPresets: some View {
  307. Section {
  308. ForEach(state.tempTargetPresets) { preset in
  309. tempTargetView(for: preset, showCheckmark: showCheckmark) {
  310. enactTempTargetPreset(preset)
  311. }
  312. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  313. swipeActions(for: preset)
  314. }
  315. }
  316. .onMove(perform: state.reorderTempTargets)
  317. .confirmationDialog(
  318. deleteConfirmationTitle,
  319. isPresented: $isConfirmDeletePresented,
  320. titleVisibility: .visible
  321. ) {
  322. deleteConfirmationButtons()
  323. } message: {
  324. deleteConfirmationMessage
  325. }
  326. .listRowBackground(Color.chart)
  327. } header: {
  328. Text("Presets")
  329. } footer: {
  330. HStack {
  331. Image(systemName: "hand.draw.fill")
  332. Text("Swipe left to edit or delete a Temp Target preset. Hold, drag and drop to reorder a preset.")
  333. }
  334. }
  335. }
  336. private func enactTempTargetPreset(_ preset: TempTargetStored) {
  337. Task {
  338. let objectID = preset.objectID
  339. await state.enactTempTargetPreset(withID: objectID)
  340. selectedTempTargetPresetID = preset.id?.uuidString
  341. showCheckmark.toggle()
  342. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  343. showCheckmark = false
  344. }
  345. }
  346. }
  347. private func swipeActions(for preset: TempTargetStored) -> some View {
  348. Group {
  349. Button(role: .destructive) {
  350. Task {
  351. selectedTempTarget = preset
  352. isConfirmDeletePresented = true
  353. }
  354. } label: {
  355. Label("Delete", systemImage: "trash")
  356. .tint(.red)
  357. }
  358. Button(action: {
  359. selectedTempTarget = preset
  360. state.showTempTargetEditSheet = true
  361. }, label: {
  362. Label("Edit", systemImage: "pencil")
  363. .tint(.blue)
  364. })
  365. }
  366. }
  367. private var deleteConfirmationTitle: String {
  368. "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?"
  369. }
  370. private func deleteConfirmationButtons() -> some View {
  371. Group {
  372. if let itemToDelete = selectedTempTarget {
  373. Button(
  374. state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
  375. role: .destructive
  376. ) {
  377. if state.currentActiveTempTarget == selectedTempTarget {
  378. Task {
  379. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  380. }
  381. }
  382. Task {
  383. await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
  384. }
  385. selectedTempTarget = nil
  386. }
  387. }
  388. Button("Cancel", role: .cancel) {
  389. selectedTempTarget = nil
  390. }
  391. }
  392. }
  393. private var deleteConfirmationMessage: Text? {
  394. if state.currentActiveTempTarget == selectedTempTarget {
  395. return Text("This Temp Target preset is currently running. Deleting will stop it.")
  396. }
  397. return nil
  398. }
  399. private var currentActiveAdjustment: some View {
  400. switch state.selectedTab {
  401. case .overrides:
  402. Section {
  403. HStack {
  404. Text("\(state.activeOverrideName) is running")
  405. Spacer()
  406. Image(systemName: "square.and.pencil")
  407. .foregroundStyle(Color.blue)
  408. }
  409. .contentShape(Rectangle())
  410. .onTapGesture {
  411. Task {
  412. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  413. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  414. await state.duplicateOverridePresetAndCancelPreviousOverride()
  415. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  416. selectedOverride = state.currentActiveOverride
  417. /// Now we can show the Edit sheet
  418. state.showOverrideEditSheet = true
  419. }
  420. }
  421. }
  422. .listRowBackground(Color.blue.opacity(0.2))
  423. case .tempTargets:
  424. Section {
  425. HStack {
  426. Text("\(state.activeTempTargetName) is running")
  427. Spacer()
  428. Image(systemName: "square.and.pencil")
  429. .foregroundStyle(Color.blue)
  430. }
  431. .contentShape(Rectangle())
  432. .onTapGesture {
  433. Task {
  434. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  435. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  436. await state.duplicateTempTargetPresetAndCancelPreviousTempTarget()
  437. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  438. selectedTempTarget = state.currentActiveTempTarget
  439. /// Now we can show the Edit sheet
  440. state.showTempTargetEditSheet = true
  441. }
  442. }
  443. }
  444. .listRowBackground(Color.blue.opacity(0.2))
  445. }
  446. }
  447. var stickyStopButton: some View {
  448. ZStack {
  449. Rectangle()
  450. .frame(width: UIScreen.main.bounds.width, height: 65)
  451. .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
  452. .background(.thinMaterial)
  453. .opacity(0.8)
  454. .clipShape(Rectangle())
  455. Group {
  456. switch state.selectedTab {
  457. case .overrides:
  458. Button(action: {
  459. Task {
  460. // Save cancelled Override in OverrideRunStored Entity
  461. // Cancel ALL active Override
  462. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  463. }
  464. }, label: {
  465. Text("Stop Override")
  466. .padding(10)
  467. })
  468. .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
  469. .disabled(!state.isEnabled)
  470. .background(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
  471. .tint(.white)
  472. .clipShape(RoundedRectangle(cornerRadius: 8))
  473. case .tempTargets:
  474. Button(action: {
  475. Task {
  476. // Save cancelled Temp Targets in TempTargetRunStored Entity
  477. // Cancel ALL active Temp Targets
  478. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  479. // Update View
  480. state.updateLatestTempTargetConfiguration()
  481. }
  482. }, label: {
  483. Text("Stop Temp Target")
  484. .padding(10)
  485. })
  486. .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
  487. .disabled(!state.isTempTargetEnabled)
  488. .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  489. .tint(.white)
  490. .clipShape(RoundedRectangle(cornerRadius: 8))
  491. }
  492. }.padding(5)
  493. }
  494. }
  495. private var cancelAdjustmentButton: some View {
  496. switch state.selectedTab {
  497. case .overrides:
  498. Button(action: {
  499. Task {
  500. // Save cancelled Override in OverrideRunStored Entity
  501. // Cancel ALL active Override
  502. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  503. }
  504. }, label: {
  505. Text("Stop Override")
  506. })
  507. .frame(maxWidth: .infinity, alignment: .center)
  508. .disabled(!state.isEnabled)
  509. .listRowBackground(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
  510. .tint(.white)
  511. case .tempTargets:
  512. Button(action: {
  513. Task {
  514. // Save cancelled Temp Targets in TempTargetRunStored Entity
  515. // Cancel ALL active Temp Targets
  516. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  517. // Update View
  518. state.updateLatestTempTargetConfiguration()
  519. }
  520. }, label: {
  521. Text("Stop Temp Target")
  522. })
  523. .frame(maxWidth: .infinity, alignment: .center)
  524. .disabled(!state.isTempTargetEnabled)
  525. .listRowBackground(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  526. .tint(.white)
  527. }
  528. }
  529. private func tempTargetView(
  530. for preset: TempTargetStored,
  531. showCheckmark: Bool = false,
  532. onTap: (() -> Void)? = nil
  533. ) -> some View {
  534. let target = preset.target ?? 100
  535. let presetTarget = Decimal(target as! Double.RawValue)
  536. let isSelected = preset.id?.uuidString == selectedTempTargetPresetID
  537. let presetHalfBasalTarget = Decimal(
  538. preset.halfBasalTarget as? Double
  539. .RawValue ?? Double(state.settingHalfBasalTarget)
  540. )
  541. let percentage = Int(
  542. state.computeAdjustedPercentage(usingHBT: presetHalfBasalTarget, usingTarget: presetTarget)
  543. )
  544. return ZStack(alignment: .trailing) {
  545. HStack {
  546. VStack {
  547. HStack {
  548. Text(preset.name ?? "")
  549. Spacer()
  550. }
  551. HStack(spacing: 2) {
  552. Text(formattedGlucose(glucose: target as Decimal))
  553. .foregroundColor(.secondary)
  554. .font(.caption)
  555. Text("for")
  556. .foregroundColor(.secondary)
  557. .font(.caption)
  558. Text("\(formatter.string(from: (preset.duration ?? 0) as NSNumber)!)")
  559. .foregroundColor(.secondary)
  560. .font(.caption)
  561. Text("min")
  562. .foregroundColor(.secondary)
  563. .font(.caption)
  564. if state.isAdjustSensEnabled(usingTarget: presetTarget) {
  565. Text(", \(percentage)%")
  566. .foregroundColor(.secondary)
  567. .font(.caption)
  568. }
  569. Spacer()
  570. }
  571. .padding(.top, 2)
  572. }
  573. .contentShape(Rectangle())
  574. .onTapGesture {
  575. onTap?()
  576. }
  577. }
  578. if showCheckmark && isSelected {
  579. Image(systemName: "checkmark.circle.fill")
  580. .imageScale(.large)
  581. .fontWeight(.bold)
  582. .foregroundStyle(Color.green)
  583. } else if onTap != nil {
  584. Image(systemName: "line.3.horizontal")
  585. .imageScale(.medium)
  586. .foregroundStyle(.secondary)
  587. }
  588. }
  589. }
  590. // private func tempTargetView(for preset: TempTargetStored) -> some View {
  591. // let target = preset.target ?? 100
  592. // let presetTarget = Decimal(target as! Double.RawValue)
  593. // let isSelected = preset.id?.uuidString == selectedTempTargetPresetID
  594. // let presetHalfBasalTarget = Decimal(
  595. // preset.halfBasalTarget as? Double
  596. // .RawValue ?? Double(state.settingHalfBasalTarget)
  597. // )
  598. // let percentage = Int(
  599. // state.computeAdjustedPercentage(usingHBT: presetHalfBasalTarget, usingTarget: presetTarget)
  600. // )
  601. //
  602. // return ZStack(alignment: .trailing, content: {
  603. // HStack {
  604. // VStack {
  605. // HStack {
  606. // Text(preset.name ?? "")
  607. // Spacer()
  608. // }
  609. // HStack(spacing: 2) {
  610. // Text(formattedGlucose(glucose: target as Decimal))
  611. // .foregroundColor(.secondary)
  612. // .font(.caption)
  613. // Text("for")
  614. // .foregroundColor(.secondary)
  615. // .font(.caption)
  616. // Text("\(formatter.string(from: (preset.duration ?? 0) as NSNumber)!)")
  617. // .foregroundColor(.secondary)
  618. // .font(.caption)
  619. // Text("min")
  620. // .foregroundColor(.secondary)
  621. // .font(.caption)
  622. // if state.isAdjustSensEnabled(usingTarget: presetTarget) { Text(", \(percentage)%")
  623. // .foregroundColor(.secondary)
  624. // .font(.caption)
  625. // }
  626. // Spacer()
  627. // }.padding(.top, 2)
  628. // }
  629. // .contentShape(Rectangle())
  630. // .onTapGesture {
  631. // Task {
  632. // let objectID = preset.objectID
  633. // await state.enactTempTargetPreset(withID: objectID)
  634. // selectedTempTargetPresetID = preset.id?.uuidString
  635. // showCheckmark.toggle()
  636. //
  637. // DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  638. // showCheckmark = false
  639. // }
  640. // }
  641. // }
  642. // }
  643. // if showCheckmark && isSelected {
  644. // // show checkmark to indicate if the preset was actually pressed
  645. // Image(systemName: "checkmark.circle.fill")
  646. // .imageScale(.large)
  647. // .fontWeight(.bold)
  648. // .foregroundStyle(Color.green)
  649. // } else {
  650. // Image(systemName: "line.3.horizontal")
  651. // .imageScale(.medium)
  652. // .foregroundStyle(.secondary)
  653. // }
  654. // })
  655. // }
  656. //
  657. // private func scheduledTempTargetView(for preset: TempTargetStored) -> some View {
  658. // let target = preset.target ?? 100
  659. // let presetTarget = Decimal(target as! Double.RawValue)
  660. // let isSelected = preset.id?.uuidString == selectedTempTargetPresetID
  661. // let presetHalfBasalTarget = Decimal(
  662. // preset.halfBasalTarget as? Double
  663. // .RawValue ?? Double(state.settingHalfBasalTarget)
  664. // )
  665. // let percentage = Int(
  666. // state.computeAdjustedPercentage(usingHBT: presetHalfBasalTarget, usingTarget: presetTarget)
  667. // )
  668. //
  669. // return ZStack(alignment: .trailing, content: {
  670. // HStack {
  671. // VStack {
  672. // HStack {
  673. // Text(preset.name ?? "")
  674. // Spacer()
  675. // }
  676. // HStack(spacing: 2) {
  677. // Text(formattedGlucose(glucose: target as Decimal))
  678. // .foregroundColor(.secondary)
  679. // .font(.caption)
  680. // Text("for")
  681. // .foregroundColor(.secondary)
  682. // .font(.caption)
  683. // Text("\(formatter.string(from: (preset.duration ?? 0) as NSNumber)!)")
  684. // .foregroundColor(.secondary)
  685. // .font(.caption)
  686. // Text("min")
  687. // .foregroundColor(.secondary)
  688. // .font(.caption)
  689. // if state.isAdjustSensEnabled(usingTarget: presetTarget) { Text(", \(percentage)%")
  690. // .foregroundColor(.secondary)
  691. // .font(.caption)
  692. // }
  693. // Spacer()
  694. // }.padding(.top, 2)
  695. // }
  696. // .contentShape(Rectangle())
  697. // }
  698. // })
  699. // }
  700. private var overrideLabelDivider: some View {
  701. Divider()
  702. .frame(width: 1, height: 20)
  703. }
  704. @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
  705. let isSelected = preset.id == selectedPresetID
  706. let name = preset.name ?? ""
  707. let indefinite = preset.indefinite
  708. let duration = preset.duration?.decimalValue ?? Decimal(0)
  709. let percentage = preset.percentage
  710. let smbMinutes = preset.smbMinutes?.decimalValue ?? Decimal(0)
  711. let uamMinutes = preset.uamMinutes?.decimalValue ?? Decimal(0)
  712. let target: String = {
  713. guard let targetValue = preset.target, targetValue != 0 else { return "" }
  714. return state.units == .mgdL ? targetValue.description : targetValue.decimalValue.formattedAsMmolL
  715. }()
  716. let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
  717. let durationString = indefinite ? "" : "\(formatHrMin(Int(duration)))"
  718. let scheduledSMBString: String = {
  719. guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
  720. return " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
  721. }()
  722. let smbString: String = {
  723. guard preset.smbIsOff || preset.smbIsScheduledOff else { return "" }
  724. return "SMBs Off\(scheduledSMBString)"
  725. }()
  726. let maxSmbMinsString: String = {
  727. guard smbMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
  728. smbMinutes != state.defaultSmbMinutes else { return "" }
  729. return "\(smbMinutes.formatted()) min SMB"
  730. }()
  731. let maxUamMinsString: String = {
  732. guard uamMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
  733. uamMinutes != state.defaultUamMinutes else { return "" }
  734. return "\(uamMinutes.formatted()) min UAM"
  735. }()
  736. let isfAndCrString: String = {
  737. switch (preset.isfAndCr, preset.isf, preset.cr) {
  738. case (_, true, true),
  739. (true, _, _):
  740. return " ISF/CR"
  741. case (false, true, false):
  742. return " ISF"
  743. case (false, false, true):
  744. return " CR"
  745. default:
  746. return ""
  747. }
  748. }()
  749. let percentageString = percentage != 100 ? "\(Int(percentage))%\(isfAndCrString)" : ""
  750. // Combine all labels into a single array, filtering out empty strings
  751. let labels: [String] = [
  752. durationString,
  753. percentageString,
  754. targetString,
  755. smbString,
  756. maxSmbMinsString,
  757. maxUamMinsString
  758. ].filter { !$0.isEmpty }
  759. if !name.isEmpty {
  760. ZStack(alignment: .trailing) {
  761. HStack {
  762. VStack {
  763. HStack {
  764. Text(name)
  765. Spacer()
  766. }
  767. HStack(spacing: 5) {
  768. ForEach(labels, id: \.self) { label in
  769. Text(label)
  770. if label != labels.last { // Add divider between labels
  771. overrideLabelDivider
  772. }
  773. }
  774. Spacer()
  775. }
  776. .padding(.top, 2)
  777. .foregroundColor(.secondary)
  778. .font(.caption)
  779. }
  780. .contentShape(Rectangle())
  781. .onTapGesture {
  782. Task {
  783. let objectID = preset.objectID
  784. await state.enactOverridePreset(withID: objectID)
  785. state.hideModal()
  786. showCheckmark.toggle()
  787. selectedPresetID = preset.id
  788. // Deactivate checkmark after 3 seconds
  789. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  790. showCheckmark = false
  791. }
  792. }
  793. }
  794. }
  795. // show checkmark to indicate if the preset was actually pressed
  796. if showCheckmark && isSelected {
  797. Image(systemName: "checkmark.circle.fill")
  798. .imageScale(.large)
  799. .fontWeight(.bold)
  800. .foregroundStyle(Color.green)
  801. } else {
  802. Image(systemName: "line.3.horizontal")
  803. .imageScale(.medium)
  804. .foregroundStyle(.secondary)
  805. }
  806. }
  807. }
  808. }
  809. }
  810. }