Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 191 additions & 24 deletions LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
)
}
Expand All @@ -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"),
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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."
Expand Down Expand Up @@ -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
Expand All @@ -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
)
}

Expand Down