Explorar o código

Alternate Bolus Calculator and Edit of Meals

* Implement new Bolus Calculator, by @polscm32, in Swift.

* Refactor the new calculator above and the UI/UX of the new Bolus View

* Add  Feature: Edit the Meals from the Bolus View . Go from the  Carbs View <--> Bolus View .
Jon B Mårtensson %!s(int64=2) %!d(string=hai) anos
pai
achega
044f5d5a1b
Modificáronse 33 ficheiros con 1274 adicións e 433 borrados
  1. 9 1
      Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
  2. 52 0
      FreeAPS.xcodeproj/project.pbxproj
  3. 5 1
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  4. 11 20
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  5. 2 2
      FreeAPS/Sources/Models/CarbsEntry.swift
  6. 20 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  7. 2 0
      FreeAPS/Sources/Models/NightscoutTreatment.swift
  8. 2 0
      FreeAPS/Sources/Models/Suggestion.swift
  9. 51 14
      FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift
  10. 7 2
      FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift
  11. 144 36
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  12. 23 0
      FreeAPS/Sources/Modules/Bolus/Components/CheckboxToggleStyle.swift
  13. 521 0
      FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift
  14. 8 261
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  15. 275 0
      FreeAPS/Sources/Modules/Bolus/View/DefaultBolusCalcRootView.swift
  16. 5 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorConfigDataFlow.swift
  17. 3 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorConfigProvider.swift
  18. 27 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift
  19. 52 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  20. 1 1
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  21. 2 2
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  22. 1 1
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  23. 7 2
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  24. 2 0
      FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift
  25. 5 3
      FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift
  26. 1 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  27. 9 6
      FreeAPS/Sources/Router/Screen.swift
  28. 1 1
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  29. 6 5
      FreeAPS/Sources/Services/Network/NightscoutAPI.swift
  30. 18 48
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  31. 1 1
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  32. 1 1
      FreeAPS/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift
  33. 0 25
      FreeAPS/Sources/Views/DecimalTextField.swift

+ 9 - 1
Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="22G90" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="22G120" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
     <entity name="BGaverages" representedClassName="BGaverages" syncable="YES" codeGenerationType="class">
         <attribute name="average" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
@@ -54,6 +54,14 @@
         <attribute name="loopStatus" optional="YES" attributeType="String"/>
         <attribute name="start" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
     </entity>
+    <entity name="Meals" representedClassName="Meals" syncable="YES" codeGenerationType="class">
+        <attribute name="carbs" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
+        <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="fat" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
+        <attribute name="id" optional="YES" attributeType="String"/>
+        <attribute name="note" optional="YES" attributeType="String"/>
+        <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
+    </entity>
     <entity name="Oref0Suggestion" representedClassName="Oref0Suggestion" syncable="YES" codeGenerationType="class">
         <relationship name="computedInsulinDistribution" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InsulinDistribution" inverseName="insulin" inverseEntity="InsulinDistribution"/>
         <relationship name="computedTDD" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TDD" inverseName="computed" inverseEntity="TDD"/>

+ 52 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -301,6 +301,13 @@
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
 		BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
+		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
+		BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */; };
+		BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */; };
+		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
+		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
+		BDFD165A2AE40438007F0DDA /* AlternativeBolusCalcRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */; };
+		BDFD165C2AE40688007F0DDA /* DefaultBolusCalcRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
@@ -819,6 +826,13 @@
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
+		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
+		BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigDataFlow.swift; sourceTree = "<group>"; };
+		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
+		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
+		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
+		BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlternativeBolusCalcRootView.swift; sourceTree = "<group>"; };
+		BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultBolusCalcRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
@@ -1154,6 +1168,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */,
 				190EBCC229FF134900BA767D /* StatConfig */,
 				CE94597C29E9E1CD0047C9C6 /* WatchConfig */,
 				19F95FF129F10F9C00314DDC /* Stat */,
