OverrideStateModel.swift 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. import Combine
  2. import CoreData
  3. import Observation
  4. import SwiftUI
  5. extension OverrideConfig {
  6. @Observable final class StateModel: BaseStateModel<Provider> {
  7. @ObservationIgnored @Injected() var broadcaster: Broadcaster!
  8. @ObservationIgnored @Injected() var storage: TempTargetsStorage!
  9. @ObservationIgnored @Injected() var apsManager: APSManager!
  10. @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
  11. @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
  12. var overridePercentage: Double = 100
  13. var isEnabled = false
  14. var indefinite = true
  15. var overrideDuration: Decimal = 0
  16. var target: Decimal = 100
  17. var shouldOverrideTarget: Bool = false
  18. var smbIsOff: Bool = false
  19. var id = ""
  20. var overrideName: String = ""
  21. var isPreset: Bool = false
  22. var overridePresets: [OverrideStored] = []
  23. var advancedSettings: Bool = false
  24. var isfAndCr: Bool = true
  25. var isf: Bool = true
  26. var cr: Bool = true
  27. var smbIsScheduledOff: Bool = false
  28. var start: Decimal = 0
  29. var end: Decimal = 0
  30. var smbMinutes: Decimal = 0
  31. var uamMinutes: Decimal = 0
  32. var defaultSmbMinutes: Decimal = 0
  33. var defaultUamMinutes: Decimal = 0
  34. var selectedTab: Tab = .overrides
  35. var activeOverrideName: String = ""
  36. var currentActiveOverride: OverrideStored?
  37. var showOverrideEditSheet = false
  38. var units: GlucoseUnits = .mgdL
  39. // temp target stuff
  40. var low: Decimal = 0
  41. var high: Decimal = 0
  42. var durationTT: Decimal = 0
  43. var date = Date()
  44. var newPresetName = ""
  45. var presetsTT: [TempTarget] = []
  46. var percentageTT = 100.0
  47. var maxValue: Decimal = 1.2
  48. var viewPercantage = false
  49. var hbt: Double = 160
  50. var didSaveSettings: Bool = false
  51. var isHelpSheetPresented: Bool = false
  52. var helpSheetDetent = PresentationDetent.large
  53. var alertMessage: String {
  54. let target: String = units == .mgdL ? "70-270 mg/dl" : "4-15 mmol/l"
  55. return "Please enter a valid target between" + " \(target)."
  56. }
  57. private var cancellables = Set<AnyCancellable>()
  58. override func subscribe() {
  59. setupNotification()
  60. units = settingsManager.settings.units
  61. defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
  62. defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
  63. setupOverridePresetsArray()
  64. updateLatestOverrideConfiguration()
  65. presetsTT = storage.presets()
  66. maxValue = settingsManager.preferences.autosensMax
  67. broadcaster.register(SettingsObserver.self, observer: self)
  68. }
  69. let coredataContext = CoreDataStack.shared.newTaskContext()
  70. let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  71. }
  72. }
  73. // MARK: - Setup Notifications
  74. extension OverrideConfig.StateModel {
  75. // Custom Notification to update View when an Override has been cancelled via Home View
  76. func setupNotification() {
  77. Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
  78. .sink { [weak self] _ in
  79. guard let self = self else { return }
  80. self.updateLatestOverrideConfiguration()
  81. }
  82. .store(in: &cancellables)
  83. }
  84. // MARK: - Enact Overrides
  85. func reorderOverride(from source: IndexSet, to destination: Int) {
  86. overridePresets.move(fromOffsets: source, toOffset: destination)
  87. for (index, override) in overridePresets.enumerated() {
  88. override.orderPosition = Int16(index + 1)
  89. }
  90. do {
  91. guard viewContext.hasChanges else { return }
  92. try viewContext.save()
  93. // Update Presets View
  94. setupOverridePresetsArray()
  95. Task {
  96. await nightscoutManager.uploadProfiles()
  97. }
  98. } catch {
  99. debugPrint(
  100. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Override Presets with error: \(error.localizedDescription)"
  101. )
  102. }
  103. }
  104. /// here we only have to update the Boolean Flag 'enabled'
  105. @MainActor func enactOverridePreset(withID id: NSManagedObjectID) async {
  106. do {
  107. /// get the underlying NSManagedObject of the Override that should be enabled
  108. let overrideToEnact = try viewContext.existingObject(with: id) as? OverrideStored
  109. overrideToEnact?.enabled = true
  110. overrideToEnact?.date = Date()
  111. overrideToEnact?.isUploadedToNS = false
  112. /// Update the 'Cancel Override' button state
  113. isEnabled = true
  114. /// disable all active Overrides and reset state variables
  115. /// do not create a OverrideRunEntry because we only want that if we cancel a running Override, not when enacting a Preset
  116. await disableAllActiveOverrides(except: id, createOverrideRunEntry: currentActiveOverride != nil)
  117. await resetStateVariables()
  118. guard viewContext.hasChanges else { return }
  119. try viewContext.save()
  120. // Update View
  121. updateLatestOverrideConfiguration()
  122. } catch {
  123. debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
  124. }
  125. }
  126. // MARK: - Save the Override that we want to cancel to the OverrideRunStored Entity, then cancel ALL active overrides
  127. @MainActor func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil, createOverrideRunEntry: Bool) async {
  128. // Get ALL NSManagedObject IDs of ALL active Override to cancel every single Override
  129. let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
  130. await viewContext.perform {
  131. do {
  132. // Fetch the existing OverrideStored objects from the context
  133. let results = try ids.compactMap { id in
  134. try self.viewContext.existingObject(with: id) as? OverrideStored
  135. }
  136. // If there are no results, return early
  137. guard !results.isEmpty else { return }
  138. // Check if we also need to create a corresponding OverrideRunStored entry, i.e. when the User uses the Cancel Button in Override View
  139. if createOverrideRunEntry {
  140. // Use the first override to create a new OverrideRunStored entry
  141. if let canceledOverride = results.first {
  142. let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
  143. newOverrideRunStored.id = UUID()
  144. newOverrideRunStored.name = canceledOverride.name
  145. newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
  146. newOverrideRunStored.endDate = Date()
  147. newOverrideRunStored
  148. .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
  149. newOverrideRunStored.override = canceledOverride
  150. newOverrideRunStored.isUploadedToNS = false
  151. }
  152. }
  153. // Disable all override except the one with overrideID
  154. for overrideToCancel in results {
  155. if overrideToCancel.objectID != overrideID {
  156. overrideToCancel.enabled = false
  157. }
  158. }
  159. // Save the context if there are changes
  160. if self.viewContext.hasChanges {
  161. try self.viewContext.save()
  162. // Update the View
  163. self.updateLatestOverrideConfiguration()
  164. }
  165. } catch {
  166. debugPrint(
  167. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
  168. )
  169. }
  170. }
  171. }
  172. // MARK: - Override (presets) save operations
  173. // Saves a Custom Override in a background context
  174. /// not a Preset
  175. func saveCustomOverride() async {
  176. let override = Override(
  177. name: overrideName,
  178. enabled: true,
  179. date: Date(),
  180. duration: overrideDuration,
  181. indefinite: indefinite,
  182. percentage: overridePercentage,
  183. smbIsOff: smbIsOff,
  184. isPreset: isPreset,
  185. id: id,
  186. overrideTarget: shouldOverrideTarget,
  187. target: target,
  188. advancedSettings: advancedSettings,
  189. isfAndCr: isfAndCr,
  190. isf: isf,
  191. cr: cr,
  192. smbIsScheduledOff: smbIsScheduledOff,
  193. start: start,
  194. end: end,
  195. smbMinutes: smbMinutes,
  196. uamMinutes: uamMinutes
  197. )
  198. // First disable all Overrides
  199. await disableAllActiveOverrides(createOverrideRunEntry: true)
  200. // Then save and activate a new custom Override and reset the State variables
  201. async let storeOverride: () = overrideStorage.storeOverride(override: override)
  202. async let resetState: () = resetStateVariables()
  203. _ = await (storeOverride, resetState)
  204. // Update View
  205. updateLatestOverrideConfiguration()
  206. }
  207. // Save Presets
  208. /// enabled has to be false, isPreset has to be true
  209. func saveOverridePreset() async {
  210. let preset = Override(
  211. name: overrideName,
  212. enabled: false,
  213. date: Date(),
  214. duration: overrideDuration,
  215. indefinite: indefinite,
  216. percentage: overridePercentage,
  217. smbIsOff: smbIsOff,
  218. isPreset: true,
  219. id: id,
  220. overrideTarget: shouldOverrideTarget,
  221. target: target,
  222. advancedSettings: advancedSettings,
  223. isfAndCr: isfAndCr,
  224. isf: isf,
  225. cr: cr,
  226. smbIsScheduledOff: smbIsScheduledOff,
  227. start: start,
  228. end: end,
  229. smbMinutes: smbMinutes,
  230. uamMinutes: uamMinutes
  231. )
  232. async let storeOverride: () = overrideStorage.storeOverride(override: preset)
  233. async let resetState: () = resetStateVariables()
  234. _ = await (storeOverride, resetState)
  235. // Update Presets View
  236. setupOverridePresetsArray()
  237. await nightscoutManager.uploadProfiles()
  238. }
  239. // MARK: - Setup Override Presets Array
  240. // Fill the array of the Override Presets to display them in the UI
  241. private func setupOverridePresetsArray() {
  242. Task {
  243. let ids = await self.overrideStorage.fetchForOverridePresets()
  244. await updateOverridePresetsArray(with: ids)
  245. }
  246. }
  247. @MainActor private func updateOverridePresetsArray(with IDs: [NSManagedObjectID]) async {
  248. do {
  249. let overrideObjects = try IDs.compactMap { id in
  250. try viewContext.existingObject(with: id) as? OverrideStored
  251. }
  252. overridePresets = overrideObjects
  253. } catch {
  254. debugPrint(
  255. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Overrides as NSManagedObjects from the NSManagedObjectIDs with error: \(error.localizedDescription)"
  256. )
  257. }
  258. }
  259. // MARK: - Override Preset Deletion
  260. func invokeOverridePresetDeletion(_ objectID: NSManagedObjectID) async {
  261. await overrideStorage.deleteOverridePreset(objectID)
  262. // Update Presets View
  263. setupOverridePresetsArray()
  264. await nightscoutManager.uploadProfiles()
  265. }
  266. // MARK: - Setup the State variables with the last Override configuration
  267. /// First get the latest Overrides corresponding NSManagedObjectID with a background fetch
  268. /// Then unpack it on the view context and update the State variables which can be used on in the View for some Logic
  269. /// This also needs to be called when we cancel an Override via the Home View to update the State of the Button for this case
  270. func updateLatestOverrideConfiguration() {
  271. Task {
  272. let id = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
  273. async let updateState: () = updateLatestOverrideConfigurationOfState(from: id)
  274. async let setOverride: () = setCurrentOverride(from: id)
  275. _ = await (updateState, setOverride)
  276. }
  277. }
  278. @MainActor func updateLatestOverrideConfigurationOfState(from IDs: [NSManagedObjectID]) async {
  279. do {
  280. let result = try IDs.compactMap { id in
  281. try viewContext.existingObject(with: id) as? OverrideStored
  282. }
  283. isEnabled = result.first?.enabled ?? false
  284. if !isEnabled {
  285. await resetStateVariables()
  286. }
  287. } catch {
  288. debugPrint(
  289. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to updateLatestOverrideConfiguration"
  290. )
  291. }
  292. }
  293. // Sets the current active Preset name to show in the UI
  294. @MainActor func setCurrentOverride(from IDs: [NSManagedObjectID]) async {
  295. do {
  296. guard let firstID = IDs.first else {
  297. activeOverrideName = "Custom Override"
  298. currentActiveOverride = nil
  299. return
  300. }
  301. if let overrideToEdit = try viewContext.existingObject(with: firstID) as? OverrideStored {
  302. currentActiveOverride = overrideToEdit
  303. activeOverrideName = overrideToEdit.name ?? "Custom Override"
  304. }
  305. } catch {
  306. debugPrint(
  307. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active preset name with error: \(error.localizedDescription)"
  308. )
  309. }
  310. }
  311. @MainActor func duplicateOverridePresetAndCancelPreviousOverride() async {
  312. // We get the current active Preset by using currentActiveOverride which can either be a Preset or a custom Override
  313. guard let overridePresetToDuplicate = currentActiveOverride, overridePresetToDuplicate.isPreset == true else { return }
  314. // Copy the current Override-Preset to not edit the underlying Preset
  315. let duplidateId = await overrideStorage.copyRunningOverride(overridePresetToDuplicate)
  316. // Cancel the duplicated Override
  317. /// As we are on the Main Thread already we don't need to cancel via the objectID in this case
  318. do {
  319. try await viewContext.perform {
  320. overridePresetToDuplicate.enabled = false
  321. guard self.viewContext.hasChanges else { return }
  322. try self.viewContext.save()
  323. }
  324. // Update View
  325. // TODO: -
  326. if let overrideToEdit = try viewContext.existingObject(with: duplidateId) as? OverrideStored
  327. {
  328. currentActiveOverride = overrideToEdit
  329. activeOverrideName = overrideToEdit.name ?? "Custom Override"
  330. }
  331. } catch {
  332. debugPrint(
  333. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error.localizedDescription)"
  334. )
  335. }
  336. }
  337. // MARK: - Helper functions for Overrides
  338. @MainActor func resetStateVariables() async {
  339. id = ""
  340. overrideDuration = 0
  341. indefinite = true
  342. overridePercentage = 100
  343. advancedSettings = false
  344. smbIsOff = false
  345. overrideName = ""
  346. shouldOverrideTarget = false
  347. isf = true
  348. cr = true
  349. isfAndCr = true
  350. smbIsScheduledOff = false
  351. start = 0
  352. end = 0
  353. smbMinutes = defaultSmbMinutes
  354. uamMinutes = defaultUamMinutes
  355. target = 100
  356. }
  357. static func roundTargetToStep(_ target: Decimal, _ step: Decimal) -> Decimal {
  358. // Convert target and step to NSDecimalNumber
  359. guard let targetValue = NSDecimalNumber(decimal: target).doubleValue as Double?,
  360. let stepValue = NSDecimalNumber(decimal: step).doubleValue as Double?
  361. else {
  362. return target
  363. }
  364. // Perform the remainder check using truncatingRemainder
  365. let remainder = Decimal(targetValue.truncatingRemainder(dividingBy: stepValue))
  366. if remainder != 0 {
  367. // Calculate how much to adjust (up or down) based on the remainder
  368. let adjustment = step - remainder
  369. return target + adjustment
  370. }
  371. // Return the original target if no adjustment is needed
  372. return target
  373. }
  374. static func roundOverridePercentageToStep(_ percentage: Double, _ step: Int) -> Double {
  375. let stepDouble = Double(step)
  376. // Check if overridePercentage is not divisible by the selected step
  377. if percentage.truncatingRemainder(dividingBy: stepDouble) != 0 {
  378. let roundedValue: Double
  379. if percentage > 100 {
  380. // Round down to the nearest valid step away from 100
  381. let stepCount = (percentage - 100) / stepDouble
  382. roundedValue = 100 + floor(stepCount) * stepDouble
  383. } else {
  384. // Round up to the nearest valid step away from 100
  385. let stepCount = (100 - percentage) / stepDouble
  386. roundedValue = 100 - floor(stepCount) * stepDouble
  387. }
  388. // Ensure the value stays between 10 and 200
  389. return max(10, min(roundedValue, 200))
  390. }
  391. return percentage
  392. }
  393. }
  394. // MARK: - TEMP TARGET
  395. extension OverrideConfig.StateModel {
  396. func enact() {
  397. guard durationTT > 0 else {
  398. return
  399. }
  400. var lowTarget = low
  401. if viewPercantage {
  402. lowTarget = Decimal(round(Double(computeTarget())))
  403. coredataContext.performAndWait {
  404. let saveToCoreData = TempTargets(context: self.coredataContext)
  405. saveToCoreData.id = UUID().uuidString
  406. saveToCoreData.active = true
  407. saveToCoreData.hbt = hbt
  408. saveToCoreData.date = Date()
  409. saveToCoreData.duration = durationTT as NSDecimalNumber
  410. saveToCoreData.startDate = Date()
  411. try? self.coredataContext.save()
  412. }
  413. didSaveSettings = true
  414. } else {
  415. coredataContext.performAndWait {
  416. let saveToCoreData = TempTargets(context: coredataContext)
  417. saveToCoreData.active = false
  418. saveToCoreData.date = Date()
  419. do {
  420. guard coredataContext.hasChanges else { return }
  421. try coredataContext.save()
  422. } catch {
  423. print(error.localizedDescription)
  424. }
  425. }
  426. }
  427. var highTarget = lowTarget
  428. if units == .mmolL, !viewPercantage {
  429. lowTarget = Decimal(round(Double(lowTarget)))
  430. highTarget = lowTarget
  431. }
  432. let entry = TempTarget(
  433. name: TempTarget.custom,
  434. createdAt: date,
  435. targetTop: highTarget,
  436. targetBottom: lowTarget,
  437. duration: durationTT,
  438. enteredBy: TempTarget.manual,
  439. reason: TempTarget.custom
  440. )
  441. storage.storeTempTargets([entry])
  442. showModal(for: nil)
  443. }
  444. func cancel() {
  445. storage.storeTempTargets([TempTarget.cancel(at: Date())])
  446. showModal(for: nil)
  447. coredataContext.performAndWait {
  448. let saveToCoreData = TempTargets(context: self.coredataContext)
  449. saveToCoreData.active = false
  450. saveToCoreData.date = Date()
  451. do {
  452. guard coredataContext.hasChanges else { return }
  453. try coredataContext.save()
  454. } catch {
  455. print(error.localizedDescription)
  456. }
  457. let setHBT = TempTargetsSlider(context: self.coredataContext)
  458. setHBT.enabled = false
  459. setHBT.date = Date()
  460. do {
  461. guard coredataContext.hasChanges else { return }
  462. try coredataContext.save()
  463. } catch {
  464. print(error.localizedDescription)
  465. }
  466. }
  467. }
  468. func save() {
  469. guard durationTT > 0 else {
  470. return
  471. }
  472. var lowTarget = low
  473. if viewPercantage {
  474. lowTarget = Decimal(round(Double(computeTarget())))
  475. didSaveSettings = true
  476. }
  477. var highTarget = lowTarget
  478. if units == .mmolL, !viewPercantage {
  479. lowTarget = Decimal(round(Double(lowTarget.asMgdL)))
  480. highTarget = lowTarget
  481. }
  482. let entry = TempTarget(
  483. name: newPresetName.isEmpty ? TempTarget.custom : newPresetName,
  484. createdAt: Date(),
  485. targetTop: highTarget,
  486. targetBottom: lowTarget,
  487. duration: durationTT,
  488. enteredBy: TempTarget.manual,
  489. reason: newPresetName.isEmpty ? TempTarget.custom : newPresetName
  490. )
  491. presetsTT.append(entry)
  492. storage.storePresets(presetsTT)
  493. if viewPercantage {
  494. let id = entry.id
  495. coredataContext.performAndWait {
  496. let saveToCoreData = TempTargetsSlider(context: self.coredataContext)
  497. saveToCoreData.id = id
  498. saveToCoreData.isPreset = true
  499. saveToCoreData.enabled = true
  500. saveToCoreData.hbt = hbt
  501. saveToCoreData.date = Date()
  502. saveToCoreData.duration = durationTT as NSDecimalNumber
  503. do {
  504. guard coredataContext.hasChanges else { return }
  505. try coredataContext.save()
  506. } catch {
  507. print(error.localizedDescription)
  508. }
  509. }
  510. }
  511. }
  512. func enactPreset(id: String) {
  513. if var preset = presetsTT.first(where: { $0.id == id }) {
  514. preset.createdAt = Date()
  515. storage.storeTempTargets([preset])
  516. showModal(for: nil)
  517. coredataContext.performAndWait {
  518. var tempTargetsArray = [TempTargetsSlider]()
  519. let requestTempTargets = TempTargetsSlider.fetchRequest() as NSFetchRequest<TempTargetsSlider>
  520. let sortTT = NSSortDescriptor(key: "date", ascending: false)
  521. requestTempTargets.sortDescriptors = [sortTT]
  522. try? tempTargetsArray = coredataContext.fetch(requestTempTargets)
  523. let whichID = tempTargetsArray.first(where: { $0.id == id })
  524. if whichID != nil {
  525. let saveToCoreData = TempTargets(context: self.coredataContext)
  526. saveToCoreData.active = true
  527. saveToCoreData.date = Date()
  528. saveToCoreData.hbt = whichID?.hbt ?? 160
  529. // saveToCoreData.id = id
  530. saveToCoreData.startDate = Date()
  531. saveToCoreData.duration = whichID?.duration ?? 0
  532. do {
  533. guard coredataContext.hasChanges else { return }
  534. try coredataContext.save()
  535. } catch {
  536. print(error.localizedDescription)
  537. }
  538. } else {
  539. let saveToCoreData = TempTargets(context: self.coredataContext)
  540. saveToCoreData.active = false
  541. saveToCoreData.date = Date()
  542. do {
  543. guard coredataContext.hasChanges else { return }
  544. try coredataContext.save()
  545. } catch {
  546. print(error.localizedDescription)
  547. }
  548. }
  549. }
  550. }
  551. }
  552. func removePreset(id: String) {
  553. presetsTT = presetsTT.filter { $0.id != id }
  554. storage.storePresets(presetsTT)
  555. }
  556. func computeTarget() -> Decimal {
  557. var ratio = Decimal(percentageTT / 100)
  558. let c = Decimal(hbt - 100)
  559. var target = (c / ratio) - c + 100
  560. if c * (c + target - 100) <= 0 {
  561. ratio = maxValue
  562. target = (c / ratio) - c + 100
  563. }
  564. return Decimal(Double(target))
  565. }
  566. }
  567. extension OverrideConfig.StateModel: SettingsObserver {
  568. func settingsDidChange(_: FreeAPSSettings) {
  569. units = settingsManager.settings.units
  570. defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
  571. defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
  572. maxValue = settingsManager.preferences.autosensMax
  573. }
  574. }
  575. extension PickerSettingsProvider {
  576. func generatePickerValues(from setting: PickerSetting, units: GlucoseUnits, roundMinToStep: Bool) -> [Decimal] {
  577. if !roundMinToStep {
  578. return generatePickerValues(from: setting, units: units)
  579. }
  580. // Adjust min to be divisible by step
  581. var newSetting = setting
  582. var min = Double(newSetting.min)
  583. let step = Double(newSetting.step)
  584. let remainder = min.truncatingRemainder(dividingBy: step)
  585. if remainder != 0 {
  586. // Move min up to the next value divisible by targetStep
  587. min += (step - remainder)
  588. }
  589. newSetting.min = Decimal(min)
  590. return generatePickerValues(from: newSetting, units: units)
  591. }
  592. }
  593. enum IsfAndOrCrOptions: String, CaseIterable {
  594. case isfAndCr = "ISF/CR"
  595. case isf = "ISF"
  596. case cr = "CR"
  597. case nothing = "None"
  598. }
  599. enum DisableSmbOptions: String, CaseIterable {
  600. case dontDisable = "Don't Disable"
  601. case disable = "Disable"
  602. case disableOnSchedule = "Disable on Schedule"
  603. }
  604. func percentageDescription(_ percent: Double) -> Text? {
  605. if percent.isNaN || percent == 100 { return nil }
  606. var description: String = "Insulin doses will be "
  607. if percent < 100 {
  608. description += "decreased by "
  609. } else {
  610. description += "increased by "
  611. }
  612. let deviationFrom100 = abs(percent - 100)
  613. description += String(format: "%.0f% %.", deviationFrom100)
  614. return Text(description)
  615. }
  616. // Function to check if the phone is using 24-hour format
  617. func is24HourFormat() -> Bool {
  618. let formatter = DateFormatter()
  619. formatter.locale = Locale.current
  620. formatter.dateStyle = .none
  621. formatter.timeStyle = .short
  622. let dateString = formatter.string(from: Date())
  623. return !dateString.contains("AM") && !dateString.contains("PM")
  624. }
  625. // Helper function to convert hours to AM/PM format
  626. func convertTo12HourFormat(_ hour: Int) -> String {
  627. let formatter = DateFormatter()
  628. formatter.dateFormat = "h a"
  629. // Create a date from the hour and format it to AM/PM
  630. let calendar = Calendar.current
  631. let components = DateComponents(hour: hour)
  632. let date = calendar.date(from: components) ?? Date()
  633. return formatter.string(from: date)
  634. }
  635. // Helper function to format 24-hour numbers as two digits
  636. func format24Hour(_ hour: Int) -> String {
  637. String(format: "%02d", hour)
  638. }
  639. func formatHrMin(_ durationInMinutes: Int) -> String {
  640. let hours = durationInMinutes / 60
  641. let minutes = durationInMinutes % 60
  642. switch (hours, minutes) {
  643. case let (0, m):
  644. return "\(m) min"
  645. case let (h, 0):
  646. return "\(h) hr"
  647. default:
  648. return "\(hours) hr \(minutes) min"
  649. }
  650. }
  651. func convertToMinutes(_ hours: Int, _ minutes: Int) -> Decimal {
  652. let totalMinutes = (hours * 60) + minutes
  653. return Decimal(max(0, totalMinutes))
  654. }
  655. struct RadioButton: View {
  656. var isSelected: Bool
  657. var label: String
  658. var action: () -> Void
  659. var body: some View {
  660. Button(action: {
  661. action()
  662. }) {
  663. HStack {
  664. Image(systemName: isSelected ? "largecircle.fill.circle" : "circle")
  665. Text(label) // Add label inside the button to make it tappable
  666. }
  667. }
  668. .buttonStyle(PlainButtonStyle())
  669. }
  670. }