OverridePresetsView.swift 13 KB

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