@@ -2039,6 +2054,35 @@
 			isa = PBXGroup;
 			children = (
 				10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */,
+				BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */,
+				BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
+		BD2FF19E2AE29D24005D1C5D /* Components */ = {
+			isa = PBXGroup;
+			children = (
+				BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */,
+			);
+			path = Components;
+			sourceTree = "<group>";
+		};
+		BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */ = {
+			isa = PBXGroup;
+			children = (
+				BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */,
+				BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */,
+				BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */,
+				BD7DA9AA2AE06E9600601B20 /* View */,
+			);
+			path = BolusCalculatorConfig;
+			sourceTree = "<group>";
+		};
+		BD7DA9AA2AE06E9600601B20 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2057,6 +2101,7 @@
 		C2C98283C436DB934D7E7994 /* Bolus */ = {
 			isa = PBXGroup;
 			children = (
+				BD2FF19E2AE29D24005D1C5D /* Components */,
 				C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */,
 				C19984D62EFC0035A9E9644D /* BolusProvider.swift */,
 				223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */,
@@ -2632,6 +2677,7 @@
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				19A910302A24BF6300C8951B /* StatsView.swift in Sources */,
+				BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */,
 				CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */,
 				190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
@@ -2663,6 +2709,7 @@
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
+				BDFD165A2AE40438007F0DDA /* AlternativeBolusCalcRootView.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
 				190EBCC429FF136900BA767D /* StatConfigDataFlow.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
@@ -2688,6 +2735,7 @@
 				9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */,
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
+				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
@@ -2729,6 +2777,7 @@
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
 				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,
+				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				FEFA5C0F299F810B00765C17 /* Core_Data.xcdatamodeld in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
@@ -2768,6 +2817,7 @@
 				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
 				38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */,
 				5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */,
+				BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */,
 				E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */,
 				919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */,
 				8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */,
@@ -2780,6 +2830,7 @@
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
+				BDFD165C2AE40688007F0DDA /* DefaultBolusCalcRootView.swift in Sources */,
 				19D4E4EB29FC6A9F00351451 /* TIRforChart.swift in Sources */,
 				FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */,
 				F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */,
@@ -2807,6 +2858,7 @@
 				F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */,
 				BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */,
 				61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */,
+				BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */,
 				6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				903D18976088B09110BCBE29 /* LibreConfigStateModel.swift in Sources */,

+ 5 - 1
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -42,5 +42,9 @@
   "oneDimensionalGraph" : false,
   "rulerMarks" : false,
   "maxCarbs": 1000,
-  "displayFatAndProteinOnWatch": false
+  "displayFatAndProteinOnWatch": false,
+  "overrideFactor": 0.8,
+  "useCalc": false,
+  "fattyMeals": false,
+  "fattyMealFactor": 0.7
 }

+ 11 - 20
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -12,7 +12,7 @@ protocol CarbsStorage {
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
-    func deleteCarbs(at date: Date)
+    func deleteCarbs(at uniqueID: String)
 }
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -71,7 +71,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 // New date for each carb equivalent
                 var useDate = entries.last?.createdAt ?? Date()
                 // Group and Identify all FPUs together
-                let fpuID = UUID().uuidString
+                let fpuID = (entries.last?.collectionID ?? "") + ".fpu"
                 // Create an array of all future carb equivalents.
                 var futureCarbArray = [CarbsEntry]()
                 while carbEquivalents > 0, numberOfEquivalents > 0 {
@@ -81,7 +81,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) }
 
                     let eachCarbEntry = CarbsEntry(
-                        id: UUID().uuidString, createdAt: useDate, carbs: equivalent, fat: 0, protein: 0, note: nil,
+                        collectionID: fpuID, createdAt: useDate, carbs: equivalent, fat: 0, protein: 0, note: nil,
                         enteredBy: CarbsEntry.manual, isFPU: true,
                         fpuID: fpuID
                     )
@@ -101,7 +101,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             } // ------------------------- END OF TPU ----------------------------------------
             // Store the actual (normal) carbs
             if entries.last?.carbs ?? 0 > 0 {
-                uniqEvents = []
+                // uniqEvents = []
                 self.storage.transaction { storage in
                     storage.append(entries, to: file, uniqBy: \.createdAt)
                     uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
@@ -143,24 +143,14 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
     }
 
-    func deleteCarbs(at date: Date) {
+    func deleteCarbs(at uniqueID: String) {
         processQueue.sync {
             var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
 
-            guard let entryIndex = allValues.firstIndex(where: { $0.createdAt == date }) else {
-                return
-            }
-
-            // If deleteing a FPUs remove all of those with the same ID
-            if allValues[entryIndex].isFPU != nil, allValues[entryIndex].isFPU ?? false {
-                let fpuString = allValues[entryIndex].fpuID
-                allValues.removeAll(where: { $0.fpuID == fpuString })
-                storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
-                broadcaster.notify(CarbsObserver.self, on: processQueue) {
-                    $0.carbsDidUpdate(allValues)
-                }
+            if allValues.firstIndex(where: { $0.collectionID == uniqueID }) == nil {
+                debug(.default, "Didn't find any carb entries to delete. ID to search for: " + uniqueID.description)
             } else {
-                allValues.remove(at: entryIndex)
+                allValues.removeAll(where: { $0.collectionID == uniqueID })
                 storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
                 broadcaster.notify(CarbsObserver.self, on: processQueue) {
                     $0.carbsDidUpdate(allValues)
@@ -170,7 +160,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
 
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? []
+        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCarbs, as: [NigtscoutTreatment].self) ?? []
 
         let eventsManual = recent().filter { $0.enteredBy == CarbsEntry.manual }
         let treatments = eventsManual.map {
@@ -190,7 +180,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 protein: nil,
                 foodType: $0.note,
                 targetTop: nil,
-                targetBottom: nil
+                targetBottom: nil,
+                collectionID: $0.collectionID
             )
         }
         return Array(Set(treatments).subtracting(Set(uploaded)))

+ 2 - 2
FreeAPS/Sources/Models/CarbsEntry.swift

@@ -1,7 +1,7 @@
 import Foundation
 
 struct CarbsEntry: JSON, Equatable, Hashable {
-    let id: String?
+    let collectionID: String?
     let createdAt: Date
     let carbs: Decimal
     let fat: Decimal?
@@ -25,7 +25,7 @@ struct CarbsEntry: JSON, Equatable, Hashable {
 
 extension CarbsEntry {
     private enum CodingKeys: String, CodingKey {
-        case id = "_id"
+        case collectionID = "_id"
         case createdAt = "created_at"
         case carbs
         case fat

+ 20 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -45,6 +45,10 @@ struct FreeAPSSettings: JSON, Equatable {
     var maxCarbs: Decimal = 1000
     var displayFatAndProteinOnWatch: Bool = false
     var onlyAutotuneBasals: Bool = false
+    var overrideFactor: Decimal = 0.8
+    var useCalc: Bool = false
+    var fattyMeals: Bool = false
+    var fattyMealFactor: Decimal = 0.7
 }
 
 extension FreeAPSSettings: Decodable {
@@ -139,6 +143,22 @@ extension FreeAPSSettings: Decodable {
             settings.individualAdjustmentFactor = individualAdjustmentFactor
         }
 
+        if let useCalc = try? container.decode(Bool.self, forKey: .useCalc) {
+            settings.useCalc = useCalc
+        }
+
+        if let fattyMeals = try? container.decode(Bool.self, forKey: .fattyMeals) {
+            settings.fattyMeals = fattyMeals
+        }
+
+        if let fattyMealFactor = try? container.decode(Decimal.self, forKey: .fattyMealFactor) {
+            settings.fattyMealFactor = fattyMealFactor
+        }
+
+        if let overrideFactor = try? container.decode(Decimal.self, forKey: .overrideFactor) {
+            settings.overrideFactor = overrideFactor
+        }
+
         if let timeCap = try? container.decode(Int.self, forKey: .timeCap) {
             settings.timeCap = timeCap
         }

+ 2 - 0
FreeAPS/Sources/Models/NightscoutTreatment.swift

@@ -21,6 +21,7 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable {
     var glucoseType: String?
     var glucose: String?
     var units: String?
+    var collectionID: String?
 
     static let local = "iAPS"
 
@@ -57,5 +58,6 @@ extension NigtscoutTreatment {
         case glucoseType
         case glucose
         case units
+        case collectionID
     }
 }

+ 2 - 0
FreeAPS/Sources/Models/Suggestion.swift

@@ -29,6 +29,7 @@ struct Suggestion: JSON, Equatable {
     let minGuardBG: Decimal?
     let minPredBG: Decimal?
     let threshold: Decimal?
+    let carbRatio: Decimal?
 }
 
 struct Predictions: JSON, Equatable {
@@ -75,6 +76,7 @@ extension Suggestion {
         case minGuardBG
         case minPredBG
         case threshold
+        case carbRatio = "CR"
     }
 }
 

+ 51 - 14
FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift

@@ -18,6 +18,8 @@ extension AddCarbs {
         @Published var summation: [String] = []
         @Published var maxCarbs: Decimal = 0
         @Published var note: String = ""
+        @Published var id_: String = ""
+        @Published var summary: String = ""
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
@@ -33,25 +35,28 @@ extension AddCarbs {
                 return
             }
             carbs = min(carbs, maxCarbs)
-
-            carbsStorage.storeCarbs(
-                [CarbsEntry(
-                    id: UUID().uuidString,
-                    createdAt: date,
-                    carbs: carbs,
-                    fat: fat,
-                    protein: protein,
-                    note: note,
-                    enteredBy: CarbsEntry.manual,
-                    isFPU: false, fpuID: nil
-                )]
-            )
+            id_ = UUID().uuidString
+
+            let carbsToStore = [CarbsEntry(
+                collectionID: id_,
+                createdAt: date,
+                carbs: carbs,
+                fat: fat,
+                protein: protein,
+                note: note,
+                enteredBy: CarbsEntry.manual,
+                isFPU: false, fpuID: nil
+            )]
+            carbsStorage.storeCarbs(carbsToStore)
 
             if settingsManager.settings.skipBolusScreenAfterCarbs {
                 apsManager.determineBasalSync()
                 showModal(for: nil)
+            } else if carbs > 0 {
+                saveToCoreData(carbsToStore)
+                showModal(for: .bolus(waitForSuggestion: true, fetch: true))
             } else {
-                showModal(for: .bolus(waitForSuggestion: true))
+                hideModal()
             }
         }
 
@@ -160,5 +165,37 @@ extension AddCarbs {
             }
             return waitersNotepadString
         }
+
+        func loadEntries(_ editMode: Bool) {
+            if editMode {
+                coredataContext.perform {
+                    var mealToEdit = [Meals]()
+                    let requestMeal = Meals.fetchRequest() as NSFetchRequest<Meals>
+                    let sortMeal = NSSortDescriptor(key: "createdAt", ascending: false)
+                    requestMeal.sortDescriptors = [sortMeal]
+                    requestMeal.fetchLimit = 1
+                    try? mealToEdit = self.coredataContext.fetch(requestMeal)
+
+                    self.carbs = Decimal(mealToEdit.first?.carbs ?? 0)
+                    self.fat = Decimal(mealToEdit.first?.fat ?? 0)
+                    self.protein = Decimal(mealToEdit.first?.protein ?? 0)
+                    self.note = mealToEdit.first?.note ?? ""
+                    self.id_ = mealToEdit.first?.id ?? ""
+                }
+            }
+        }
+
+        func saveToCoreData(_ stored: [CarbsEntry]) {
+            let save = Meals(context: coredataContext)
+            save.createdAt = stored.first?.createdAt ?? .distantPast
+            save.id = stored.first?.collectionID ?? ""
+            save.carbs = Double(stored.first?.carbs ?? 0)
+            save.fat = Double(stored.first?.fat ?? 0)
+            save.protein = Double(stored.first?.protein ?? 0)
+            save.note = stored.first?.note ?? ""
+            if coredataContext.hasChanges {
+                try? coredataContext.save()
+            }
+        }
     }
 }

+ 7 - 2
FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift

@@ -5,6 +5,7 @@ import Swinject
 extension AddCarbs {
     struct RootView: BaseView {
         let resolver: Resolver
+        let editMode: Bool
         @StateObject var state = StateModel()
         @State var dish: String = ""
         @State var isPromptPresented = false
@@ -118,7 +119,7 @@ extension AddCarbs {
 
                 Section {
                     Button { state.add() }
-                    label: { Text("Save and continue") }
+                    label: { Text(state.carbs > 0 ? "Save and continue" : "Save") }
                         .disabled(state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
                         .frame(maxWidth: .infinity, alignment: .center)
                 } footer: { Text(state.waitersNotepad().description) }
@@ -129,7 +130,11 @@ extension AddCarbs {
                     }
                 }
             }
-            .onAppear(perform: configureView)
+            .onAppear {
+                configureView {
+                    state.loadEntries(editMode)
+                }
+            }
             .navigationTitle("Add Meals")
             .navigationBarTitleDisplayMode(.inline)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))

+ 144 - 36
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -1,3 +1,5 @@
+
+import LoopKit
 import SwiftUI
 import Swinject
 
@@ -7,28 +9,63 @@ extension Bolus {
         @Injected() var apsManager: APSManager!
         @Injected() var broadcaster: Broadcaster!
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
+        // added for bolus calculator
+        @Injected() var glucoseStorage: GlucoseStorage!
+        @Injected() var settings: SettingsManager!
+        @Injected() var nsManager: NightscoutManager!
 
+        @Published var suggestion: Suggestion?
         @Published var amount: Decimal = 0
         @Published var insulinRecommended: Decimal = 0
         @Published var insulinRequired: Decimal = 0
-        @Published var waitForSuggestion: Bool = false
-        @Published var error: Bool = false
+        @Published var units: GlucoseUnits = .mmolL
+        @Published var percentage: Decimal = 0
+        @Published var threshold: Decimal = 0
+        @Published var maxBolus: Decimal = 0
         @Published var errorString: Decimal = 0
         @Published var evBG: Int = 0
         @Published var insulin: Decimal = 0
-        @Published var target: Decimal = 0
         @Published var isf: Decimal = 0
-        @Published var percentage: Decimal = 0
-        @Published var threshold: Decimal = 0
+        @Published var error: Bool = false
         @Published var minGuardBG: Decimal = 0
         @Published var minDelta: Decimal = 0
         @Published var expectedDelta: Decimal = 0
         @Published var minPredBG: Decimal = 0
-        @Published var units: GlucoseUnits = .mmolL
-        @Published var maxBolus: Decimal = 0
+        @Published var waitForSuggestion: Bool = false
+        @Published var carbRatio: Decimal = 0
 
         var waitForSuggestionInitial: Bool = false
 
+        // added for bolus calculator
+        @Published var recentGlucose: BloodGlucose?
+        @Published var target: Decimal = 0
+        @Published var cob: Decimal = 0
+        @Published var iob: Decimal = 0
+
+        @Published var currentBG: Decimal = 0
+        @Published var fifteenMinInsulin: Decimal = 0
+        @Published var deltaBG: Decimal = 0
+        @Published var targetDifferenceInsulin: Decimal = 0
+        @Published var wholeCobInsulin: Decimal = 0
+        @Published var iobInsulinReduction: Decimal = 0
+        @Published var wholeCalc: Decimal = 0
+        @Published var roundedWholeCalc: Decimal = 0
+        @Published var insulinCalculated: Decimal = 0
+        @Published var roundedInsulinCalculated: Decimal = 0
+        @Published var fraction: Decimal = 0
+        @Published var useCalc: Bool = false
+        @Published var basal: Decimal = 0
+        @Published var fattyMeals: Bool = false
+        @Published var fattyMealFactor: Decimal = 0
+        @Published var useFattyMealCorrectionFactor: Bool = false
+        @Published var eventualBG: Int = 0
+
+        @Published var meal: [CarbsEntry]?
+        @Published var carbs: Decimal = 0
+        @Published var fat: Decimal = 0
+        @Published var protein: Decimal = 0
+        @Published var note: String = ""
+
         override func subscribe() {
             setupInsulinRequired()
             broadcaster.register(SuggestionObserver.self, observer: self)
@@ -36,6 +73,11 @@ extension Bolus {
             percentage = settingsManager.settings.insulinReqPercentage
             threshold = provider.suggestion?.threshold ?? 0
             maxBolus = provider.pumpSettings().maxBolus
+            // added
+            fraction = settings.settings.overrideFactor
+            useCalc = settings.settings.useCalc
+            fattyMeals = settings.settings.fattyMeals
+            fattyMealFactor = settings.settings.fattyMealFactor
 
             if waitForSuggestionInitial {
                 apsManager.determineBasal()
@@ -51,13 +93,79 @@ extension Bolus {
             }
         }
 
+        func getDeltaBG() {
+            let glucose = glucoseStorage.recent()
+            guard glucose.count >= 3 else { return }
+            let lastGlucose = glucose.last!
+            let thirdLastGlucose = glucose[glucose.count - 3]
+            let delta = Decimal(lastGlucose.glucose!) - Decimal(thirdLastGlucose.glucose!)
+            deltaBG = delta
+        }
+
+        // CALCULATIONS FOR THE BOLUS CALCULATOR
+        func calculateInsulin() -> Decimal {
+            // for mmol conversion
+            var conversion: Decimal = 1.0
+            if units == .mmolL {
+                conversion = 0.0555
+            }
+            // insulin needed for the current blood glucose
+            let targetDifference = (currentBG - target) * conversion
+            targetDifferenceInsulin = targetDifference / isf
+
+            // more or less insulin because of bg trend in the last 15 minutes
+            fifteenMinInsulin = (deltaBG * conversion) / isf
+
+            // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
+            wholeCobInsulin = cob / carbRatio
+
+            // determine how much the calculator reduces/ increases the bolus because of IOB
+            iobInsulinReduction = (-1) * iob
+
+            // adding everything together
+            // add a calc for the case that no fifteenMinInsulin is available
+            if deltaBG != 0 {
+                wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin + fifteenMinInsulin)
+            } else {
+                // add (rare) case that no glucose value is available -> maybe display warning?
+                // if no bg is available, ?? sets its value to 0
+                if currentBG == 0 {
+                    wholeCalc = (iobInsulinReduction + wholeCobInsulin)
+                } else {
+                    wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin)
+                }
+            }
+            // rounding
+            let wholeCalcAsDouble = Double(wholeCalc)
+            roundedWholeCalc = Decimal(round(100 * wholeCalcAsDouble) / 100)
+
+            // apply custom factor at the end of the calculations
+            let result = wholeCalc * fraction
+
+            // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
+            if useFattyMealCorrectionFactor {
+                insulinCalculated = result * fattyMealFactor
+            } else {
+                insulinCalculated = result
+            }
+
+            // display no negative insulinCalculated
+            insulinCalculated = max(insulinCalculated, 0)
+            let insulinCalculatedAsDouble = Double(insulinCalculated)
+            roundedInsulinCalculated = Decimal(round(100 * insulinCalculatedAsDouble) / 100)
+
+            insulinCalculated = min(insulinCalculated, maxBolus)
+
+            return insulinCalculated
+        }
+
         func add() {
             guard amount > 0 else {
                 showModal(for: nil)
                 return
             }
 
-            let maxAmount = Double(min(amount, maxBolus))
+            let maxAmount = Double(min(amount, provider.pumpSettings().maxBolus))
 
             unlockmanager.unlock()
                 .sink { _ in } receiveValue: { [weak self] _ in
@@ -68,38 +176,10 @@ extension Bolus {
                 .store(in: &lifetime)
         }
 
-        func addWithoutBolus() {
-            guard amount > 0 else {
-                showModal(for: nil)
-                return
-            }
-            amount = min(amount, maxBolus * 3) // Allow for 3 * Max Bolus for non-pump insulin
-
-            pumpHistoryStorage.storeEvents(
-                [
-                    PumpHistoryEvent(
-                        id: UUID().uuidString,
-                        type: .bolus,
-                        timestamp: Date(),
-                        amount: amount,
-                        duration: nil,
-                        durationMin: nil,
-                        rate: nil,
-                        temp: nil,
-                        carbInput: nil,
-                        isExternal: true
-                    )
-                ]
-            )
-            showModal(for: nil)
-        }
-
         func setupInsulinRequired() {
             DispatchQueue.main.async {
                 self.insulinRequired = self.provider.suggestion?.insulinReq ?? 0
 
-                // Manual Bolus recommendation (normally) yields a higher amount than the insulin reqiured amount computed for SMBs (auto boluses). A manual bolus threfore now (test) uses the Eventual BG for glucose prediction, whereas the insulinReg for SMBs uses the minPredBG for glucose prediction (typically lower than Eventual BG).
-
                 var conversion: Decimal = 1.0
                 if self.units == .mmolL {
                     conversion = 0.0555
@@ -109,6 +189,11 @@ extension Bolus {
                 self.insulin = self.provider.suggestion?.insulinForManualBolus ?? 0
                 self.target = self.provider.suggestion?.current_target ?? 0
                 self.isf = self.provider.suggestion?.isf ?? 0
+                self.iob = self.provider.suggestion?.iob ?? 0
+                self.currentBG = (self.provider.suggestion?.bg ?? 0)
+                self.cob = self.provider.suggestion?.cob ?? 0
+                self.basal = self.provider.suggestion?.rate ?? 0
+                self.carbRatio = self.provider.suggestion?.carbRatio ?? 0
 
                 if self.settingsManager.settings.insulinReqPercentage != 100 {
                     self.insulinRecommended = self.insulin * (self.settingsManager.settings.insulinReqPercentage / 100)
@@ -125,7 +210,30 @@ extension Bolus {
 
                 self.insulinRecommended = self.apsManager
                     .roundBolus(amount: max(self.insulinRecommended, 0))
+
+                if self.useCalc {
+                    self.getDeltaBG()
+                    self.insulinCalculated = self.calculateInsulin()
+                }
+            }
+        }
+
+        func backToCarbsView(complexEntry: Bool, _ id: String) {
+            if complexEntry {
+                DispatchQueue.safeMainSync {
+                    nsManager.deleteCarbs(
+                        at: id, isFPU: nil, fpuID: nil, syncID: id
+                    )
+                    nsManager.deleteCarbs(
+                        at: id + ".fpu", isFPU: nil, fpuID: nil, syncID: id
+                    )
+                }
+            } else {
+                nsManager.deleteCarbs(
+                    at: id, isFPU: nil, fpuID: nil, syncID: id
+                )
             }
+            showModal(for: .addCarbs(editMode: complexEntry))
         }
     }
 }

+ 23 - 0
FreeAPS/Sources/Modules/Bolus/Components/CheckboxToggleStyle.swift

@@ -0,0 +1,23 @@
+import SwiftUI
+
+struct CheckboxToggleStyle: ToggleStyle {
+    func makeBody(configuration: Self.Configuration) -> some View {
+        HStack {
+            RoundedRectangle(cornerRadius: 5)
+                .stroke(lineWidth: 2)
+                .frame(width: 20, height: 20)
+                .cornerRadius(5)
+                .overlay {
+                    if configuration.isOn {
+                        Image(systemName: "checkmark")
+                    }
+                }
+                .onTapGesture {
+                    withAnimation {
+                        configuration.isOn.toggle()
+                    }
+                }
+            configuration.label
+        }
+    }
+}

+ 521 - 0
FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift

@@ -0,0 +1,521 @@
+import Charts
+import CoreData
+import SwiftUI
+import Swinject
+
+extension Bolus {
+    struct AlternativeBolusCalcRootView: BaseView {
+        let resolver: Resolver
+        let waitForSuggestion: Bool
+        let fetch: Bool
+        @StateObject var state: StateModel
+        @State private var showInfo = false
+        @State private var exceededMaxBolus = false
+
+        private enum Config {
+            static let dividerHeight: CGFloat = 2
+            static let overlayColour: Color = .white // Currently commented out
+            static let spacing: CGFloat = 3
+        }
+
+        @Environment(\.colorScheme) var colorScheme
+
+        @FetchRequest(
+            entity: Meals.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "createdAt", ascending: false)]
+        ) var meal: FetchedResults<Meals>
+
+        private var formatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
+        private var mealFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 1
+            return formatter
+        }
+
+        private var gluoseFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            if state.units == .mmolL {
+                formatter.maximumFractionDigits = 1
+            } else { formatter.maximumFractionDigits = 0 }
+            return formatter
+        }
+
+        private var fractionDigits: Int {
+            if state.units == .mmolL {
+                return 1
+            } else { return 0 }
+        }
+
+        var body: some View {
+            Form {
+                if state.waitForSuggestion {
+                    HStack {
+                        Text("Wait please").foregroundColor(.secondary)
+                        Spacer()
+                        ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
+                    }
+                }
+                Section {
+                    if fetch {
+                        VStack {
+                            if let carbs = meal.first?.carbs, carbs > 0 {
+                                HStack {
+                                    Text("Carbs")
+                                    Spacer()
+                                    Text(carbs.formatted())
+                                    Text("g")
+                                }.foregroundColor(.secondary)
+                            }
+                            if let fat = meal.first?.fat, fat > 0 {
+                                HStack {
+                                    Text("Fat")
+                                    Spacer()
+                                    Text(fat.formatted())
+                                    Text("g")
+                                }.foregroundColor(.secondary)
+                            }
+                            if let protein = meal.first?.protein, protein > 0 {
+                                HStack {
+                                    Text("Protein")
+                                    Spacer()
+                                    Text(protein.formatted())
+                                    Text("g")
+                                }.foregroundColor(.secondary)
+                            }
+                            if let note = meal.first?.note, note != "" {
+                                HStack {
+                                    Text("Note")
+                                    Spacer()
+                                    Text(note)
+                                }.foregroundColor(.secondary)
+                            }
+                        }
+                    } else {
+                        Text("No Meal")
+                    }
+                } header: { Text("Meal Summary") }
+
+                Section {
+                    Button {
+                        let id_ = meal.first?.id ?? ""
+                        state.backToCarbsView(complexEntry: fetch, id_)
+                    }
+                    label: { Text("Edit Meal / Add Meal") }.frame(maxWidth: .infinity, alignment: .center)
+                }
+
+                Section {
+                    HStack {
+                        Button(action: {
+                            showInfo.toggle()
+                        }, label: {
+                            Image(systemName: "info.circle")
+                            Text("Calculations")
+                        })
+                            .foregroundStyle(.blue)
+                            .font(.footnote)
+                            .buttonStyle(PlainButtonStyle())
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                        if state.fattyMeals {
+                            Spacer()
+                            Toggle(isOn: $state.useFattyMealCorrectionFactor) {
+                                Text("Fatty Meal")
+                            }
+                            .toggleStyle(CheckboxToggleStyle())
+                            .font(.footnote)
+                            .onChange(of: state.useFattyMealCorrectionFactor) { _ in
+                                state.insulinCalculated = state.calculateInsulin()
+                            }
+                        }
+                    }
+
+                    if state.waitForSuggestion {
+                        HStack {
+                            Text("Wait please").foregroundColor(.secondary)
+                            Spacer()
+                            ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
+                        }
+                    } else {
+                        HStack {
+                            Text("Recommended Bolus")
+                            Spacer()
+                            Text(
+                                formatter
+                                    .string(from: Double(state.insulinCalculated) as NSNumber) ?? ""
+                            )
+                            Text(
+                                NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
+                            ).foregroundColor(.secondary)
+                        }.contentShape(Rectangle())
+                            .onTapGesture { state.amount = state.insulinCalculated }
+                    }
+
+                    if !state.waitForSuggestion {
+                        HStack {
+                            Text("Bolus")
+                            Spacer()
+                            DecimalTextField(
+                                "0",
+                                value: $state.amount,
+                                formatter: formatter,
+                                autofocus: false,
+                                cleanInput: true
+                            )
+                            Text(exceededMaxBolus ? "😵" : " U").foregroundColor(.secondary)
+                        }
+                        .onChange(of: state.amount) { newValue in
+                            if newValue > state.maxBolus {
+                                exceededMaxBolus = true
+                            } else {
+                                exceededMaxBolus = false
+                            }
+                        }
+                    }
+                } header: { Text("Bolus Summary") }
+
+                Section {
+                    if state.amount == 0, waitForSuggestion {
+                        Button { state.showModal(for: nil) }
+                        label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
+                    } else {
+                        Button { state.add() }
+                        label: { Text(exceededMaxBolus ? "Max Bolus exceeded!" : "Enact bolus") }
+                            .frame(maxWidth: .infinity, alignment: .center)
+                            .foregroundColor(exceededMaxBolus ? .loopRed : .accentColor)
+                            .disabled(
+                                state.amount <= 0 || state.amount > state.maxBolus
+                            )
+                    }
+                }
+            }
+            .blur(radius: showInfo ? 3 : 0)
+            .navigationTitle("Enact Bolus")
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationBarItems(leading: Button("Close", action: state.hideModal))
+
+            .onAppear {
+                configureView {
+                    state.waitForSuggestionInitial = waitForSuggestion
+                    state.waitForSuggestion = waitForSuggestion
+                    state.insulinCalculated = state.calculateInsulin()
+                }
+            }
+
+            .popup(isPresented: showInfo) {
+                bolusInfoAlternativeCalculator
+            }
+        }
+
+        var changed: Bool {
+            ((meal.first?.carbs ?? 0) > 0) || ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
+        }
+
+        var hasFatOrProtein: Bool {
+            ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
+        }
+
+        // Pop-up
+        var bolusInfoAlternativeCalculator: some View {
+            VStack {
+                let unit = NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
+                VStack {
+                    VStack(spacing: Config.spacing) {
+                        HStack {
+                            Text("Calculations")
+                                .font(.title3).frame(maxWidth: .infinity, alignment: .center)
+                        }.padding(10)
+
+                        if fetch {
+                            VStack {
+                                if let note = meal.first?.note, note != "" {
+                                    HStack {
+                                        Text("Note")
+                                            .foregroundColor(.secondary)
+                                        Spacer()
+                                        Text(note)
+                                    }
+                                }
+                                if let carbs = meal.first?.carbs, carbs > 0 {
+                                    HStack {
+                                        Text("Carbs")
+                                            .foregroundColor(.secondary)
+                                        Spacer()
+                                        Text(mealFormatter.string(from: carbs as NSNumber) ?? "")
+                                        Text("g").foregroundColor(.secondary)
+                                    }
+                                }
+                                if let protein = meal.first?.protein, protein > 0 {
+                                    HStack {
+                                        Text("Protein")
+                                            .foregroundColor(.secondary)
+                                        Spacer()
+                                        Text(mealFormatter.string(from: protein as NSNumber) ?? "")
+                                        Text("g").foregroundColor(.secondary)
+                                    }
+                                }
+                                if let fat = meal.first?.fat, fat > 0 {
+                                    HStack {
+                                        Text("Fat")
+                                            .foregroundColor(.secondary)
+                                        Spacer()
+                                        Text(mealFormatter.string(from: fat as NSNumber) ?? "")
+                                        Text("g").foregroundColor(.secondary)
+                                    }
+                                }
+                            }.padding()
+                        }
+
+                        if fetch { Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
+                        }
+
+                        VStack {
+                            HStack {
+                                Text("Carb Ratio")
+                                    .foregroundColor(.secondary)
+                                Spacer()
+                                Text(state.carbRatio.formatted())
+                                Text(NSLocalizedString(" g/U", comment: " grams per Unit"))
+                                    .foregroundColor(.secondary)
+                            }
+                            HStack {
+                                Text("ISF")
+                                    .foregroundColor(.secondary)
+                                Spacer()
+                                let isf = state.isf
+                                Text(isf.formatted())
+                                Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
+                                    .foregroundColor(.secondary)
+                            }
+                            HStack {
+                                Text("Target Glucose")
+                                    .foregroundColor(.secondary)
+                                Spacer()
+                                let target = state.units == .mmolL ? state.target.asMmolL : state.target
+                                Text(
+                                    target
+                                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+                                )
+                                Text(state.units.rawValue)
+                                    .foregroundColor(.secondary)
+                            }
+                            HStack {
+                                Text("Basal")
+                                    .foregroundColor(.secondary)
+                                Spacer()
+                                let basal = state.basal
+                                Text(basal.formatted())
+                                Text(NSLocalizedString(" U/h", comment: " Units per hour"))
+                                    .foregroundColor(.secondary)
+                            }
+                            HStack {
+                                Text("Fraction")
+                                    .foregroundColor(.secondary)
+                                Spacer()
+                                let fraction = state.fraction
+                                Text(fraction.formatted())
+                            }
+                            if state.useFattyMealCorrectionFactor {
+                                HStack {
+                                    Text("Fatty Meal Factor")
+                                        .foregroundColor(.orange)
+                                    Spacer()
+                                    let fraction = state.fattyMealFactor
+                                    Text(fraction.formatted())
+                                        .foregroundColor(.orange)
+                                }
+                            }
+                        }.padding()
+                    }
+
+                    Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
+
+                    VStack(spacing: Config.spacing) {
+                        HStack {
+                            Text("Glucose")
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            let glucose = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
+                            Text(glucose.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
+                            Text(state.units.rawValue)
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            Image(systemName: "arrow.right")
+                            Spacer()
+
+                            let targetDifferenceInsulin = state.targetDifferenceInsulin
+                            // rounding
+                            let targetDifferenceInsulinAsDouble = NSDecimalNumber(decimal: targetDifferenceInsulin).doubleValue
+                            let roundedTargetDifferenceInsulin = Decimal(round(100 * targetDifferenceInsulinAsDouble) / 100)
+                            Text(roundedTargetDifferenceInsulin.formatted())
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                        }
+                        HStack {
+                            Text("IOB")
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            let iob = state.iob
+                            // rounding
+                            let iobAsDouble = NSDecimalNumber(decimal: iob).doubleValue
+                            let roundedIob = Decimal(round(100 * iobAsDouble) / 100)
+                            Text(roundedIob.formatted())
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                            Spacer()
+
+                            Image(systemName: "arrow.right")
+                            Spacer()
+
+                            let iobCalc = state.iobInsulinReduction
+                            // rounding
+                            let iobCalcAsDouble = NSDecimalNumber(decimal: iobCalc).doubleValue
+                            let roundedIobCalc = Decimal(round(100 * iobCalcAsDouble) / 100)
+                            Text(roundedIobCalc.formatted())
+                            Text(unit).foregroundColor(.secondary)
+                        }
+                        HStack {
+                            Text("Trend")
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            let trend = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
+                            Text(trend.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
+                            Text(state.units.rawValue).foregroundColor(.secondary)
+                            Spacer()
+
+                            Image(systemName: "arrow.right")
+                            Spacer()
+
+                            let trendInsulin = state.fifteenMinInsulin
+                            // rounding
+                            let trendInsulinAsDouble = NSDecimalNumber(decimal: trendInsulin).doubleValue
+                            let roundedTrendInsulin = Decimal(round(100 * trendInsulinAsDouble) / 100)
+                            Text(roundedTrendInsulin.formatted())
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                        }
+                        HStack {
+                            Text("COB")
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            let cob = state.cob
+                            Text(cob.formatted())
+
+                            let unitGrams = NSLocalizedString(" g", comment: "grams")
+                            Text(unitGrams).foregroundColor(.secondary)
+
+                            Spacer()
+
+                            Image(systemName: "arrow.right")
+                            Spacer()
+
+                            let insulinCob = state.wholeCobInsulin
+                            // rounding
+                            let insulinCobAsDouble = NSDecimalNumber(decimal: insulinCob).doubleValue
+                            let roundedInsulinCob = Decimal(round(100 * insulinCobAsDouble) / 100)
+                            Text(roundedInsulinCob.formatted())
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                        }
+                    }.padding()
+
+                    Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
+
+                    VStack {
+                        HStack {
+                            Text("Full Bolus")
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            let insulin = state.roundedWholeCalc
+                            Text(insulin.formatted()).foregroundStyle(state.roundedWholeCalc < 0 ? Color.loopRed : Color.primary)
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                        }
+                    }.padding(.horizontal)
+
+                    Divider().frame(height: Config.dividerHeight)
+
+                    VStack {
+                        HStack {
+                            Text("Result")
+                                .fontWeight(.bold)
+                            Spacer()
+                            let fraction = state.fraction
+                            Text(fraction.formatted())
+                            Text(" x ")
+                                .foregroundColor(.secondary)
+
+                            // if fatty meal is chosen
+                            if state.useFattyMealCorrectionFactor {
+                                let fattyMealFactor = state.fattyMealFactor
+                                Text(fattyMealFactor.formatted())
+                                    .foregroundColor(.orange)
+                                Text(" x ")
+                                    .foregroundColor(.secondary)
+                            }
+
+                            let insulin = state.roundedWholeCalc
+                            Text(insulin.formatted()).foregroundStyle(state.roundedWholeCalc < 0 ? Color.loopRed : Color.primary)
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                            Text(" = ")
+                                .foregroundColor(.secondary)
+
+                            let result = state.insulinCalculated
+                            // rounding
+                            let resultAsDouble = NSDecimalNumber(decimal: result).doubleValue
+                            let roundedResult = Decimal(round(100 * resultAsDouble) / 100)
+                            Text(roundedResult.formatted())
+                                .fontWeight(.bold)
+                                .font(.system(size: 16))
+                                .foregroundColor(.blue)
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                        }
+                    }.padding()
+
+                    Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
+
+                    if exceededMaxBolus {
+                        HStack {
+                            let maxBolus = state.maxBolus
+                            let maxBolusFormatted = maxBolus.formatted()
+                            Text("Your entered amount was limited by your max Bolus setting of \(maxBolusFormatted)\(unit)!")
+                        }
+                        .padding()
+                        .fontWeight(.semibold)
+                        .foregroundStyle(Color.loopRed)
+                    }
+                }
+                .padding(.top, 10)
+                .padding(.bottom, 15)
+
+                // Hide pop-up
+                VStack {
+                    Button {
+                        showInfo = false
+                    }
+                    label: {
+                        Text("OK")
+                    }
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .font(.system(size: 16))
+                    .fontWeight(.semibold)
+                    .foregroundColor(.blue)
+                }
+                .padding(.bottom, 20)
+            }
+            .font(.footnote)
+            .background(
+                RoundedRectangle(cornerRadius: 10, style: .continuous)
+                    .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4).opacity(0.9))
+            )
+        }
+    }
+}

+ 8 - 261
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -5,275 +5,22 @@ extension Bolus {
     struct RootView: BaseView {
         let resolver: Resolver
         let waitForSuggestion: Bool
+        let fetch: Bool
         @StateObject var state = StateModel()
 
-        @State private var isAddInsulinAlertPresented = false
-        @State private var presentInfo = false
-        @State private var displayError = false
-
-        @Environment(\.colorScheme) var colorScheme
-
-        private var formatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 2
-            return formatter
-        }
-
-        private var fractionDigits: Int {
-            if state.units == .mmolL {
-                return 1
-            } else { return 0 }
-        }
-
         var body: some View {
-            Form {
-                Section {
-                    if state.waitForSuggestion {
-                        HStack {
-                            Text("Wait please").foregroundColor(.secondary)
-                            Spacer()
-                            ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
-                        }
-                    } else {
-                        HStack {
-                            Text("Insulin recommended")
-                            Image(systemName: "info.bubble")
-                                .symbolRenderingMode(.palette)
-                                .foregroundStyle(.primary, .blue)
-                                .onTapGesture {
-                                    presentInfo.toggle()
-                                }
-
-                            Spacer()
-
-                            Text(
-                                formatter
-                                    .string(from: state.insulinRecommended as NSNumber)! +
-                                    NSLocalizedString(" U", comment: "Insulin unit")
-                            ).foregroundColor((state.error && state.insulinRecommended > 0) ? .red : .secondary)
-                                .onTapGesture {
-                                    if state.error, state.insulinRecommended > 0 { displayError = true }
-                                    else { state.amount = state.insulinRecommended }
-                                }
-                        }.contentShape(Rectangle())
-                    }
-                }
-                header: { Text("Recommendation") }
-                if !state.waitForSuggestion {
-                    Section {
-                        HStack {
-                            Text("Amount")
-                            Spacer()
-                            DecimalTextField(
-                                "0",
-                                value: $state.amount,
-                                formatter: formatter,
-                                autofocus: true,
-                                cleanInput: true
-                            )
-                            Text(!(state.amount > state.maxBolus) ? "U" : "😵").foregroundColor(.secondary)
-                        }
-                    }
-                    header: { Text("Bolus") }
-                    Section {
-                        Button { state.add() }
-                        label: { Text(!(state.amount > state.maxBolus) ? "Enact bolus" : "Max Bolus exceeded!") }
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .disabled(
-                                state.amount <= 0 || state.amount > state.maxBolus
-                            )
-                    }
-
-                    if waitForSuggestion {
-                        Section {
-                            Button { state.showModal(for: nil) }
-                            label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
-                        }
-                    }
-                }
-            }
-            .alert(isPresented: $displayError) {
-                Alert(
-                    title: Text("Warning!"),
-                    message: Text("\n" + alertString() + "\n"),
-                    primaryButton: .destructive(
-                        Text("Add"),
-                        action: {
-                            state.amount = state.insulinRecommended
-                            displayError = false
-                        }
-                    ),
-                    secondaryButton: .cancel()
-                )
-            }.onAppear {
-                configureView {
-                    state.waitForSuggestionInitial = waitForSuggestion
-                    state.waitForSuggestion = waitForSuggestion
-                }
-            }
-            .navigationTitle("Enact Bolus")
-            .navigationBarTitleDisplayMode(.inline)
-            .navigationBarItems(leading: Button("Close", action: state.hideModal))
-            .popup(isPresented: presentInfo, alignment: .center, direction: .bottom) {
-                bolusInfo
-            }
-        }
-
-        var bolusInfo: some View {
-            VStack {
-                // Variables
-                VStack(spacing: 3) {
-                    HStack {
-                        Text("Eventual Glucose").foregroundColor(.secondary)
-                        let evg = state.units == .mmolL ? Decimal(state.evBG).asMmolL : Decimal(state.evBG)
-                        Text(evg.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
-                    HStack {
-                        Text("Target Glucose").foregroundColor(.secondary)
-                        let target = state.units == .mmolL ? state.target.asMmolL : state.target
-                        Text(target.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
-                    HStack {
-                        Text("ISF").foregroundColor(.secondary)
-                        let isf = state.isf
-                        Text(isf.formatted())
-                        Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
-                            .foregroundColor(.secondary)
-                    }
-                    HStack {
-                        Text("ISF:")
-                        Text("Insulin Sensitivity")
-                    }.foregroundColor(.secondary).italic()
-                    if state.percentage != 100 {
-                        HStack {
-                            Text("Percentage setting").foregroundColor(.secondary)
-                            let percentage = state.percentage
-                            Text(percentage.formatted())
-                            Text("%").foregroundColor(.secondary)
-                        }
-                    }
-                    HStack {
-                        Text("Formula:")
-                        Text("(Eventual Glucose - Target) / ISF")
-                    }.foregroundColor(.secondary).italic().padding(.top, 5)
-                }
-                .font(.footnote)
-                .padding(.top, 10)
-                Divider()
-                // Formula
-                VStack(spacing: 5) {
-                    let unit = NSLocalizedString(
-                        " U",
-                        comment: "Unit in number of units delivered (keep the space character!)"
-                    )
-                    let color: Color = (state.percentage != 100 && state.insulin > 0) ? .secondary : .blue
-                    let fontWeight: Font.Weight = (state.percentage != 100 && state.insulin > 0) ? .regular : .bold
-                    HStack {
-                        Text(NSLocalizedString("Insulin recommended", comment: "") + ":").font(.callout)
-                        Text(state.insulin.formatted() + unit).font(.callout).foregroundColor(color).fontWeight(fontWeight)
-                    }
-                    if state.percentage != 100, state.insulin > 0 {
-                        Divider()
-                        HStack { Text(state.percentage.formatted() + " % ->").font(.callout).foregroundColor(.secondary)
-                            Text(
-                                state.insulinRecommended.formatted() + unit
-                            ).font(.callout).foregroundColor(.blue).bold()
-                        }
-                    }
-                }
-                // Warning
-                if state.error, state.insulinRecommended > 0 {
-                    VStack(spacing: 5) {
-                        Divider()
-                        Text("Warning!").font(.callout).bold().foregroundColor(.orange)
-                        Text(alertString()).font(.footnote)
-                        Divider()
-                    }.padding(.horizontal, 10)
-                }
-                // Footer
-                if !(state.error && state.insulinRecommended > 0) {
-                    VStack {
-                        Text(
-                            "Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended."
-                        ).font(.caption2).foregroundColor(.secondary)
-                    }.padding(20)
-                }
-                // Hide button
-                VStack {
-                    Button { presentInfo = false }
-                    label: { Text("Hide") }.frame(maxWidth: .infinity, alignment: .center).font(.callout)
-                        .foregroundColor(.blue)
-                }.padding(.bottom, 10)
-            }
-            .background(
-                RoundedRectangle(cornerRadius: 8, style: .continuous)
-                    .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4))
-                // .fill(Color(.systemGray).gradient)  // A more prominent pop-up, but harder to read
-            )
-        }
-
-        // Localize the Oref0 error/warning strings. The default should never be returned
-        private func alertString() -> String {
-            switch state.errorString {
-            case 1,
-                 2:
-                return NSLocalizedString(
-                    "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
-                    comment: "Bolus pop-up / Alert string. Make translations concise!"
-                ) + state.minGuardBG
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state.units
-                    .rawValue + ", " +
-                    NSLocalizedString(
-                        "which is below your Threshold (",
-                        comment: "Bolus pop-up / Alert string. Make translations concise!"
-                    ) + state
-                    .threshold.formatted() + " " + state.units.rawValue + ")"
-            case 3:
-                return NSLocalizedString(
-                    "Eventual Glucose > Target Glucose, but glucose is climbing slower than expected. Expected: ",
-                    comment: "Bolus pop-up / Alert string. Make translations concise!"
-                ) +
-                    state.expectedDelta
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
-                    NSLocalizedString(". Climbing: ", comment: "Bolus pop-up / Alert string. Make translatons concise!") + state
-                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
-            case 4:
-                return NSLocalizedString(
-                    "Eventual Glucose > Target Glucose, but glucose is falling faster than expected. Expected: ",
-                    comment: "Bolus pop-up / Alert string. Make translations concise!"
-                ) +
-                    state.expectedDelta
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
-                    NSLocalizedString(". Falling: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
-                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
-            case 5:
-                return NSLocalizedString(
-                    "Eventual Glucose > Target Glucose, but glucose is changing faster than expected. Expected: ",
-                    comment: "Bolus pop-up / Alert string. Make translations concise!"
-                ) +
-                    state.expectedDelta
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
-                    NSLocalizedString(". Changing: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
-                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
-            case 6:
-                return NSLocalizedString(
-                    "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
-                    comment: "Bolus pop-up / Alert string. Make translations concise!"
-                ) + state
-                    .minPredBG
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state
-                    .units
-                    .rawValue
-            default:
-                return "Ignore Warning..."
+            if state.useCalc {
+                // show alternative bolus calc based on toggle in bolus calc settings
+                AlternativeBolusCalcRootView(resolver: resolver, waitForSuggestion: waitForSuggestion, fetch: fetch, state: state)
+            } else {
+                // show iAPS standard bolus calc
+                DefaultBolusCalcRootView(resolver: resolver, waitForSuggestion: waitForSuggestion, fetch: fetch, state: state)
             }
         }
     }
 }
 
+// fix iOS 15 bug
 struct ActivityIndicator: UIViewRepresentable {
     @Binding var isAnimating: Bool
     let style: UIActivityIndicatorView.Style

+ 275 - 0
FreeAPS/Sources/Modules/Bolus/View/DefaultBolusCalcRootView.swift

@@ -0,0 +1,275 @@
+import SwiftUI
+import Swinject
+
+extension Bolus {
+    struct DefaultBolusCalcRootView: BaseView {
+        let resolver: Resolver
+        let waitForSuggestion: Bool
+        let fetch: Bool
+        @StateObject var state = StateModel()
+
+        @State private var isAddInsulinAlertPresented = false
+        @State private var presentInfo = false
+        @State private var displayError = false
+
+        @Environment(\.colorScheme) var colorScheme
+
+        private var formatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
+        private var fractionDigits: Int {
+            if state.units == .mmolL {
+                return 1
+            } else { return 0 }
+        }
+
+        var body: some View {
+            Form {
+                Section {
+                    if state.waitForSuggestion {
+                        HStack {
+                            Text("Wait please").foregroundColor(.secondary)
+                            Spacer()
+                            ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
+                        }
+                    } else {
+                        HStack {
+                            Text("Insulin recommended")
+                            Image(systemName: "info.bubble")
+                                .symbolRenderingMode(.palette)
+                                .foregroundStyle(.primary, .blue)
+                                .onTapGesture {
+                                    presentInfo.toggle()
+                                }
+
+                            Spacer()
+
+                            Text(
+                                formatter
+                                    .string(from: state.insulinRecommended as NSNumber)! +
+                                    NSLocalizedString(" U", comment: "Insulin unit")
+                            ).foregroundColor((state.error && state.insulinRecommended > 0) ? .red : .secondary)
+                                .onTapGesture {
+                                    if state.error, state.insulinRecommended > 0 { displayError = true }
+                                    else { state.amount = state.insulinRecommended }
+                                }
+                        }.contentShape(Rectangle())
+                    }
+                }
+                header: { Text("Recommendation") }
+                if !state.waitForSuggestion {
+                    Section {
+                        HStack {
+                            Text("Amount")
+                            Spacer()
+                            DecimalTextField(
+                                "0",
+                                value: $state.amount,
+                                formatter: formatter,
+                                autofocus: true,
+                                cleanInput: true
+                            )
+                            Text(!(state.amount > state.maxBolus) ? "U" : "😵").foregroundColor(.secondary)
+                        }
+                    }
+                    header: { Text("Bolus") }
+                    Section {
+                        Button { state.add() }
+                        label: { Text(!(state.amount > state.maxBolus) ? "Enact bolus" : "Max Bolus exceeded!") }
+                            .frame(maxWidth: .infinity, alignment: .center)
+                            .disabled(
+                                state.amount <= 0 || state.amount > state.maxBolus
+                            )
+                    }
+
+                    if waitForSuggestion {
+                        Section {
+                            Button { state.showModal(for: nil) }
+                            label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
+                        }
+                    }
+                }
+            }
+            .alert(isPresented: $displayError) {
+                Alert(
+                    title: Text("Warning!"),
+                    message: Text("\n" + alertString() + "\n"),
+                    primaryButton: .destructive(
+                        Text("Add"),
+                        action: {
+                            state.amount = state.insulinRecommended
+                            displayError = false
+                        }
+                    ),
+                    secondaryButton: .cancel()
+                )
+            }.onAppear {
+                configureView {
+                    state.waitForSuggestionInitial = waitForSuggestion
+                    state.waitForSuggestion = waitForSuggestion
+                }
+            }
+            .navigationTitle("Enact Bolus")
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationBarItems(leading: Button("Close", action: state.hideModal))
+            .popup(isPresented: presentInfo, alignment: .center, direction: .bottom) {
+                bolusInfo
+            }
+        }
+
+        var bolusInfo: some View {
+            VStack {
+                // Variables
+                VStack(spacing: 3) {
+                    HStack {
+                        Text("Eventual Glucose").foregroundColor(.secondary)
+                        let evg = state.units == .mmolL ? Decimal(state.evBG).asMmolL : Decimal(state.evBG)
+                        Text(evg.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("Target Glucose").foregroundColor(.secondary)
+                        let target = state.units == .mmolL ? state.target.asMmolL : state.target
+                        Text(target.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("ISF").foregroundColor(.secondary)
+                        let isf = state.isf
+                        Text(isf.formatted())
+                        Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
+                            .foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("ISF:")
+                        Text("Insulin Sensitivity")
+                    }.foregroundColor(.secondary).italic()
+                    if state.percentage != 100 {
+                        HStack {
+                            Text("Percentage setting").foregroundColor(.secondary)
+                            let percentage = state.percentage
+                            Text(percentage.formatted())
+                            Text("%").foregroundColor(.secondary)
+                        }
+                    }
+                    HStack {
+                        Text("Formula:")
+                        Text("(Eventual Glucose - Target) / ISF")
+                    }.foregroundColor(.secondary).italic().padding(.top, 5)
+                }
+                .font(.footnote)
+                .padding(.top, 10)
+                Divider()
+                // Formula
+                VStack(spacing: 5) {
+                    let unit = NSLocalizedString(
+                        " U",
+                        comment: "Unit in number of units delivered (keep the space character!)"
+                    )
+                    let color: Color = (state.percentage != 100 && state.insulin > 0) ? .secondary : .blue
+                    let fontWeight: Font.Weight = (state.percentage != 100 && state.insulin > 0) ? .regular : .bold
+                    HStack {
+                        Text(NSLocalizedString("Insulin recommended", comment: "") + ":").font(.callout)
+                        Text(state.insulin.formatted() + unit).font(.callout).foregroundColor(color).fontWeight(fontWeight)
+                    }
+                    if state.percentage != 100, state.insulin > 0 {
+                        Divider()
+                        HStack { Text(state.percentage.formatted() + " % ->").font(.callout).foregroundColor(.secondary)
+                            Text(
+                                state.insulinRecommended.formatted() + unit
+                            ).font(.callout).foregroundColor(.blue).bold()
+                        }
+                    }
+                }
+                // Warning
+                if state.error, state.insulinRecommended > 0 {
+                    VStack(spacing: 5) {
+                        Divider()
+                        Text("Warning!").font(.callout).bold().foregroundColor(.orange)
+                        Text(alertString()).font(.footnote)
+                        Divider()
+                    }.padding(.horizontal, 10)
+                }
+                // Footer
+                if !(state.error && state.insulinRecommended > 0) {
+                    VStack {
+                        Text(
+                            "Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended."
+                        ).font(.caption2).foregroundColor(.secondary)
+                    }.padding(20)
+                }
+                // Hide button
+                VStack {
+                    Button { presentInfo = false }
+                    label: { Text("Hide") }.frame(maxWidth: .infinity, alignment: .center).font(.callout)
+                        .foregroundColor(.blue)
+                }.padding(.bottom, 10)
+            }
+            .background(
+                RoundedRectangle(cornerRadius: 8, style: .continuous)
+                    .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4))
+            )
+        }
+
+        // Localize the Oref0 error/warning strings. The default should never be returned
+        private func alertString() -> String {
+            switch state.errorString {
+            case 1,
+                 2:
+                return NSLocalizedString(
+                    "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
+                    comment: "Bolus pop-up / Alert string. Make translations concise!"
+                ) + state.minGuardBG
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state.units
+                    .rawValue + ", " +
+                    NSLocalizedString(
+                        "which is below your Threshold (",
+                        comment: "Bolus pop-up / Alert string. Make translations concise!"
+                    ) + state
+                    .threshold.formatted() + " " + state.units.rawValue + ")"
+            case 3:
+                return NSLocalizedString(
+                    "Eventual Glucose > Target Glucose, but glucose is climbing slower than expected. Expected: ",
+                    comment: "Bolus pop-up / Alert string. Make translations concise!"
+                ) +
+                    state.expectedDelta
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                    NSLocalizedString(". Climbing: ", comment: "Bolus pop-up / Alert string. Make translatons concise!") + state
+                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+            case 4:
+                return NSLocalizedString(
+                    "Eventual Glucose > Target Glucose, but glucose is falling faster than expected. Expected: ",
+                    comment: "Bolus pop-up / Alert string. Make translations concise!"
+                ) +
+                    state.expectedDelta
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                    NSLocalizedString(". Falling: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
+                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+            case 5:
+                return NSLocalizedString(
+                    "Eventual Glucose > Target Glucose, but glucose is changing faster than expected. Expected: ",
+                    comment: "Bolus pop-up / Alert string. Make translations concise!"
+                ) +
+                    state.expectedDelta
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                    NSLocalizedString(". Changing: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
+                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+            case 6:
+                return NSLocalizedString(
+                    "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
+                    comment: "Bolus pop-up / Alert string. Make translations concise!"
+                ) + state
+                    .minPredBG
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state
+                    .units
+                    .rawValue
+            default:
+                return "Ignore Warning..."
+            }
+        }
+    }
+}

+ 5 - 0
FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorConfigDataFlow.swift

@@ -0,0 +1,5 @@
+enum BolusCalculatorConfig {
+    enum Config {}
+}
+
+protocol BolusCalculatorConfigProvider {}

+ 3 - 0
FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorConfigProvider.swift

@@ -0,0 +1,3 @@
+extension BolusCalculatorConfig {
+    final class Provider: BaseProvider, BolusCalculatorConfigProvider {}
+}

+ 27 - 0
FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift

@@ -0,0 +1,27 @@
+import SwiftUI
+
+extension BolusCalculatorConfig {
+    final class StateModel: BaseStateModel<Provider> {
+        @Published var overrideFactor: Decimal = 0
+        @Published var useCalc: Bool = false
+        @Published var fattyMeals: Bool = false
+        @Published var fattyMealFactor: Decimal = 0
+
+        override func subscribe() {
+            subscribeSetting(\.overrideFactor, on: $overrideFactor, initial: {
+                let value = max(min($0, 1.2), 0.1)
+                overrideFactor = value
+            }, map: {
+                $0
+            })
+            subscribeSetting(\.useCalc, on: $useCalc) { useCalc = $0 }
+            subscribeSetting(\.fattyMeals, on: $fattyMeals) { fattyMeals = $0 }
+            subscribeSetting(\.fattyMealFactor, on: $fattyMealFactor, initial: {
+                let value = max(min($0, 1.2), 0.1)
+                fattyMealFactor = value
+            }, map: {
+                $0
+            })
+        }
+    }
+}

+ 52 - 0
FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -0,0 +1,52 @@
+import SwiftUI
+import Swinject
+
+extension BolusCalculatorConfig {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        private var conversionFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 1
+
+            return formatter
+        }
+
+        var body: some View {
+            Form {
+                Section(header: Text("Calculator settings")) {
+                    HStack {
+                        Toggle("Use alternative Bolus Calculator", isOn: $state.useCalc)
+                    }
+                    HStack {
+                        Text("Override With A Factor Of ")
+                        Spacer()
+                        DecimalTextField("0.8", value: $state.overrideFactor, formatter: conversionFormatter)
+                    }
+                }
+                Section(header: Text("Fatty Meals")) {
+                    HStack {
+                        Toggle("Apply factor for fatty meals", isOn: $state.fattyMeals)
+                    }
+                    HStack {
+                        Text("Override With A Factor Of ")
+                        Spacer()
+                        DecimalTextField("0.7", value: $state.fattyMealFactor, formatter: conversionFormatter)
+                    }
+                }
+
+                Section(
+                    footer: Text(
+                        "This is another approach to the bolus calculator integrated in iAPS. If the toggle is on you use this bolus calculator and not the original iAPS calculator. At the end of the calculation a custom factor is applied as it is supposed to be when using smbs (default 0.8).\n\nYou can also add the option in your bolus calculator to apply another (!) customizable factor at the end of the calculation which could be useful for fatty meals, e.g Pizza (default 0.7)."
+                    )
+                )
+                    {}
+            }
+            .onAppear(perform: configureView)
+            .navigationBarTitle("Bolus Calculator")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 1 - 1
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -33,7 +33,7 @@ extension DataTable {
 
         func deleteCarbs(_ treatement: Treatment) {
             nightscoutManager.deleteCarbs(
-                at: treatement.date,
+                at: treatement.id,
                 isFPU: treatement.isFPU,
                 fpuID: treatement.fpuID,
                 syncID: treatement.id

+ 2 - 2
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -40,7 +40,7 @@ extension DataTable {
                 let carbs = self.provider.carbs()
                     .filter { !($0.isFPU ?? false) }
                     .map {
-                        if let id = $0.id {
+                        if let id = $0.collectionID {
                             return Treatment(
                                 units: units,
                                 type: .carbs,
@@ -62,7 +62,7 @@ extension DataTable {
                             type: .fpus,
                             date: $0.createdAt,
                             amount: $0.carbs,
-                            id: $0.id,
+                            id: $0.collectionID,
                             isFPU: $0.isFPU,
                             fpuID: $0.fpuID,
                             note: $0.note

+ 1 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -196,7 +196,7 @@ extension Home {
         }
 
         func addCarbs() {
-            showModal(for: .addCarbs)
+            showModal(for: .addCarbs(editMode: false))
         }
 
         func runLoop() {

+ 7 - 2
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -481,7 +481,7 @@ extension Home {
                 Rectangle().fill(Color.gray.opacity(0.3)).frame(height: 50 + geo.safeAreaInsets.bottom)
 
                 HStack {
-                    Button { state.showModal(for: .addCarbs) }
+                    Button { state.showModal(for: .addCarbs(editMode: false)) }
                     label: {
                         ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
                             Image("carbs")
@@ -511,7 +511,12 @@ extension Home {
                     .foregroundColor(.loopGreen)
                     .buttonStyle(.borderless)
                     Spacer()
-                    Button { state.showModal(for: .bolus(waitForSuggestion: false)) }
+                    Button {
+                        state.showModal(for: .bolus(
+                            waitForSuggestion: true,
+                            fetch: false
+                        ))
+                    }
                     label: {
                         Image("bolus")
                             .renderingMode(.template)

+ 2 - 0
FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift

@@ -8,9 +8,11 @@ extension PreferencesEditor {
         @Published var insulinReqPercentage: Decimal = 70
         @Published var skipBolusScreenAfterCarbs = false
         @Published var sections: [FieldSection] = []
+        @Published var useAlternativeBolusCalc: Bool = false
 
         override func subscribe() {
             preferences = provider.preferences
+            useAlternativeBolusCalc = settingsManager.settings.useCalc
             subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.insulinReqPercentage, on: $insulinReqPercentage) { insulinReqPercentage = $0 }
             subscribeSetting(\.skipBolusScreenAfterCarbs, on: $skipBolusScreenAfterCarbs) { skipBolusScreenAfterCarbs = $0 }

+ 5 - 3
FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift

@@ -30,9 +30,11 @@ extension PreferencesEditor {
 
                     Toggle("Remote control", isOn: $state.allowAnnouncements)
 
-                    HStack {
-                        Text("Recommended Bolus Percentage")
-                        DecimalTextField("", value: $state.insulinReqPercentage, formatter: formatter)
+                    if !state.useAlternativeBolusCalc {
+                        HStack {
+                            Text("Recommended Bolus Percentage")
+                            DecimalTextField("", value: $state.insulinReqPercentage, formatter: formatter)
+                        }
                     }
 
                     Toggle("Skip Bolus screen after carbs", isOn: $state.skipBolusScreenAfterCarbs)

+ 1 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -43,6 +43,7 @@ extension Settings {
                     Text("Carb Ratios").navigationLink(to: .crEditor, from: self)
                     Text("Target Glucose").navigationLink(to: .targetsEditor, from: self)
                     Text("Autotune").navigationLink(to: .autotuneConfig, from: self)
+                    Text("Bolus Calculator").navigationLink(to: .bolusCalculatorConfig, from: self)
                 }
 
                 Section(header: Text("Developer")) {

+ 9 - 6
FreeAPS/Sources/Router/Screen.swift

@@ -14,9 +14,9 @@ enum Screen: Identifiable, Hashable {
     case crEditor
     case targetsEditor
     case preferencesEditor
-    case addCarbs
+    case addCarbs(editMode: Bool)
     case addTempTarget
-    case bolus(waitForSuggestion: Bool)
+    case bolus(waitForSuggestion: Bool, fetch: Bool)
     case manualTempBasal
     case autotuneConfig
     case dataTable
@@ -32,6 +32,7 @@ enum Screen: Identifiable, Hashable {
     case statistics
     case watch
     case statisticsConfig
+    case bolusCalculatorConfig
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -63,12 +64,12 @@ extension Screen {
             TargetsEditor.RootView(resolver: resolver)
         case .preferencesEditor:
             PreferencesEditor.RootView(resolver: resolver)
-        case .addCarbs:
-            AddCarbs.RootView(resolver: resolver)
+        case let .addCarbs(editMode):
+            AddCarbs.RootView(resolver: resolver, editMode: editMode)
         case .addTempTarget:
             AddTempTarget.RootView(resolver: resolver)
-        case let .bolus(waitForSuggestion):
-            Bolus.RootView(resolver: resolver, waitForSuggestion: waitForSuggestion)
+        case let .bolus(waitForSuggestion, fetch):
+            Bolus.RootView(resolver: resolver, waitForSuggestion: waitForSuggestion, fetch: fetch)
         case .manualTempBasal:
             ManualTempBasal.RootView(resolver: resolver)
         case .autotuneConfig:
@@ -99,6 +100,8 @@ extension Screen {
             Stat.RootView(resolver: resolver)
         case .statisticsConfig:
             StatConfig.RootView(resolver: resolver)
+        case .bolusCalculatorConfig:
+            BolusCalculatorConfig.RootView(resolver: resolver)
         }
     }
 

+ 1 - 1
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -194,7 +194,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             let sampleIDs = samples.compactMap(\.syncIdentifier)
             let sampleDates = samples.map(\.startDate)
             let samplesToSave = carbsWithId
-                .filter { !sampleIDs.contains($0.id!) } // id existing in AH
+                .filter { !sampleIDs.contains($0.collectionID!) } // id existing in AH
                 .filter { !sampleDates.contains($0.createdAt) } // not id but exaclty the same datetime
                 .map {
                     HKQuantitySample(

+ 6 - 5
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -141,17 +141,18 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
     }
 
-    func deleteCarbs(at date: Date) -> AnyPublisher<Void, Swift.Error> {
+    func deleteCarbs(at uniqueID: String) -> AnyPublisher<Void, Swift.Error> {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
         components.port = url.port
         components.path = Config.treatmentsPath
         components.queryItems = [
-            URLQueryItem(name: "find[carbs][$exists]", value: "true"),
+            // Removed below because it prevented all futire entries to be deleted. Don't know why?
+            /* URLQueryItem(name: "find[carbs][$exists]", value: "true"), */
             URLQueryItem(
-                name: "find[created_at][$eq]",
-                value: Formatter.iso8601withFractionalSeconds.string(from: date)
+                name: "find[collectionID][$eq]",
+                value: uniqueID
             )
         ]
 
@@ -322,7 +323,7 @@ extension NightscoutAPI {
         if let secret = secret {
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
         }
-        request.httpBody = try! JSONCoding.encoder.encode(treatments)
+        request.httpBody = try? JSONCoding.encoder.encode(treatments)
         request.httpMethod = "POST"
 
         return service.run(request)

+ 18 - 48
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -9,7 +9,7 @@ protocol NightscoutManager: GlucoseSource {
     func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never>
     func fetchTempTargets() -> AnyPublisher<[TempTarget], Never>
     func fetchAnnouncements() -> AnyPublisher<[Announcement], Never>
-    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
+    func deleteCarbs(at uniqueID: String, isFPU: Bool?, fpuID: String?, syncID: String)
     func deleteInsulin(at date: Date)
     func deleteManualGlucose(at: Date)
     func uploadStatus()
@@ -177,62 +177,32 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             .eraseToAnyPublisher()
     }
 
-    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String) {
+    func deleteCarbs(at uniqueID: String, isFPU: Bool?, fpuID: String?, syncID: String) {
         // remove in AH
         healthkitManager.deleteCarbs(syncID: syncID, isFPU: isFPU, fpuID: fpuID)
 
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
-            carbsStorage.deleteCarbs(at: date)
+            carbsStorage.deleteCarbs(at: uniqueID)
             return
         }
 
-        if let isFPU = isFPU, isFPU {
-            guard let fpuID = fpuID else { return }
-            let allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
-            let dates = allValues.filter { $0.fpuID == fpuID }.map(\.createdAt).removeDublicates()
-
-            let publishers = dates
-                .map { d -> AnyPublisher<Void, Swift.Error> in
-                    nightscout.deleteCarbs(
-                        at: d
+        nightscout.deleteCarbs(at: uniqueID)
+            .collect()
+            .sink { completion in
+                self.carbsStorage.deleteCarbs(at: uniqueID)
+                switch completion {
+                case .finished:
+                    debug(.nightscout, "Carbs deleted")
+                case let .failure(error):
+                    info(
+                        .nightscout,
+                        "Deletion of carbs in NightScout not done \n \(error.localizedDescription)",
+                        type: MessageType.warning
                     )
                 }
-
-            Publishers.MergeMany(publishers)
-                .collect()
-                .sink { completion in
-                    self.carbsStorage.deleteCarbs(at: date)
-                    switch completion {
-                    case .finished:
-                        debug(.nightscout, "Carbs deleted")
-
-                    case let .failure(error):
-                        info(
-                            .nightscout,
-                            "Deletion of carbs in NightScout not done \n \(error.localizedDescription)",
-                            type: MessageType.warning
-                        )
-                    }
-                } receiveValue: { _ in }
-                .store(in: &lifetime)
-
-        } else {
-            nightscout.deleteCarbs(at: date)
-                .sink { completion in
-                    self.carbsStorage.deleteCarbs(at: date)
-                    switch completion {
-                    case .finished:
-                        debug(.nightscout, "Carbs deleted")
-                    case let .failure(error):
-                        info(
-                            .nightscout,
-                            "Deletion of carbs in NightScout not done \n \(error.localizedDescription)",
-                            type: MessageType.warning
-                        )
-                    }
-                } receiveValue: {}
-                .store(in: &lifetime)
-        }
+            } receiveValue: { _ in }
+            .store(in: &lifetime)
+        // }
     }
 
     func deleteInsulin(at date: Date) {

+ 1 - 1
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -272,7 +272,7 @@ extension BaseWatchManager: WCSessionDelegate {
         {
             carbsStorage.storeCarbs(
                 [CarbsEntry(
-                    id: UUID().uuidString,
+                    collectionID: UUID().uuidString,
                     createdAt: Date(),
                     carbs: Decimal(carbs),
                     fat: Decimal(fat),

+ 1 - 1
FreeAPS/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift

@@ -11,7 +11,7 @@ import Foundation
 
         carbsStorage.storeCarbs(
             [CarbsEntry(
-                id: UUID().uuidString,
+                collectionID: UUID().uuidString,
                 createdAt: dateAdded,
                 carbs: carbs,
                 fat: Decimal(quantityFat),

+ 0 - 25
FreeAPS/Sources/Views/DecimalTextField.swift

@@ -30,31 +30,6 @@ struct DecimalTextField: UIViewRepresentable {
         textfield.text = cleanInput ? "" : formatter.string(for: value) ?? placeholder
         textfield.textAlignment = .right
 
-        let toolBar = UIToolbar(frame: CGRect(
-            x: 0,
-            y: 0,
-            width: textfield.frame.size.width,
-            height: 44
-        ))
-        let clearButton = UIBarButtonItem(
-            title: NSLocalizedString("Clear", comment: "Clear button"),
-            style: .plain,
-            target: self,
-            action: #selector(textfield.clearButtonTapped(button:))
-        )
-        let doneButton = UIBarButtonItem(
-            title: NSLocalizedString("Done", comment: "Done button"),
-            style: .done,
-            target: self,
-            action: #selector(textfield.doneButtonTapped(button:))
-        )
-        let space = UIBarButtonItem(
-            barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace,
-            target: nil,
-            action: nil
-        )
-        toolBar.setItems([clearButton, space, doneButton], animated: true)
-        textfield.inputAccessoryView = toolBar
         if autofocus {
             DispatchQueue.main.async {
                 textfield.becomeFirstResponder()