diff --git a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift index d34d8fffb..7516c9f61 100644 --- a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift +++ b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift @@ -67,8 +67,7 @@ struct OverridePresetsView: View { isActivating: viewModel.isActivating && viewModel.selectedPreset?.name == preset.name, onActivate: { viewModel.selectedPreset = preset - viewModel.alertType = .confirmActivation - viewModel.showAlert = true + viewModel.showOverrideModal = true } ) } @@ -87,21 +86,24 @@ struct OverridePresetsView: View { await viewModel.loadOverridePresets() } } - .alert(isPresented: $viewModel.showAlert) { - switch viewModel.alertType { - case .confirmActivation: - return Alert( - title: Text("Activate Override"), - message: Text("Do you want to activate the override '\(viewModel.selectedPreset?.name ?? "")'?"), - primaryButton: .default(Text("Confirm"), action: { - if let preset = viewModel.selectedPreset { - Task { - await viewModel.activateOverride(preset: preset) - } + .sheet(isPresented: $viewModel.showOverrideModal) { + if let preset = viewModel.selectedPreset { + OverrideActivationModal( + preset: preset, + onActivate: { duration in + viewModel.showOverrideModal = false + Task { + await viewModel.activateOverride(preset: preset, duration: duration) } - }), - secondaryButton: .cancel() + }, + onCancel: { + viewModel.showOverrideModal = false + } ) + } + } + .alert(isPresented: $viewModel.showAlert) { + switch viewModel.alertType { case .confirmCancellation: return Alert( title: Text("Cancel Override"), @@ -155,7 +157,7 @@ struct OverridePresetRow: View { HStack(spacing: 8) { if let targetRange = preset.targetRange { - Text("Target: \(Int(targetRange.lowerBound))-\(Int(targetRange.upperBound))") + Text("Target: \(Localizer.formatLocalDouble(targetRange.lowerBound))-\(Localizer.formatLocalDouble(targetRange.upperBound))") .font(.caption) .foregroundColor(.secondary) } @@ -191,6 +193,172 @@ struct OverridePresetRow: View { } } +struct OverrideActivationModal: View { + let preset: OverridePreset + let onActivate: (TimeInterval?) -> Void + let onCancel: () -> Void + + @State private var enableIndefinitely: Bool + @State private var durationHours: Double = 1.0 + + init(preset: OverridePreset, onActivate: @escaping (TimeInterval?) -> Void, onCancel: @escaping () -> Void) { + self.preset = preset + self.onActivate = onActivate + self.onCancel = onCancel + + // Initialize state based on preset duration + if preset.duration == 0 { + // Indefinite override - allow user to choose + _enableIndefinitely = State(initialValue: true) + } else { + // Override with predefined duration - use preset duration + _enableIndefinitely = State(initialValue: false) + _durationHours = State(initialValue: preset.duration / 3600) + } + } + + var body: some View { + NavigationView { + VStack(spacing: 20) { + // Preset Info + VStack(spacing: 12) { + if let symbol = preset.symbol { + Text(symbol) + .font(.largeTitle) + } + + Text(preset.name) + .font(.title2) + .fontWeight(.semibold) + .multilineTextAlignment(.center) + + if let targetRange = preset.targetRange { + Text("Target: \(Localizer.formatLocalDouble(targetRange.lowerBound))-\(Localizer.formatLocalDouble(targetRange.upperBound))") + .font(.subheadline) + .foregroundColor(.secondary) + } + + if let insulinNeedsScaleFactor = preset.insulinNeedsScaleFactor { + Text("Insulin: \(Int(insulinNeedsScaleFactor * 100))%") + .font(.subheadline) + .foregroundColor(.secondary) + } + + // Only show duration for overrides with predefined duration + if preset.duration != 0 { + Text("Duration: \(preset.durationDescription)") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + .padding(.top) + + Spacer() + + // Duration Settings (only show for overrides without predefined duration) + if preset.duration == 0 { + VStack(spacing: 16) { + // Duration Input (only show when not indefinite) + if !enableIndefinitely { + VStack(spacing: 8) { + HStack { + Text("Duration") + .font(.headline) + Spacer() + Text(formatDuration(durationHours)) + .font(.headline) + .foregroundColor(.blue) + } + + Slider(value: $durationHours, in: 0.25 ... 24.0, step: 0.25) + .accentColor(.blue) + HStack { + Text("15m") + .font(.caption) + .foregroundColor(.secondary) + .frame(width: 80, alignment: .leading) + Spacer() + Text("24h") + .font(.caption) + .foregroundColor(.secondary) + .frame(width: 80, alignment: .trailing) + } + } + .padding(.horizontal) + } + + // Indefinitely Toggle + HStack { + Toggle("Enable indefinitely", isOn: $enableIndefinitely) + Spacer() + } + .padding(.horizontal) + } + } + + // Action Buttons + VStack(spacing: 12) { + Button(action: { + let duration: TimeInterval? + if preset.duration == 0 { + // For indefinite overrides, use user selection + duration = enableIndefinitely ? nil : (durationHours * 3600) + } else { + // For overrides with predefined duration, use preset duration + duration = preset.duration + } + onActivate(duration) + }) { + Text("Activate Override") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .cornerRadius(10) + } + + Button(action: onCancel) { + Text("Cancel") + .font(.headline) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity) + .padding() + .background(Color.gray.opacity(0.2)) + .cornerRadius(10) + } + } + .padding(.horizontal) + .padding(.bottom) + } + .navigationBarTitle("Activate Override", displayMode: .inline) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Cancel") { + onCancel() + } + } + } + } + } + + // Helper function to format duration in hours and minutes + private func formatDuration(_ hours: Double) -> String { + let totalMinutes = Int(hours * 60) + let hours = totalMinutes / 60 + let minutes = totalMinutes % 60 + + if hours > 0 && minutes > 0 { + return "\(hours)h \(minutes)m" + } else if hours > 0 { + return "\(hours)h" + } else { + return "\(minutes)m" + } + } +} + class OverridePresetsViewModel: ObservableObject { @Published var overridePresets: [OverridePreset] = [] @Published var isLoading = false @@ -199,9 +367,9 @@ class OverridePresetsViewModel: ObservableObject { @Published var alertType: AlertType? = nil @Published var statusMessage: String? = nil @Published var selectedPreset: OverridePreset? = nil + @Published var showOverrideModal = false enum AlertType { - case confirmActivation case confirmCancellation case statusSuccess case statusFailure @@ -213,7 +381,7 @@ class OverridePresetsViewModel: ObservableObject { } do { - let presets = try await fetchOverridePresetsFromNightscout() + let presets = try await fetchOverridePresetsFromStorage() await MainActor.run { self.overridePresets = presets self.isLoading = false @@ -228,13 +396,13 @@ class OverridePresetsViewModel: ObservableObject { } } - func activateOverride(preset: OverridePreset) async { + func activateOverride(preset: OverridePreset, duration: TimeInterval?) async { await MainActor.run { isActivating = true } do { - try await sendOverrideNotification(preset: preset) + try await sendOverrideNotification(preset: preset, duration: duration) await MainActor.run { self.isActivating = false self.statusMessage = "\(preset.name) override activated successfully." @@ -274,8 +442,7 @@ class OverridePresetsViewModel: ObservableObject { } } - private func fetchOverridePresetsFromNightscout() async throws -> [OverridePreset] { - // Use ProfileManager's already loaded overrides instead of fetching from Nightscout + private func fetchOverridePresetsFromStorage() async throws -> [OverridePreset] { let loopOverrides = ProfileManager.shared.loopOverrides return loopOverrides.map { override in @@ -298,11 +465,11 @@ class OverridePresetsViewModel: ObservableObject { } } - private func sendOverrideNotification(preset: OverridePreset) async throws { + private func sendOverrideNotification(preset: OverridePreset, duration: TimeInterval?) async throws { let apnsService = LoopAPNSService() try await apnsService.sendOverrideNotification( presetName: preset.name, - duration: preset.duration + duration: duration ) }