AlternativeBolusCalcRootView.swift 37 KB

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