OverridePresetsView.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. // LoopFollow
  2. // OverridePresetsView.swift
  3. import SwiftUI
  4. struct OverridePresetsView: View {
  5. @StateObject private var viewModel = OverridePresetsViewModel()
  6. @Environment(\.presentationMode) var presentationMode
  7. @ObservedObject var overrideNote = Observable.shared.override
  8. var body: some View {
  9. NavigationView {
  10. VStack {
  11. List {
  12. // Current Active Override Section
  13. if let activeNote = overrideNote.value {
  14. Section(header: Text("Active Override")) {
  15. HStack {
  16. Image(systemName: "checkmark.circle.fill")
  17. .foregroundColor(.green)
  18. .font(.title2)
  19. VStack(alignment: .leading, spacing: 4) {
  20. Text("Active Override")
  21. .font(.headline)
  22. .foregroundColor(.primary)
  23. Text(activeNote)
  24. .font(.subheadline)
  25. .foregroundColor(.secondary)
  26. }
  27. Spacer()
  28. }
  29. .padding(.vertical, 4)
  30. Button(action: {
  31. viewModel.alertType = .confirmCancellation
  32. viewModel.showAlert = true
  33. }) {
  34. HStack {
  35. Image(systemName: "xmark.circle")
  36. .foregroundColor(.red)
  37. Text("Cancel Active Override")
  38. .foregroundColor(.red)
  39. }
  40. }
  41. }
  42. }
  43. Section(header: Text("Available Overrides")) {
  44. if viewModel.isLoading {
  45. HStack {
  46. ProgressView()
  47. .scaleEffect(0.8)
  48. Text("Loading override presets...")
  49. .foregroundColor(.secondary)
  50. }
  51. } else if viewModel.overridePresets.isEmpty {
  52. Text("No override presets found. Configure presets in your Loop app.")
  53. .foregroundColor(.secondary)
  54. .italic()
  55. } else {
  56. ForEach(viewModel.overridePresets, id: \.name) { preset in
  57. OverridePresetRow(
  58. preset: preset,
  59. isActivating: viewModel.isActivating && viewModel.selectedPreset?.name == preset.name,
  60. onActivate: {
  61. viewModel.selectedPreset = preset
  62. viewModel.alertType = .confirmActivation
  63. viewModel.showAlert = true
  64. }
  65. )
  66. }
  67. }
  68. }
  69. }
  70. if viewModel.isActivating {
  71. ProgressView("Please wait...")
  72. .padding()
  73. }
  74. }
  75. .navigationBarTitle("Remote Overrides", displayMode: .inline)
  76. .onAppear {
  77. Task {
  78. await viewModel.loadOverridePresets()
  79. }
  80. }
  81. .alert(isPresented: $viewModel.showAlert) {
  82. switch viewModel.alertType {
  83. case .confirmActivation:
  84. return Alert(
  85. title: Text("Activate Override"),
  86. message: Text("Do you want to activate the override '\(viewModel.selectedPreset?.name ?? "")'?"),
  87. primaryButton: .default(Text("Confirm"), action: {
  88. if let preset = viewModel.selectedPreset {
  89. Task {
  90. await viewModel.activateOverride(preset: preset)
  91. }
  92. }
  93. }),
  94. secondaryButton: .cancel()
  95. )
  96. case .confirmCancellation:
  97. return Alert(
  98. title: Text("Cancel Override"),
  99. message: Text("Are you sure you want to cancel the active override?"),
  100. primaryButton: .default(Text("Confirm"), action: {
  101. Task {
  102. await viewModel.cancelOverride()
  103. }
  104. }),
  105. secondaryButton: .cancel()
  106. )
  107. case .statusSuccess:
  108. return Alert(
  109. title: Text("Success"),
  110. message: Text(viewModel.statusMessage ?? ""),
  111. dismissButton: .default(Text("OK"), action: {
  112. presentationMode.wrappedValue.dismiss()
  113. })
  114. )
  115. case .statusFailure:
  116. return Alert(
  117. title: Text("Error"),
  118. message: Text(viewModel.statusMessage ?? "An error occurred."),
  119. dismissButton: .default(Text("OK"))
  120. )
  121. case .none:
  122. return Alert(title: Text("Unknown Alert"))
  123. }
  124. }
  125. }
  126. }
  127. }
  128. struct OverridePresetRow: View {
  129. let preset: OverridePreset
  130. let isActivating: Bool
  131. let onActivate: () -> Void
  132. var body: some View {
  133. Button(action: onActivate) {
  134. HStack {
  135. if let symbol = preset.symbol {
  136. Text(symbol)
  137. .font(.title2)
  138. }
  139. VStack(alignment: .leading, spacing: 4) {
  140. Text(preset.name)
  141. .font(.headline)
  142. .foregroundColor(.primary)
  143. HStack(spacing: 8) {
  144. if let targetRange = preset.targetRange {
  145. Text("Target: \(Int(targetRange.lowerBound))-\(Int(targetRange.upperBound))")
  146. .font(.caption)
  147. .foregroundColor(.secondary)
  148. }
  149. if let insulinNeedsScaleFactor = preset.insulinNeedsScaleFactor {
  150. Text("Insulin: \(Int(insulinNeedsScaleFactor * 100))%")
  151. .font(.caption)
  152. .foregroundColor(.secondary)
  153. }
  154. Text("Duration: \(preset.durationDescription)")
  155. .font(.caption)
  156. .foregroundColor(.secondary)
  157. }
  158. }
  159. Spacer()
  160. if isActivating {
  161. ProgressView()
  162. .scaleEffect(0.8)
  163. .foregroundColor(.blue)
  164. } else {
  165. Image(systemName: "chevron.right")
  166. .foregroundColor(.secondary)
  167. .font(.caption)
  168. }
  169. }
  170. .padding(.vertical, 4)
  171. }
  172. .buttonStyle(PlainButtonStyle())
  173. .disabled(isActivating)
  174. }
  175. }
  176. class OverridePresetsViewModel: ObservableObject {
  177. @Published var overridePresets: [OverridePreset] = []
  178. @Published var isLoading = false
  179. @Published var isActivating = false
  180. @Published var showAlert = false
  181. @Published var alertType: AlertType? = nil
  182. @Published var statusMessage: String? = nil
  183. @Published var selectedPreset: OverridePreset? = nil
  184. enum AlertType {
  185. case confirmActivation
  186. case confirmCancellation
  187. case statusSuccess
  188. case statusFailure
  189. }
  190. func loadOverridePresets() async {
  191. await MainActor.run {
  192. isLoading = true
  193. }
  194. do {
  195. let presets = try await fetchOverridePresetsFromNightscout()
  196. await MainActor.run {
  197. self.overridePresets = presets
  198. self.isLoading = false
  199. }
  200. } catch {
  201. await MainActor.run {
  202. self.statusMessage = "Failed to load override presets: \(error.localizedDescription)"
  203. self.alertType = .statusFailure
  204. self.showAlert = true
  205. self.isLoading = false
  206. }
  207. }
  208. }
  209. func activateOverride(preset: OverridePreset) async {
  210. await MainActor.run {
  211. isActivating = true
  212. }
  213. do {
  214. try await sendOverrideNotification(preset: preset)
  215. await MainActor.run {
  216. self.isActivating = false
  217. self.statusMessage = "\(preset.name) override activated successfully."
  218. self.alertType = .statusSuccess
  219. self.showAlert = true
  220. }
  221. } catch {
  222. await MainActor.run {
  223. self.statusMessage = "Failed to activate override: \(error.localizedDescription)"
  224. self.alertType = .statusFailure
  225. self.showAlert = true
  226. self.isActivating = false
  227. }
  228. }
  229. }
  230. func cancelOverride() async {
  231. await MainActor.run {
  232. isActivating = true
  233. }
  234. do {
  235. try await sendCancelOverrideNotification()
  236. await MainActor.run {
  237. self.isActivating = false
  238. self.statusMessage = "Active override cancelled successfully."
  239. self.alertType = .statusSuccess
  240. self.showAlert = true
  241. }
  242. } catch {
  243. await MainActor.run {
  244. self.statusMessage = "Failed to cancel override: \(error.localizedDescription)"
  245. self.alertType = .statusFailure
  246. self.showAlert = true
  247. self.isActivating = false
  248. }
  249. }
  250. }
  251. private func fetchOverridePresetsFromNightscout() async throws -> [OverridePreset] {
  252. // Use ProfileManager's already loaded overrides instead of fetching from Nightscout
  253. let loopOverrides = ProfileManager.shared.loopOverrides
  254. return loopOverrides.map { override in
  255. let targetRange: ClosedRange<Double>?
  256. if override.targetRange.count >= 2 {
  257. let lowValue = override.targetRange[0].doubleValue(for: ProfileManager.shared.units)
  258. let highValue = override.targetRange[1].doubleValue(for: ProfileManager.shared.units)
  259. targetRange = lowValue ... highValue
  260. } else {
  261. targetRange = nil
  262. }
  263. return OverridePreset(
  264. name: override.name,
  265. symbol: override.symbol.isEmpty ? nil : override.symbol,
  266. targetRange: targetRange,
  267. insulinNeedsScaleFactor: override.insulinNeedsScaleFactor,
  268. duration: TimeInterval(override.duration ?? 0)
  269. )
  270. }
  271. }
  272. private func sendOverrideNotification(preset: OverridePreset) async throws {
  273. let apnsService = LoopAPNSService()
  274. try await apnsService.sendOverrideNotification(
  275. presetName: preset.name,
  276. duration: preset.duration
  277. )
  278. }
  279. private func sendCancelOverrideNotification() async throws {
  280. let apnsService = LoopAPNSService()
  281. try await apnsService.sendCancelOverrideNotification()
  282. }
  283. }
  284. // MARK: - Data Models
  285. struct OverridePreset {
  286. let name: String
  287. let symbol: String?
  288. let targetRange: ClosedRange<Double>?
  289. let insulinNeedsScaleFactor: Double?
  290. let duration: TimeInterval
  291. var durationDescription: String {
  292. if duration == 0 {
  293. return "Indefinite"
  294. } else {
  295. let hours = Int(duration) / 3600
  296. let minutes = Int(duration) % 3600 / 60
  297. if hours > 0 {
  298. return "\(hours)h \(minutes)m"
  299. } else {
  300. return "\(minutes)m"
  301. }
  302. }
  303. }
  304. }
  305. enum OverrideError: LocalizedError {
  306. case nightscoutNotConfigured
  307. case invalidResponse
  308. case serverError(Int)
  309. var errorDescription: String? {
  310. switch self {
  311. case .nightscoutNotConfigured:
  312. return "Nightscout URL and token not configured in settings"
  313. case .invalidResponse:
  314. return "Invalid response from server"
  315. case let .serverError(code):
  316. return "Server error: \(code)"
  317. }
  318. }
  319. }