|
|
@@ -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
|
|
|
)
|
|
|
}
|
|
|
|