diff --git a/FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift b/FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift index 0a2a5ce25..3d3cc0882 100644 --- a/FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift +++ b/FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift @@ -44,7 +44,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable { durationMin: nil, rate: nil, temp: nil, - carbInput: nil + carbInput: nil, + isExternalInsulin: dose.manuallyEntered )] case .tempBasal: guard let dose = event.dose else { return [] } @@ -209,6 +210,13 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable { } } + func determineBolusEventType(for event: PumpHistoryEvent) -> EventType { + if event.isExternalInsulin ?? false { + return .externalInsulin + } + return event.type + } + func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] { let events = recent() guard !events.isEmpty else { return [] } @@ -249,13 +257,14 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable { let bolusesAndCarbs = events.compactMap { event -> NigtscoutTreatment? in switch event.type { case .bolus: + let eventType = determineBolusEventType(for: event) return NigtscoutTreatment( duration: event.duration, rawDuration: nil, rawRate: nil, absolute: nil, rate: nil, - eventType: .bolus, + eventType: eventType, createdAt: event.timestamp, enteredBy: NigtscoutTreatment.local, bolus: event, diff --git a/FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings b/FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings index 0fdbe33e9..7a81e045e 100644 --- a/FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings +++ b/FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings @@ -1222,7 +1222,7 @@ Enact a temp Basal or a temp target */ /* An Automatic delivered bolus (SMB) */ "SMB" = "SMB"; -/* A manually entered dose of external insulin */ +/* A manually entered dose of non-pump insulin */ "External Insulin" = "Externes Insulin"; /* Status highlight when manual temp basal is running. */ diff --git a/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings b/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings index af09f7d3b..c858425de 100644 --- a/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings +++ b/FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings @@ -1220,7 +1220,7 @@ Enact a temp Basal or a temp target */ /* An Automatic delivered bolus (SMB) */ "SMB" = "SMB"; -/* A manually entered dose of external insulin */ +/* A manually entered dose of non-pump insulin */ "External Insulin" = "External Insulin"; /* Status highlight when manual temp basal is running. */ diff --git a/FreeAPS/Sources/Models/PumpHistoryEvent.swift b/FreeAPS/Sources/Models/PumpHistoryEvent.swift index a13a801b3..60f59bd4b 100644 --- a/FreeAPS/Sources/Models/PumpHistoryEvent.swift +++ b/FreeAPS/Sources/Models/PumpHistoryEvent.swift @@ -11,6 +11,7 @@ struct PumpHistoryEvent: JSON, Equatable { let temp: TempType? let carbInput: Int? let note: String? + let isExternalInsulin: Bool? init( id: String, @@ -22,7 +23,8 @@ struct PumpHistoryEvent: JSON, Equatable { rate: Decimal? = nil, temp: TempType? = nil, carbInput: Int? = nil, - note: String? = nil + note: String? = nil, + isExternalInsulin: Bool? = nil ) { self.id = id self.type = type @@ -34,12 +36,14 @@ struct PumpHistoryEvent: JSON, Equatable { self.temp = temp self.carbInput = carbInput self.note = note + self.isExternalInsulin = isExternalInsulin } } enum EventType: String, JSON { case bolus = "Bolus" - case mealBulus = "Meal Bolus" + case externalInsulin = "External Insulin" + case mealBolus = "Meal Bolus" case correctionBolus = "Correction Bolus" case snackBolus = "Snack Bolus" case bolusWizard = "BolusWizard" @@ -80,5 +84,6 @@ extension PumpHistoryEvent { case temp case carbInput = "carb_input" case note + case isExternalInsulin } } diff --git a/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift b/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift index c24119b3d..8c4a5a97d 100644 --- a/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift +++ b/FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift @@ -83,7 +83,8 @@ extension Bolus { durationMin: nil, rate: nil, temp: nil, - carbInput: nil + carbInput: nil, + isExternalInsulin: true ) ] ) diff --git a/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift b/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift index 99dff4a33..ca76fa8c7 100644 --- a/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift +++ b/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift @@ -81,33 +81,11 @@ extension Bolus { label: { Text("Enact bolus") } .disabled(state.amount <= 0) } - Section { - if waitForSuggestion { + if waitForSuggestion { + Section { Button { state.showModal(for: nil) } label: { Text("Continue without bolus") } - } else { - Button { isAddInsulinAlertPresented = true } - label: { Text("Add insulin without actually bolusing") } - .disabled(state.amount <= 0) - } - } - .alert(isPresented: $isAddInsulinAlertPresented) { - Alert( - title: Text("Are you sure?"), - message: Text( - NSLocalizedString("Add", comment: "Add insulin without bolusing alert") + " " + formatter - .string(from: state.amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit") + - NSLocalizedString(" without bolusing", comment: "Add insulin without bolusing alert") - ), - primaryButton: .destructive( - Text("Add"), - action: { - state.addWithoutBolus() - isAddInsulinAlertPresented = false - } - ), - secondaryButton: .cancel() - ) + }.frame(maxWidth: .infinity, alignment: .center) } } } diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift b/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift index 974ced7e8..11d849278 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift @@ -66,8 +66,9 @@ enum DataTable { let isFPU: Bool? let fpuID: String? let note: String? + let isExternal: Bool? - private var numberFormater: NumberFormatter { + private var numberFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 2 @@ -92,7 +93,8 @@ enum DataTable { idPumpEvent: String? = nil, isFPU: Bool? = false, fpuID: String? = nil, - note: String? = nil + note: String? = nil, + isExternal: Bool? = nil ) { self.units = units self.type = type @@ -105,6 +107,7 @@ enum DataTable { self.isFPU = isFPU self.fpuID = fpuID self.note = note + self.isExternal = isExternal } static func == (lhs: Treatment, rhs: Treatment) -> Bool { @@ -126,14 +129,21 @@ enum DataTable { switch type { case .carbs: - return numberFormater.string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carbs") + return numberFormatter.string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carbs") case .fpus: - return numberFormater + return numberFormatter .string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carb equilvalents") case .bolus: - return numberFormater.string(from: amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit") + var bolusText = "" + + if isExternal ?? false { + bolusText += " " + NSLocalizedString("External", comment: "External Insulin") + } + + return numberFormatter + .string(from: amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit") + bolusText case .tempBasal: - return numberFormater + return numberFormatter .string(from: amount as NSNumber)! + NSLocalizedString(" U/hr", comment: "Unit insulin per hour") case .tempTarget: var converted = amount @@ -142,7 +152,7 @@ enum DataTable { } guard var secondAmount = secondAmount else { - return numberFormater.string(from: converted as NSNumber)! + " \(units.rawValue)" + return numberFormatter.string(from: converted as NSNumber)! + " \(units.rawValue)" } if units == .mmolL { secondAmount = secondAmount.asMmolL @@ -177,7 +187,7 @@ enum DataTable { guard let duration = duration, duration > 0 else { return nil } - return numberFormater.string(from: duration as NSNumber)! + " min" + return numberFormatter.string(from: duration as NSNumber)! + " min" } } diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift index 3bd4bd98b..0970e2b47 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift @@ -13,6 +13,12 @@ extension DataTable { pumpHistoryStorage.recent() } + func pumpSettings() -> PumpSettings { + storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self) + ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings)) + ?? PumpSettings(insulinActionCurve: 6, maxBolus: 10, maxBasal: 2) + } + func tempTargets() -> [TempTarget] { tempTargetsStorage.recent() } diff --git a/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift b/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift index cc80c97a0..7828d2e82 100644 --- a/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift +++ b/FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift @@ -6,6 +6,7 @@ extension DataTable { @Injected() var broadcaster: Broadcaster! @Injected() var unlockmanager: UnlockManager! @Injected() private var storage: FileStorage! + @Injected() var pumpHistoryStorage: PumpHistoryStorage! let coredataContext = CoreDataStack.shared.persistentContainer.viewContext @@ -13,11 +14,15 @@ extension DataTable { @Published var treatments: [Treatment] = [] @Published var glucose: [Glucose] = [] @Published var manualGlcuose: Decimal = 0 + @Published var maxBolus: Decimal = 0 + @Published var externalInsulinAmount: Decimal = 0 + @Published var externalInsulinDate = Date() var units: GlucoseUnits = .mmolL override func subscribe() { units = settingsManager.settings.units + maxBolus = provider.pumpSettings().maxBolus setupTreatments() setupGlucose() broadcaster.register(SettingsObserver.self, observer: self) @@ -66,7 +71,14 @@ extension DataTable { let boluses = self.provider.pumpHistory() .filter { $0.type == .bolus } .map { - Treatment(units: units, type: .bolus, date: $0.timestamp, amount: $0.amount, idPumpEvent: $0.id) + Treatment( + units: units, + type: .bolus, + date: $0.timestamp, + amount: $0.amount, + idPumpEvent: $0.id, + isExternal: $0.isExternalInsulin + ) } let tempBasals = self.provider.pumpHistory() @@ -181,6 +193,40 @@ extension DataTable { provider.glucoseStorage.storeGlucose([saveToJSON]) debug(.default, "Manual Glucose saved to glucose.json") } + + func addExternalInsulin() { + guard externalInsulinAmount > 0 else { + showModal(for: nil) + return + } + + externalInsulinAmount = min(externalInsulinAmount, maxBolus * 3) // Allow for 3 * Max Bolus for external insulin + unlockmanager.unlock() + .sink { _ in } receiveValue: { [weak self] _ in + guard let self = self else { return } + pumpHistoryStorage.storeEvents( + [ + PumpHistoryEvent( + id: UUID().uuidString, + type: .bolus, + timestamp: externalInsulinDate, + amount: externalInsulinAmount, + duration: nil, + durationMin: nil, + rate: nil, + temp: nil, + carbInput: nil, + isExternalInsulin: true + ) + ] + ) + debug(.default, "External insulin saved to pumphistory.json") + + // Reset amount to 0 for next entry + externalInsulinAmount = 0 + } + .store(in: &lifetime) + } } } diff --git a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift index 073884ed6..8f7eafc68 100644 --- a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift +++ b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift @@ -12,9 +12,18 @@ extension DataTable { @State private var isRemoveInsulinAlertPresented = false @State private var removeInsulinAlert: Alert? @State private var newGlucose = false + @State private var showExternalInsulin = false + @State private var isAmountUnconfirmed = true @Environment(\.colorScheme) var colorScheme + private var insulinFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 2 + return formatter + } + private var glucoseFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -57,6 +66,14 @@ extension DataTable { leading: Button("Close", action: state.hideModal), trailing: state.mode == .glucose ? EditButton().asAny() : EmptyView().asAny() ) + .sheet(isPresented: $showExternalInsulin, onDismiss: { + if isAmountUnconfirmed { + state.externalInsulinAmount = 0 + state.externalInsulinDate = Date() + } + }) { + addExternalInsulinView + } .popup(isPresented: newGlucose, alignment: .top, direction: .bottom) { VStack(spacing: 20) { HStack { @@ -90,8 +107,30 @@ extension DataTable { private var treatmentsList: some View { List { - ForEach(state.treatments) { item in - treatmentView(item) + HStack { + Spacer() + Button(action: { showExternalInsulin = true + state.externalInsulinDate = Date() }, label: { + HStack { + Text("Add") + .foregroundColor(Color.secondary) + .font(.caption) + + Image(systemName: "syringe") + .foregroundColor(Color.accentColor) + }.frame(maxWidth: .infinity, alignment: .trailing) + + }).buttonStyle(.borderless) + } + + if !state.treatments.isEmpty { + ForEach(state.treatments) { item in + treatmentView(item) + } + } else { + HStack { + Text("No data.") + } } } } @@ -191,6 +230,70 @@ extension DataTable { } } + var addExternalInsulinView: some View { + NavigationView { + VStack { + Form { + Section { + HStack { + Text("Amount") + Spacer() + DecimalTextField( + "0", + value: $state.externalInsulinAmount, + formatter: insulinFormatter, + autofocus: true, + cleanInput: true + ) + Text("U").foregroundColor(.secondary) + } + } + + Section { + DatePicker("Date", selection: $state.externalInsulinDate, in: ...Date()) + } + + let amountWarningCondition = (state.externalInsulinAmount > state.maxBolus) && + (state.externalInsulinAmount <= state.maxBolus * 3) + + Section { + HStack { + Button { + state.addExternalInsulin() + isAmountUnconfirmed = false + showExternalInsulin = false + } + label: { + Text("Log external insulin") + } + .foregroundColor(amountWarningCondition ? Color.white : Color.accentColor) + .frame(maxWidth: .infinity, alignment: .center) + .disabled( + state.externalInsulinAmount <= 0 || state.externalInsulinAmount > state + .maxBolus * 3 + ) + } + } + header: { + if amountWarningCondition + { + Text("⚠️ Warning! The entered insulin amount is greater than your Max Bolus setting!") + } + } + .listRowBackground( + amountWarningCondition ? Color + .red : colorScheme == .dark ? Color(UIColor.secondarySystemBackground) : Color.white + ) + } + } + .onAppear(perform: configureView) + .navigationTitle("External Insulin") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(leading: Button("Close", action: { showExternalInsulin = false + state.externalInsulinAmount = 0 })) + } + } + @ViewBuilder private func glucoseView(_ item: Glucose) -> some View { VStack(alignment: .leading, spacing: 4) { HStack {