LoopOverrideView.swift 9.1 KB

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