LoopOverrideView.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. // LoopFollow
  2. // LoopOverrideView.swift
  3. // Created by Jonas Björkert on 2024-10-09.
  4. import HealthKit
  5. import SwiftUI
  6. struct LoopOverrideView: View {
  7. @Environment(\.presentationMode) private var presentationMode
  8. @ObservedObject var device = Storage.shared.device
  9. @ObservedObject var overrideNote = Observable.shared.override
  10. @ObservedObject var nsAdmin = Storage.shared.nsWriteAuth
  11. @StateObject private var viewModel = LoopOverrideViewModel()
  12. @State private var showAlert: Bool = false
  13. @State private var alertType: AlertType? = nil
  14. @State private var alertMessage: String? = nil
  15. @State private var isLoading: Bool = false
  16. @State private var statusMessage: String? = nil
  17. @State private var selectedOverride: ProfileManager.LoopOverride? = nil
  18. @State private var showConfirmation: Bool = false
  19. @FocusState private var noteFieldIsFocused: Bool
  20. private let pushNotificationManager = PushNotificationManager()
  21. private var profileManager = ProfileManager.shared
  22. enum AlertType {
  23. case confirmActivation
  24. case confirmCancellation
  25. case statusSuccess
  26. case statusFailure
  27. case validation
  28. }
  29. var body: some View {
  30. NavigationView {
  31. VStack {
  32. if device.value != "Loop" {
  33. ErrorMessageView(
  34. message: "Remote commands are currently only available for Loop."
  35. )
  36. } else if !nsAdmin.value {
  37. ErrorMessageView(
  38. message: "Please update your token to include the 'admin' role in order to do remote commands with Loop."
  39. )
  40. } else {
  41. Form {
  42. if let activeNote = overrideNote.value {
  43. Section(header: Text("Active Override")) {
  44. HStack {
  45. Text("Override")
  46. Spacer()
  47. Text(activeNote)
  48. .foregroundColor(.secondary)
  49. }
  50. Button {
  51. alertType = .confirmCancellation
  52. showAlert = true
  53. } label: {
  54. HStack {
  55. Text("Cancel Override")
  56. Spacer()
  57. Image(systemName: "xmark.app")
  58. .font(.title)
  59. }
  60. }
  61. .tint(.red)
  62. }
  63. Section {
  64. Text("Setting a new override while one is active will cancel the active override and set the new one.")
  65. .foregroundColor(.secondary)
  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) \(Localizer.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. LogManager.shared.log(category: .nightscout, message: "LoopOverrideView: sendActivateOverrideRequest succeeded for override: \(override.name)")
  184. } else {
  185. self.statusMessage = message ?? "Failed to send override command."
  186. self.alertType = .statusFailure
  187. LogManager.shared.log(category: .nightscout, message: "LoopOverrideView: sendActivateOverrideRequest failed for override: \(override.name) with error: \(message ?? "unknown error")")
  188. }
  189. self.showAlert = true
  190. }
  191. }
  192. private func cancelOverride() {
  193. isLoading = true
  194. viewModel.sendCancelOverrideRequest { success, message in
  195. self.isLoading = false
  196. if success {
  197. self.statusMessage = "Cancellation request successfully sent to Nightscout."
  198. self.alertType = .statusSuccess
  199. LogManager.shared.log(category: .nightscout, message: "LoopOverrideView: sendCancelOverrideRequest succeeded")
  200. } else {
  201. self.statusMessage = message ?? "Failed to cancel override."
  202. self.alertType = .statusFailure
  203. LogManager.shared.log(category: .nightscout, message: "LoopOverrideView: sendCancelOverrideRequest failed with error: \(message ?? "unknown error")")
  204. }
  205. self.showAlert = true
  206. }
  207. }
  208. }