AlternativeBolusCalcRootView.swift 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  1. import Charts
  2. import CoreData
  3. import SwiftUI
  4. import Swinject
  5. extension Bolus {
  6. struct AlternativeBolusCalcRootView: BaseView {
  7. let resolver: Resolver
  8. let waitForSuggestion: Bool
  9. let fetch: Bool
  10. let editMode: Bool
  11. let override: Bool
  12. @StateObject var state: StateModel
  13. @State private var showInfo = false
  14. @State private var showAlert = false
  15. @State private var exceededMaxBolus = false
  16. @State private var keepForNextWiew: Bool = false
  17. @State private var calcButtonPressed: Bool = false
  18. @State private var autofocus: Bool = true
  19. @State private var calculatorDetent = PresentationDetent.medium
  20. @State var pushed = false
  21. @State var isPromptPresented = false
  22. @State var dish: String = ""
  23. @State var saved = false
  24. @State private var treatmentsViewMode: TreatmentsViewMode = .calcMode
  25. @FocusState private var isFocused: Bool
  26. @ObservedObject var appState: AppState
  27. @Environment(\.managedObjectContext) var moc
  28. private enum Config {
  29. static let dividerHeight: CGFloat = 2
  30. static let spacing: CGFloat = 3
  31. }
  32. private enum TreatmentsViewMode {
  33. case editMode
  34. case calcMode
  35. }
  36. @Environment(\.colorScheme) var colorScheme
  37. @FetchRequest(
  38. entity: Meals.entity(),
  39. sortDescriptors: [NSSortDescriptor(key: "createdAt", ascending: false)]
  40. ) var meal: FetchedResults<Meals>
  41. @FetchRequest(
  42. entity: Presets.entity(),
  43. sortDescriptors: [NSSortDescriptor(key: "dish", ascending: true)]
  44. ) var carbPresets: FetchedResults<Presets>
  45. private var formatter: NumberFormatter {
  46. let formatter = NumberFormatter()
  47. formatter.numberStyle = .decimal
  48. formatter.maximumFractionDigits = 2
  49. return formatter
  50. }
  51. private var mealFormatter: NumberFormatter {
  52. let formatter = NumberFormatter()
  53. formatter.numberStyle = .decimal
  54. formatter.maximumFractionDigits = 1
  55. return formatter
  56. }
  57. private var gluoseFormatter: NumberFormatter {
  58. let formatter = NumberFormatter()
  59. formatter.numberStyle = .decimal
  60. if state.units == .mmolL {
  61. formatter.maximumFractionDigits = 1
  62. } else { formatter.maximumFractionDigits = 0 }
  63. return formatter
  64. }
  65. private var fractionDigits: Int {
  66. if state.units == .mmolL {
  67. return 1
  68. } else { return 0 }
  69. }
  70. private var color: LinearGradient {
  71. colorScheme == .dark ? LinearGradient(
  72. gradient: Gradient(colors: [
  73. Color.bgDarkBlue,
  74. Color.bgDarkerDarkBlue
  75. ]),
  76. startPoint: .top,
  77. endPoint: .bottom
  78. )
  79. :
  80. LinearGradient(
  81. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  82. startPoint: .top,
  83. endPoint: .bottom
  84. )
  85. }
  86. private var empty: Bool {
  87. state.carbs <= 0 && state.fat <= 0 && state.protein <= 0
  88. }
  89. private var presetPopover: some View {
  90. Form {
  91. Section {
  92. TextField("Name Of Dish", text: $dish)
  93. Button {
  94. saved = true
  95. if dish != "", saved {
  96. let preset = Presets(context: moc)
  97. preset.dish = dish
  98. preset.fat = state.fat as NSDecimalNumber
  99. preset.protein = state.protein as NSDecimalNumber
  100. preset.carbs = state.carbs as NSDecimalNumber
  101. try? moc.save()
  102. state.addNewPresetToWaitersNotepad(dish)
  103. saved = false
  104. isPromptPresented = false
  105. }
  106. }
  107. label: { Text("Save") }
  108. Button {
  109. dish = ""
  110. saved = false
  111. isPromptPresented = false }
  112. label: { Text("Cancel") }
  113. } header: { Text("Enter Meal Preset Name") }
  114. }
  115. }
  116. private var minusButton: some View {
  117. Button {
  118. if state.carbs != 0,
  119. (state.carbs - (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
  120. {
  121. state.carbs -= (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal)
  122. } else { state.carbs = 0 }
  123. if state.fat != 0,
  124. (state.fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
  125. {
  126. state.fat -= (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal)
  127. } else { state.fat = 0 }
  128. if state.protein != 0,
  129. (state.protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
  130. {
  131. state.protein -= (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal)
  132. } else { state.protein = 0 }
  133. state.removePresetFromNewMeal()
  134. if state.carbs == 0, state.fat == 0, state.protein == 0 { state.summation = [] }
  135. }
  136. label: { Image(systemName: "minus.circle.fill")
  137. .font(.system(size: 20))
  138. }
  139. .disabled(
  140. state
  141. .selection == nil ||
  142. (
  143. !state.summation
  144. .contains(state.selection?.dish ?? "") && (state.selection?.dish ?? "") != ""
  145. )
  146. )
  147. .buttonStyle(.borderless)
  148. .tint(.blue)
  149. }
  150. private var plusButton: some View {
  151. Button {
  152. state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
  153. state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
  154. state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
  155. state.addPresetToNewMeal()
  156. }
  157. label: { Image(systemName: "plus.circle.fill")
  158. .font(.system(size: 20))
  159. }
  160. .disabled(state.selection == nil)
  161. .buttonStyle(.borderless)
  162. .tint(.blue)
  163. }
  164. private var mealPresets: some View {
  165. Section {
  166. HStack {
  167. if state.selection != nil {
  168. minusButton
  169. }
  170. Picker("Preset", selection: $state.selection) {
  171. Text("Saved Food").tag(nil as Presets?)
  172. ForEach(carbPresets, id: \.self) { (preset: Presets) in
  173. Text(preset.dish ?? "").tag(preset as Presets?)
  174. }
  175. }
  176. .labelsHidden()
  177. .frame(maxWidth: .infinity, alignment: .center)
  178. ._onBindingChange($state.selection) { _ in
  179. state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
  180. state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
  181. state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
  182. state.addToSummation()
  183. }
  184. if state.selection != nil {
  185. plusButton
  186. }
  187. }
  188. HStack {
  189. Button("Delete Preset") {
  190. showAlert.toggle()
  191. }
  192. .disabled(state.selection == nil)
  193. .tint(.orange)
  194. .buttonStyle(.borderless)
  195. .alert(
  196. "Delete preset '\(state.selection?.dish ?? "")'?",
  197. isPresented: $showAlert,
  198. actions: {
  199. Button("No", role: .cancel) {}
  200. Button("Yes", role: .destructive) {
  201. state.deletePreset()
  202. state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
  203. state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
  204. state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
  205. state.addPresetToNewMeal()
  206. }
  207. }
  208. )
  209. Spacer()
  210. Button {
  211. isPromptPresented = true
  212. }
  213. label: { Text("Save as Preset") }
  214. .buttonStyle(.borderless)
  215. .disabled(
  216. empty ||
  217. (
  218. (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal) == state
  219. .carbs && (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) == state
  220. .fat && (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) == state
  221. .protein
  222. )
  223. )
  224. }
  225. }
  226. }
  227. @ViewBuilder private func proteinAndFat() -> some View {
  228. HStack {
  229. Text("Fat").foregroundColor(.orange)
  230. Spacer()
  231. DecimalTextField(
  232. "0",
  233. value: $state.fat,
  234. formatter: formatter,
  235. autofocus: false,
  236. cleanInput: true
  237. )
  238. Text("g").foregroundColor(.secondary)
  239. }
  240. HStack {
  241. Text("Protein").foregroundColor(.red)
  242. Spacer()
  243. DecimalTextField(
  244. "0",
  245. value: $state.protein,
  246. formatter: formatter,
  247. autofocus: false,
  248. cleanInput: true
  249. ).foregroundColor(.loopRed)
  250. Text("g").foregroundColor(.secondary)
  251. }
  252. }
  253. var body: some View {
  254. Form {
  255. // MARK: ADDED
  256. Section {
  257. HStack {
  258. Text("Carbs").fontWeight(.semibold)
  259. Spacer()
  260. DecimalTextField(
  261. "0",
  262. value: $state.carbs,
  263. formatter: formatter,
  264. autofocus: true,
  265. cleanInput: true
  266. )
  267. Text("g").foregroundColor(.secondary)
  268. }
  269. if state.useFPUconversion {
  270. proteinAndFat()
  271. }
  272. // Summary when combining presets
  273. if state.waitersNotepad() != "" {
  274. HStack {
  275. Text("Total")
  276. let test = state.waitersNotepad().components(separatedBy: ", ").removeDublicates()
  277. HStack(spacing: 0) {
  278. ForEach(test, id: \.self) {
  279. Text($0).foregroundStyle(Color.randomGreen()).font(.footnote)
  280. Text($0 == test[test.count - 1] ? "" : ", ")
  281. }
  282. }.frame(maxWidth: .infinity, alignment: .trailing)
  283. }
  284. }
  285. // Time
  286. HStack {
  287. Text("Time").foregroundStyle(Color.secondary)
  288. Spacer()
  289. if !pushed {
  290. Button {
  291. pushed = true
  292. } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary).padding(.trailing, 5)
  293. } else {
  294. Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
  295. label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
  296. DatePicker(
  297. "Time",
  298. selection: $state.date,
  299. displayedComponents: [.hourAndMinute]
  300. ).controlSize(.mini)
  301. .labelsHidden()
  302. Button {
  303. state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
  304. }
  305. label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
  306. }
  307. }
  308. // Optional meal note
  309. // HStack {
  310. // Image(systemName: "note.text.badge.plus").foregroundColor(.secondary)
  311. // TextField("", text: $state.note).multilineTextAlignment(.trailing)
  312. // if state.note != "", isFocused {
  313. // Button { isFocused = false } label: { Image(systemName: "keyboard.chevron.compact.down") }
  314. // .controlSize(.mini)
  315. // }
  316. // }
  317. // .focused($isFocused)
  318. .popover(isPresented: $isPromptPresented) {
  319. presetPopover
  320. }
  321. HStack {
  322. Spacer()
  323. Button {
  324. if treatmentsViewMode == .calcMode {
  325. state.addCarbs(override, fetch: editMode)
  326. treatmentsViewMode = .editMode
  327. calcButtonPressed.toggle()
  328. } else {
  329. state.backToCarbsView(complexEntry: true, meal, override: false)
  330. treatmentsViewMode = .calcMode
  331. }
  332. }
  333. label: {
  334. // Text((state.skipBolus && !override && !editMode) ? "Save" : "Calculate Bolus")
  335. // if carbs > 0 and it is the first entry then go into edit mode
  336. // conditionally change text to 'edit meal'
  337. // Text(!editMode ? "Calculate" : "Edit Meal")
  338. if treatmentsViewMode == .calcMode {
  339. Text("Calculate")
  340. } else {
  341. Text("Edit meal")
  342. }
  343. }.disabled(empty)
  344. Spacer()
  345. }
  346. } header: { Text("Carbs") }.listRowBackground(Color.chart)
  347. Section {
  348. mealPresets
  349. }.listRowBackground(Color.chart)
  350. // MARK: ADDING END
  351. Section {
  352. HStack {
  353. Button(action: {
  354. showInfo.toggle()
  355. }, label: {
  356. Image(systemName: "info.circle")
  357. Text("Calculations")
  358. })
  359. .foregroundStyle(.blue)
  360. .font(.footnote)
  361. .buttonStyle(PlainButtonStyle())
  362. .frame(maxWidth: .infinity, alignment: .leading)
  363. if state.fattyMeals {
  364. Spacer()
  365. Toggle(isOn: $state.useFattyMealCorrectionFactor) {
  366. Text("Fatty Meal")
  367. }
  368. .toggleStyle(CheckboxToggleStyle())
  369. .font(.footnote)
  370. .onChange(of: state.useFattyMealCorrectionFactor) { _ in
  371. state.insulinCalculated = state.calculateInsulin()
  372. if state.useFattyMealCorrectionFactor {
  373. state.useSuperBolus = false
  374. }
  375. }
  376. }
  377. if state.sweetMeals {
  378. Spacer()
  379. Toggle(isOn: $state.useSuperBolus) {
  380. Text("Super Bolus")
  381. }
  382. .toggleStyle(CheckboxToggleStyle())
  383. .font(.footnote)
  384. .onChange(of: state.useSuperBolus) { _ in
  385. state.insulinCalculated = state.calculateInsulin()
  386. if state.useSuperBolus {
  387. state.useFattyMealCorrectionFactor = false
  388. }
  389. }
  390. }
  391. }
  392. if state.waitForSuggestion {
  393. HStack {
  394. Text("Wait please").foregroundColor(.secondary)
  395. Spacer()
  396. ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
  397. }
  398. } else {
  399. HStack {
  400. Text("Recommended Bolus")
  401. Spacer()
  402. Text(
  403. formatter
  404. .string(from: Double(state.insulinCalculated) as NSNumber) ?? ""
  405. )
  406. Text(
  407. NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
  408. ).foregroundColor(.secondary)
  409. }.contentShape(Rectangle())
  410. .onTapGesture { state.amount = state.insulinCalculated }
  411. }
  412. HStack {
  413. Text("Bolus")
  414. Spacer()
  415. DecimalTextField(
  416. "0",
  417. value: $state.amount,
  418. formatter: formatter,
  419. autofocus: false,
  420. cleanInput: true
  421. )
  422. Text(exceededMaxBolus ? "😵" : " U").foregroundColor(.secondary)
  423. }
  424. .onChange(of: state.amount) { newValue in
  425. if newValue > state.maxBolus {
  426. exceededMaxBolus = true
  427. } else {
  428. exceededMaxBolus = false
  429. }
  430. }
  431. } header: { Text("Bolus") }.listRowBackground(Color.chart)
  432. if state.amount > 0 {
  433. Section {
  434. Button {
  435. keepForNextWiew = true
  436. state.add()
  437. state.hideModal()
  438. ///check if user pressed the calc button or just wanted to enter carbs
  439. ///otherwise carb entry is doubled
  440. if !calcButtonPressed {
  441. state.addCarbs(override, fetch: editMode)
  442. }
  443. }
  444. label: { Text(exceededMaxBolus ? "Max Bolus exceeded!" : "Enact bolus") }
  445. .frame(maxWidth: .infinity, alignment: .center)
  446. .disabled(disabled)
  447. .listRowBackground(!disabled ? Color(.systemBlue) : Color(.systemGray4))
  448. .tint(.white)
  449. }
  450. }
  451. if state.amount <= 0 {
  452. Section {
  453. Button {
  454. keepForNextWiew = true
  455. state.hideModal()
  456. ///check if user pressed the calc button or just wanted to enter carbs
  457. ///otherwise carb entry is doubled
  458. if !calcButtonPressed {
  459. state.addCarbs(override, fetch: editMode)
  460. }
  461. }
  462. label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
  463. }.listRowBackground(Color.chart)
  464. }
  465. }.scrollContentBackground(.hidden).background(color)
  466. .blur(radius: showInfo ? 3 : 0)
  467. .navigationTitle("Treatments")
  468. .navigationBarTitleDisplayMode(.large)
  469. .toolbar(content: {
  470. ToolbarItem(placement: .topBarLeading) {
  471. Button {
  472. state.hideModal()
  473. } label: {
  474. Text("Close")
  475. }
  476. }
  477. })
  478. .onAppear {
  479. configureView {
  480. state.waitForSuggestionInitial = waitForSuggestion
  481. state.waitForSuggestion = waitForSuggestion
  482. state.insulinCalculated = state.calculateInsulin()
  483. }
  484. }
  485. .onDisappear {
  486. if hasFatOrProtein, !keepForNextWiew, state.useCalc {
  487. state.delete(deleteTwice: true, meal: meal)
  488. } else if !keepForNextWiew, state.useCalc {
  489. state.delete(deleteTwice: false, meal: meal)
  490. }
  491. }
  492. .sheet(isPresented: $showInfo) {
  493. calculationsDetailView
  494. .presentationDetents(
  495. [fetch ? .large : .fraction(0.9), .large],
  496. selection: $calculatorDetent
  497. )
  498. }
  499. }
  500. var predictionChart: some View {
  501. ZStack {
  502. PredictionView(
  503. predictions: $state.predictions, units: $state.units, eventualBG: $state.evBG, target: $state.target,
  504. displayPredictions: $state.displayPredictions
  505. )
  506. }
  507. }
  508. var calcSettingsFirstRow: some View {
  509. GridRow {
  510. Group {
  511. Text("Carb Ratio:")
  512. .foregroundColor(.secondary)
  513. }.gridCellAnchor(.leading)
  514. Group {
  515. Text("ISF:")
  516. .foregroundColor(.secondary)
  517. }.gridCellAnchor(.leading)
  518. VStack {
  519. Text("Target:")
  520. .foregroundColor(.secondary)
  521. }.gridCellAnchor(.leading)
  522. }
  523. }
  524. var calcSettingsSecondRow: some View {
  525. GridRow {
  526. Text(state.carbRatio.formatted() + " " + NSLocalizedString("g/U", comment: " grams per Unit"))
  527. .gridCellAnchor(.leading)
  528. Text(
  529. state.isf.formatted() + " " + state.units
  530. .rawValue + NSLocalizedString("/U", comment: "/Insulin unit")
  531. ).gridCellAnchor(.leading)
  532. let target = state.units == .mmolL ? state.target.asMmolL : state.target
  533. Text(
  534. target
  535. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  536. " " + state.units.rawValue
  537. ).gridCellAnchor(.leading)
  538. }
  539. }
  540. var calcGlucoseFirstRow: some View {
  541. GridRow(alignment: .center) {
  542. let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
  543. let target = state.units == .mmolL ? state.target.asMmolL : state.target
  544. Text("Glucose:").foregroundColor(.secondary)
  545. let firstRow = currentBG
  546. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  547. + " - " +
  548. target
  549. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  550. + " = " +
  551. state.targetDifference
  552. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  553. Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
  554. .gridColumnAlignment(.leading)
  555. HStack {
  556. Text(
  557. self.insulinRounder(state.targetDifferenceInsulin).formatted()
  558. )
  559. Text("U").foregroundColor(.secondary)
  560. }.fontWeight(.bold)
  561. .gridColumnAlignment(.trailing)
  562. }
  563. }
  564. var calcGlucoseSecondRow: some View {
  565. GridRow(alignment: .center) {
  566. let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
  567. Text(
  568. currentBG
  569. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  570. " " +
  571. state.units.rawValue
  572. )
  573. let secondRow = state.targetDifference
  574. .formatted(
  575. .number.grouping(.never).rounded()
  576. .precision(.fractionLength(fractionDigits))
  577. )
  578. + " / " +
  579. state.isf.formatted()
  580. + " ≈ " +
  581. self.insulinRounder(state.targetDifferenceInsulin).formatted()
  582. Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
  583. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  584. }
  585. }
  586. var calcGlucoseFormulaRow: some View {
  587. GridRow(alignment: .top) {
  588. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  589. Text("(Current - Target) / ISF").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  590. .gridColumnAlignment(.leading)
  591. .gridCellColumns(2)
  592. }
  593. .font(.caption)
  594. }
  595. var calcIOBRow: some View {
  596. GridRow(alignment: .center) {
  597. HStack {
  598. Text("IOB:").foregroundColor(.secondary)
  599. Text(
  600. self.insulinRounder(state.iob).formatted()
  601. )
  602. }
  603. Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
  604. let iobFormatted = self.insulinRounder(state.iob).formatted()
  605. HStack {
  606. Text((state.iob != 0 ? "-" : "") + (state.iob >= 0 ? iobFormatted : "(" + iobFormatted + ")"))
  607. Text("U").foregroundColor(.secondary)
  608. }.fontWeight(.bold)
  609. .gridColumnAlignment(.trailing)
  610. }
  611. }
  612. var calcCOBRow: some View {
  613. GridRow(alignment: .center) {
  614. HStack {
  615. Text("COB:").foregroundColor(.secondary)
  616. Text(
  617. state.cob
  618. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
  619. NSLocalizedString(" g", comment: "grams")
  620. )
  621. }
  622. Text(
  623. state.cob
  624. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
  625. + " / " +
  626. state.carbRatio.formatted()
  627. + " ≈ " +
  628. self.insulinRounder(state.wholeCobInsulin).formatted()
  629. )
  630. .foregroundColor(.secondary)
  631. .gridColumnAlignment(.leading)
  632. HStack {
  633. Text(
  634. self.insulinRounder(state.wholeCobInsulin).formatted()
  635. )
  636. Text("U").foregroundColor(.secondary)
  637. }.fontWeight(.bold)
  638. .gridColumnAlignment(.trailing)
  639. }
  640. }
  641. var calcCOBFormulaRow: some View {
  642. GridRow(alignment: .center) {
  643. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  644. Text("COB / Carb Ratio").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  645. .gridColumnAlignment(.leading)
  646. .gridCellColumns(2)
  647. }
  648. .font(.caption)
  649. }
  650. var calcDeltaRow: some View {
  651. GridRow(alignment: .center) {
  652. Text("Delta:").foregroundColor(.secondary)
  653. let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
  654. Text(
  655. deltaBG
  656. .formatted(
  657. .number.grouping(.never).rounded()
  658. .precision(.fractionLength(fractionDigits))
  659. )
  660. + " / " +
  661. state.isf.formatted()
  662. + " ≈ " +
  663. self.insulinRounder(state.fifteenMinInsulin).formatted()
  664. )
  665. .foregroundColor(.secondary)
  666. .gridColumnAlignment(.leading)
  667. HStack {
  668. Text(
  669. self.insulinRounder(state.fifteenMinInsulin).formatted()
  670. )
  671. Text("U").foregroundColor(.secondary)
  672. }.fontWeight(.bold)
  673. .gridColumnAlignment(.trailing)
  674. }
  675. }
  676. var calcDeltaFormulaRow: some View {
  677. GridRow(alignment: .center) {
  678. let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
  679. Text(
  680. deltaBG
  681. .formatted(
  682. .number.grouping(.never).rounded()
  683. .precision(.fractionLength(fractionDigits))
  684. ) + " " +
  685. state.units.rawValue
  686. )
  687. Text("15min Delta / ISF").font(.caption).foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  688. .gridColumnAlignment(.leading)
  689. .gridCellColumns(2).padding(.top, 5)
  690. }
  691. }
  692. var calcFullBolusRow: some View {
  693. GridRow(alignment: .center) {
  694. Text("Full Bolus")
  695. .foregroundColor(.secondary)
  696. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  697. HStack {
  698. Text(self.insulinRounder(state.wholeCalc).formatted())
  699. .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
  700. Text("U").foregroundColor(.secondary)
  701. }.gridColumnAlignment(.trailing)
  702. .fontWeight(.bold)
  703. }
  704. }
  705. var calcSuperBolusRow: some View {
  706. GridRow(alignment: .center) {
  707. Text("Super Bolus")
  708. .foregroundColor(.secondary)
  709. Text("Added to Result").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
  710. HStack {
  711. Text("+" + self.insulinRounder(state.superBolusInsulin).formatted())
  712. .foregroundStyle(Color.loopRed)
  713. Text("U").foregroundColor(.secondary)
  714. }.gridColumnAlignment(.trailing)
  715. .fontWeight(.bold)
  716. }
  717. }
  718. var calcResultRow: some View {
  719. GridRow(alignment: .center) {
  720. Text("Result").fontWeight(.bold)
  721. HStack {
  722. Text(state.useSuperBolus ? "(" : "")
  723. .foregroundColor(.loopRed)
  724. + Text(state.fraction.formatted())
  725. + Text(" x ")
  726. .foregroundColor(.secondary)
  727. // if fatty meal is chosen
  728. + Text(state.useFattyMealCorrectionFactor ? state.fattyMealFactor.formatted() : "")
  729. .foregroundColor(.orange)
  730. + Text(state.useFattyMealCorrectionFactor ? " x " : "")
  731. .foregroundColor(.secondary)
  732. // endif fatty meal is chosen
  733. + Text(self.insulinRounder(state.wholeCalc).formatted())
  734. .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
  735. // if superbolus is chosen
  736. + Text(state.useSuperBolus ? ")" : "")
  737. .foregroundColor(.loopRed)
  738. + Text(state.useSuperBolus ? " + " : "")
  739. .foregroundColor(.secondary)
  740. + Text(state.useSuperBolus ? state.superBolusInsulin.formatted() : "")
  741. .foregroundColor(.loopRed)
  742. // endif superbolus is chosen
  743. + Text(" ≈ ")
  744. .foregroundColor(.secondary)
  745. }
  746. .gridColumnAlignment(.leading)
  747. HStack {
  748. Text(self.insulinRounder(state.insulinCalculated).formatted())
  749. .fontWeight(.bold)
  750. .foregroundColor(.blue)
  751. Text("U").foregroundColor(.secondary)
  752. }
  753. .gridColumnAlignment(.trailing)
  754. .fontWeight(.bold)
  755. }
  756. }
  757. var calcResultFormulaRow: some View {
  758. GridRow(alignment: .bottom) {
  759. if state.useFattyMealCorrectionFactor {
  760. Text("Factor x Fatty Meal Factor x Full Bolus")
  761. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  762. .font(.caption)
  763. .gridCellAnchor(.center)
  764. .gridCellColumns(3)
  765. } else if state.useSuperBolus {
  766. Text("(Factor x Full Bolus) + Super Bolus")
  767. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  768. .font(.caption)
  769. .gridCellAnchor(.center)
  770. .gridCellColumns(3)
  771. } else {
  772. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  773. Text("Factor x Full Bolus")
  774. .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
  775. .font(.caption)
  776. .padding(.top, 5)
  777. .gridCellAnchor(.leading)
  778. .gridCellColumns(2)
  779. }
  780. }
  781. }
  782. var calculationsDetailView: some View {
  783. NavigationStack {
  784. ScrollView {
  785. Grid(alignment: .topLeading, horizontalSpacing: 3, verticalSpacing: 0) {
  786. GridRow {
  787. Text("Calculations").fontWeight(.bold).gridCellColumns(3).gridCellAnchor(.center).padding(.vertical)
  788. }
  789. calcSettingsFirstRow
  790. calcSettingsSecondRow
  791. DividerCustom()
  792. if fetch {
  793. // meal entries as grid rows
  794. GridRow {
  795. if let carbs = meal.first?.carbs, carbs > 0 {
  796. Text("Carbs").foregroundColor(.secondary)
  797. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  798. HStack {
  799. Text(carbs.formatted())
  800. Text("g").foregroundColor(.secondary)
  801. }.gridCellAnchor(.trailing)
  802. }
  803. }
  804. GridRow {
  805. if let fat = meal.first?.fat, fat > 0 {
  806. Text("Fat").foregroundColor(.secondary)
  807. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  808. HStack {
  809. Text(fat.formatted())
  810. Text("g").foregroundColor(.secondary)
  811. }.gridCellAnchor(.trailing)
  812. }
  813. }
  814. GridRow {
  815. if let protein = meal.first?.protein, protein > 0 {
  816. Text("Protein").foregroundColor(.secondary)
  817. Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
  818. HStack {
  819. Text(protein.formatted())
  820. Text("g").foregroundColor(.secondary)
  821. }.gridCellAnchor(.trailing)
  822. }
  823. }
  824. GridRow {
  825. if let note = meal.first?.note, note != "" {
  826. Text("Note").foregroundColor(.secondary)
  827. Text(note).foregroundColor(.secondary).gridCellColumns(2).gridCellAnchor(.trailing)
  828. }
  829. }
  830. DividerCustom()
  831. }
  832. GridRow {
  833. Text("Detailed Calculation Steps").gridCellColumns(3).gridCellAnchor(.center)
  834. .padding(.bottom, 10)
  835. }
  836. calcGlucoseFirstRow
  837. calcGlucoseSecondRow.padding(.bottom, 5)
  838. calcGlucoseFormulaRow
  839. DividerCustom()
  840. calcIOBRow
  841. DividerCustom()
  842. calcCOBRow.padding(.bottom, 5)
  843. calcCOBFormulaRow
  844. DividerCustom()
  845. calcDeltaRow
  846. calcDeltaFormulaRow
  847. DividerCustom()
  848. calcFullBolusRow
  849. if state.useSuperBolus {
  850. DividerCustom()
  851. calcSuperBolusRow
  852. }
  853. DividerDouble()
  854. calcResultRow
  855. calcResultFormulaRow
  856. }
  857. Spacer()
  858. Button { showInfo = false }
  859. label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
  860. .buttonStyle(.bordered)
  861. .padding(.top)
  862. }
  863. .padding([.horizontal, .bottom])
  864. .font(.system(size: 15))
  865. }
  866. }
  867. private func insulinRounder(_ value: Decimal) -> Decimal {
  868. let toRound = NSDecimalNumber(decimal: value).doubleValue
  869. return Decimal(floor(100 * toRound) / 100)
  870. }
  871. private var disabled: Bool {
  872. state.amount <= 0 || state.amount > state.maxBolus
  873. }
  874. var changed: Bool {
  875. ((meal.first?.carbs ?? 0) > 0) || ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
  876. }
  877. var hasFatOrProtein: Bool {
  878. ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
  879. }
  880. var mealEntries: some View {
  881. VStack {
  882. if let carbs = meal.first?.carbs, carbs > 0 {
  883. HStack {
  884. Text("Carbs").foregroundColor(.secondary)
  885. Spacer()
  886. Text(carbs.formatted())
  887. Text("g").foregroundColor(.secondary)
  888. }
  889. }
  890. if let fat = meal.first?.fat, fat > 0 {
  891. HStack {
  892. Text("Fat").foregroundColor(.secondary)
  893. Spacer()
  894. Text(fat.formatted())
  895. Text("g").foregroundColor(.secondary)
  896. }
  897. }
  898. if let protein = meal.first?.protein, protein > 0 {
  899. HStack {
  900. Text("Protein").foregroundColor(.secondary)
  901. Spacer()
  902. Text(protein.formatted())
  903. Text("g").foregroundColor(.secondary)
  904. }
  905. }
  906. if let note = meal.first?.note, note != "" {
  907. HStack {
  908. Text("Note").foregroundColor(.secondary)
  909. Spacer()
  910. Text(note).foregroundColor(.secondary)
  911. }
  912. }
  913. }
  914. }
  915. }
  916. struct DividerDouble: View {
  917. var body: some View {
  918. VStack(spacing: 2) {
  919. Rectangle()
  920. .frame(height: 1)
  921. .foregroundColor(.gray.opacity(0.65))
  922. Rectangle()
  923. .frame(height: 1)
  924. .foregroundColor(.gray.opacity(0.65))
  925. }
  926. .frame(height: 4)
  927. .padding(.vertical)
  928. }
  929. }
  930. struct DividerCustom: View {
  931. var body: some View {
  932. Rectangle()
  933. .frame(height: 1)
  934. .foregroundColor(.gray.opacity(0.65))
  935. .padding(.vertical)
  936. }
  937. }
  938. }