AlternativeBolusCalcRootView.swift 37 KB

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