LoopOverrideView.swift 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. //
  2. // LoopOverrideView.swift
  3. // LoopFollow
  4. //
  5. // Created by Jonas Björkert on 2025-01-14.
  6. // Copyright © 2025 Jon Fawcett. All rights reserved.
  7. //
  8. import SwiftUI
  9. import HealthKit
  10. struct LoopOverrideView: View {
  11. @Environment(\.presentationMode) private var presentationMode
  12. @ObservedObject var device = ObservableUserDefaults.shared.device
  13. @ObservedObject var overrideNote = Observable.shared.override
  14. @ObservedObject var nsAdmin = ObservableUserDefaults.shared.nsWriteAuth
  15. @StateObject private var viewModel = LoopOverrideViewModel()
  16. @State private var showAlert: Bool = false
  17. @State private var alertType: AlertType? = nil
  18. @State private var alertMessage: String? = nil
  19. @State private var isLoading: Bool = false
  20. @State private var statusMessage: String? = nil
  21. @State private var selectedOverride: ProfileManager.LoopOverride? = nil
  22. @State private var showConfirmation: Bool = false
  23. @FocusState private var noteFieldIsFocused: Bool
  24. private let pushNotificationManager = PushNotificationManager()
  25. private var profileManager = ProfileManager.shared
  26. enum AlertType {
  27. case confirmActivation
  28. case confirmCancellation
  29. case statusSuccess
  30. case statusFailure
  31. case validation
  32. }
  33. var body: some View {
  34. NavigationView {
  35. VStack {
  36. if device.value != "Loop" {
  37. ErrorMessageView(
  38. message: "Remote commands are currently only available for Loop."
  39. )
  40. } else if !nsAdmin.value {
  41. ErrorMessageView(
  42. message: "Please update your token to include the 'admin' role in order to do remote commands."
  43. )
  44. } else {
  45. Form {
  46. if let activeNote = overrideNote.value {
  47. Section(header: Text("Active Override")) {
  48. HStack {
  49. Text("Override")
  50. Spacer()
  51. Text(activeNote)
  52. .foregroundColor(.secondary)
  53. }
  54. Button {
  55. alertType = .confirmCancellation
  56. showAlert = true
  57. } label: {
  58. HStack {
  59. Text("Cancel Override")
  60. Spacer()
  61. Image(systemName: "xmark.app")
  62. .font(.title)
  63. }
  64. }
  65. .tint(.red)
  66. }
  67. }
  68. Section(header: Text("Available Overrides")) {
  69. if profileManager.loopOverrides.isEmpty {
  70. Text("No overrides available.")
  71. .foregroundColor(.secondary)
  72. } else {
  73. ForEach(profileManager.loopOverrides, id: \.name) { override in
  74. Button(action: {
  75. selectedOverride = override
  76. alertType = .confirmActivation
  77. showAlert = true
  78. }) {
  79. HStack {
  80. VStack(alignment: .leading) {
  81. Text("\(override.symbol) \(override.name)")
  82. .font(.headline)
  83. Text("Duration: \(formattedDuration(from: override.duration))")
  84. .font(.subheadline)
  85. .foregroundColor(.secondary)
  86. Text("Insulin Scale Factor: \(String(format: "%.2f", override.insulinNeedsScaleFactor))")
  87. .font(.subheadline)
  88. .foregroundColor(.secondary)
  89. if !override.targetRange.isEmpty {
  90. let range = override.targetRange.map { Localizer.formatQuantity($0) }.joined(separator: " - ")
  91. Text("Target Range: \(range) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)")
  92. .font(.subheadline)
  93. .foregroundColor(.secondary)
  94. }
  95. }
  96. Spacer()
  97. Image(systemName: "arrow.right.circle")
  98. .foregroundColor(.blue)
  99. }
  100. }
  101. }
  102. }
  103. }
  104. }
  105. if isLoading {
  106. ProgressView("Please wait...")
  107. .padding()
  108. }
  109. }
  110. }
  111. .navigationTitle("Loop Overrides")
  112. .navigationBarTitleDisplayMode(.inline)
  113. .alert(isPresented: $showAlert) {
  114. switch alertType {
  115. case .confirmActivation:
  116. return Alert(
  117. title: Text("Activate Override"),
  118. message: Text("Do you want to activate the override '\(selectedOverride?.name ?? "")'?"),
  119. primaryButton: .default(Text("Confirm"), action: {
  120. if let override = selectedOverride {
  121. activateOverride(override: override)
  122. }
  123. }),
  124. secondaryButton: .cancel()
  125. )
  126. case .confirmCancellation:
  127. return Alert(
  128. title: Text("Cancel Override"),
  129. message: Text("Are you sure you want to cancel the active override?"),
  130. primaryButton: .default(Text("Confirm"), action: {
  131. cancelOverride()
  132. }),
  133. secondaryButton: .cancel()
  134. )
  135. case .statusSuccess:
  136. return Alert(
  137. title: Text("Success"),
  138. message: Text(statusMessage ?? ""),
  139. dismissButton: .default(Text("OK"), action: {
  140. presentationMode.wrappedValue.dismiss()
  141. })
  142. )
  143. case .statusFailure:
  144. return Alert(
  145. title: Text("Error"),
  146. message: Text(statusMessage ?? "An error occurred."),
  147. dismissButton: .default(Text("OK"))
  148. )
  149. case .validation:
  150. return Alert(
  151. title: Text("Validation Error"),
  152. message: Text(alertMessage ?? "Invalid input."),
  153. dismissButton: .default(Text("OK"))
  154. )
  155. case .none:
  156. return Alert(title: Text("Unknown Alert"))
  157. }
  158. }
  159. }
  160. }
  161. // MARK: - Functions
  162. private func formattedDuration(from duration: Int?) -> String {
  163. guard let duration = duration, duration != 0 else {
  164. return "Indefinitely"
  165. }
  166. let hours = duration / 3600
  167. let minutes = (duration % 3600) / 60
  168. if hours > 0 && minutes > 0 {
  169. return "\(hours) hr, \(minutes) min"
  170. } else if hours > 0 {
  171. return "\(hours) hr"
  172. } else {
  173. return "\(minutes) min"
  174. }
  175. }
  176. private func activateOverride(override: ProfileManager.LoopOverride) {
  177. isLoading = true
  178. viewModel.sendActivateOverrideRequest(override: override) { success, message in
  179. self.isLoading = false
  180. if success {
  181. self.statusMessage = "Override command sent successfully."
  182. self.alertType = .statusSuccess
  183. } else {
  184. self.statusMessage = message ?? "Failed to send override command."
  185. self.alertType = .statusFailure
  186. }
  187. self.showAlert = true
  188. }
  189. }
  190. private func cancelOverride() {
  191. isLoading = true
  192. viewModel.sendCancelOverrideRequest { success, message in
  193. self.isLoading = false
  194. if success {
  195. self.statusMessage = "Cancellation request successfully sent to Nightscout."
  196. self.alertType = .statusSuccess
  197. } else {
  198. self.statusMessage = message ?? "Failed to cancel temp target."
  199. self.alertType = .statusFailure
  200. }
  201. self.showAlert = true
  202. }
  203. }
  204. }