OverrideProfilesRootView.swift 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  1. import CoreData
  2. import Foundation
  3. import SwiftUI
  4. import Swinject
  5. extension OverrideProfilesConfig {
  6. struct RootView: BaseView {
  7. let resolver: Resolver
  8. @StateObject var state = StateModel()
  9. @State private var isEditing = false
  10. @State private var showOverrideCreationSheet = false
  11. @State private var showingDetail = false
  12. @State private var showCheckmark: Bool = false
  13. @State private var selectedPresetID: String?
  14. @State private var selectedOverride: OverrideStored?
  15. // temp targets
  16. @State private var isPromptPresented = false
  17. @State private var isRemoveAlertPresented = false
  18. @State private var removeAlert: Alert?
  19. @State private var isEditingTT = false
  20. @Environment(\.managedObjectContext) var moc
  21. @Environment(\.colorScheme) var colorScheme
  22. var color: LinearGradient {
  23. colorScheme == .dark ? LinearGradient(
  24. gradient: Gradient(colors: [
  25. Color.bgDarkBlue,
  26. Color.bgDarkerDarkBlue
  27. ]),
  28. startPoint: .top,
  29. endPoint: .bottom
  30. )
  31. :
  32. LinearGradient(
  33. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  34. startPoint: .top,
  35. endPoint: .bottom
  36. )
  37. }
  38. @FetchRequest(
  39. entity: TempTargetsSlider.entity(),
  40. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
  41. ) var isEnabledArray: FetchedResults<TempTargetsSlider>
  42. private var formatter: NumberFormatter {
  43. let formatter = NumberFormatter()
  44. formatter.numberStyle = .decimal
  45. formatter.maximumFractionDigits = 0
  46. return formatter
  47. }
  48. private var glucoseFormatter: NumberFormatter {
  49. let formatter = NumberFormatter()
  50. formatter.numberStyle = .decimal
  51. formatter.maximumFractionDigits = 0
  52. if state.units == .mmolL {
  53. formatter.maximumFractionDigits = 1
  54. }
  55. formatter.roundingMode = .halfUp
  56. return formatter
  57. }
  58. <<<<<<< HEAD
  59. var body: some View {
  60. VStack {
  61. Picker("Tab", selection: $state.selectedTab) {
  62. ForEach(Tab.allCases) { tab in
  63. Text(NSLocalizedString(tab.name, comment: "")).tag(tab)
  64. }
  65. }
  66. .pickerStyle(.segmented).padding(.horizontal, 10)
  67. =======
  68. var presetPopover: some View {
  69. Form {
  70. nameSection(header: "Enter a name")
  71. settingsSection(header: "Settings to save")
  72. Section {
  73. Button("Save") {
  74. state.savePreset()
  75. isSheetPresented = false
  76. }
  77. .disabled(
  78. state.profileName.isEmpty || fetchedProfiles
  79. .contains(where: { $0.name == state.profileName })
  80. )
  81. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  82. Form {
  83. switch state.selectedTab {
  84. case .overrides: overrides()
  85. case .tempTargets: tempTargets() }
  86. }.scrollContentBackground(.hidden).background(color)
  87. .onAppear(perform: configureView)
  88. .navigationBarTitle("Adjustments")
  89. .navigationBarTitleDisplayMode(.large)
  90. .toolbar {
  91. ToolbarItem(placement: .topBarTrailing) {
  92. switch state.selectedTab {
  93. case .overrides:
  94. Button(action: {
  95. showOverrideCreationSheet = true
  96. }, label: {
  97. HStack {
  98. Text("Add Override")
  99. Image(systemName: "plus")
  100. }
  101. })
  102. default:
  103. EmptyView()
  104. }
  105. }
  106. }
  107. <<<<<<< HEAD
  108. .sheet(isPresented: $state.showOverrideEditSheet, onDismiss: {
  109. Task {
  110. await state.resetStateVariables()
  111. state.showOverrideEditSheet = false
  112. }
  113. }) {
  114. if let override = selectedOverride {
  115. EditOverrideForm(overrideToEdit: override, state: state)
  116. }
  117. }
  118. .sheet(isPresented: $showOverrideCreationSheet, onDismiss: {
  119. Task {
  120. await state.resetStateVariables()
  121. showOverrideCreationSheet = false
  122. }
  123. }) {
  124. AddOverrideForm(state: state)
  125. }
  126. }.background(color)
  127. }
  128. @ViewBuilder func overrides() -> some View {
  129. if state.overridePresets.isNotEmpty {
  130. overridePresets
  131. } else {
  132. defaultText
  133. }
  134. if state.isEnabled, state.activeOverrideName.isNotEmpty {
  135. currentActiveOverride
  136. }
  137. if state.overridePresets.isNotEmpty || state.currentActiveOverride != nil {
  138. cancelOverrideButton
  139. }
  140. }
  141. private var defaultText: some View {
  142. Section {} header: {
  143. Text("Add Preset or Override by tapping the '+'").foregroundStyle(.secondary)
  144. }
  145. }
  146. private var overridePresets: some View {
  147. Section {
  148. ForEach(state.overridePresets) { preset in
  149. overridesView(for: preset)
  150. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  151. Button(role: .none) {
  152. Task {
  153. await state.invokeOverridePresetDeletion(preset.objectID)
  154. }
  155. } label: {
  156. Label("Delete", systemImage: "trash")
  157. .tint(.red)
  158. }
  159. Button(action: {
  160. // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
  161. selectedOverride = preset
  162. state.showOverrideEditSheet = true
  163. }, label: {
  164. Label("Edit", systemImage: "pencil")
  165. .tint(.blue)
  166. })
  167. }
  168. }
  169. .onMove(perform: state.reorderOverride)
  170. .listRowBackground(Color.chart)
  171. } header: {
  172. Text("Presets")
  173. } footer: {
  174. HStack {
  175. Image(systemName: "hand.draw.fill")
  176. Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
  177. =======
  178. .tint(.red)
  179. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  180. }
  181. }
  182. }
  183. <<<<<<< HEAD
  184. private var currentActiveOverride: some View {
  185. Section {
  186. HStack {
  187. Text("\(state.activeOverrideName) is running")
  188. Spacer()
  189. Image(systemName: "square.and.pencil")
  190. .foregroundStyle(Color.blue)
  191. }
  192. .contentShape(Rectangle())
  193. .onTapGesture {
  194. Task {
  195. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  196. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  197. await state.duplicateOverridePresetAndCancelPreviousOverride()
  198. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  199. selectedOverride = state.currentActiveOverride
  200. /// Now we can show the Edit sheet
  201. state.showOverrideEditSheet = true
  202. }
  203. }
  204. }
  205. .listRowBackground(Color.blue.opacity(0.2))
  206. }
  207. private var cancelOverrideButton: some View {
  208. Button(action: {
  209. Task {
  210. // Save cancelled Override in OverrideRunStored Entity
  211. // Cancel ALL active Override
  212. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  213. }
  214. }, label: {
  215. Text("Cancel Override")
  216. })
  217. .frame(maxWidth: .infinity, alignment: .center)
  218. .disabled(!state.isEnabled)
  219. .listRowBackground(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
  220. .tint(.white)
  221. }
  222. @ViewBuilder func tempTargets() -> some View {
  223. if !state.presetsTT.isEmpty {
  224. Section(header: Text("Presets")) {
  225. ForEach(state.presetsTT) { preset in
  226. presetView(for: preset)
  227. }
  228. }.listRowBackground(Color.chart)
  229. }
  230. HStack {
  231. Text("Experimental")
  232. Toggle(isOn: $state.viewPercantage) {}.controlSize(.mini)
  233. Image(systemName: "figure.highintensity.intervaltraining")
  234. Image(systemName: "fork.knife")
  235. }.listRowBackground(Color.chart)
  236. if state.viewPercantage {
  237. Section {
  238. VStack {
  239. Text("\(state.percentageTT.formatted(.number)) % Insulin")
  240. .foregroundColor(isEditingTT ? .orange : .blue)
  241. .font(.largeTitle)
  242. .padding(.vertical)
  243. Slider(
  244. value: $state.percentageTT,
  245. in: 15 ...
  246. min(Double(state.maxValue * 100), 200),
  247. step: 1,
  248. onEditingChanged: { editing in
  249. isEditingTT = editing
  250. }
  251. )
  252. // Only display target slider when not 100 %
  253. if state.percentageTT != 100 {
  254. Spacer()
  255. Divider()
  256. Text(
  257. =======
  258. var editPresetPopover: some View {
  259. Form {
  260. nameSection(header: "Change name?")
  261. settingsConfig(header: "Change settings")
  262. Section {
  263. Button("Save") {
  264. guard let selectedPreset = selectedPreset else { return }
  265. state.updatePreset(selectedPreset)
  266. isEditSheetPresented = false
  267. }
  268. .disabled(!hasChanges())
  269. Button("Cancel") {
  270. isEditSheetPresented = false
  271. }
  272. .tint(.red)
  273. }
  274. }
  275. .onAppear {
  276. if let preset = selectedPreset {
  277. originalPreset = preset
  278. state.populateSettings(from: preset)
  279. }
  280. }
  281. .onDisappear {
  282. state.savedSettings()
  283. }
  284. }
  285. @ViewBuilder private func nameSection(header: String) -> some View {
  286. Section {
  287. TextField("Profile override name", text: $state.profileName)
  288. } header: {
  289. Text(header)
  290. }
  291. }
  292. @ViewBuilder private func settingsConfig(header: String) -> some View {
  293. Section {
  294. VStack {
  295. Spacer()
  296. Text("\(state.percentage.formatted(.number)) %")
  297. .foregroundColor(
  298. state
  299. .percentage >= 130 ? .red :
  300. (isEditing ? .orange : .blue)
  301. )
  302. .font(.largeTitle)
  303. Slider(
  304. value: $state.percentage,
  305. in: 10 ... 200,
  306. step: 1,
  307. onEditingChanged: { editing in
  308. isEditing = editing
  309. }
  310. ).accentColor(state.percentage >= 130 ? .red : .blue)
  311. Spacer()
  312. Toggle(isOn: $state._indefinite) {
  313. Text("Enable indefinitely")
  314. }
  315. }
  316. if !state._indefinite {
  317. HStack {
  318. Text("Duration")
  319. TextFieldWithToolBar(text: $state.duration, placeholder: "0", numberFormatter: formatter)
  320. Text("minutes").foregroundColor(.secondary)
  321. }
  322. }
  323. HStack {
  324. Toggle(isOn: $state.override_target) {
  325. Text("Override Profile Target")
  326. }
  327. }
  328. if state.override_target {
  329. HStack {
  330. Text("Target Glucose")
  331. TextFieldWithToolBar(text: $state.target, placeholder: "0", numberFormatter: glucoseFormatter)
  332. Text(state.units.rawValue).foregroundColor(.secondary)
  333. }
  334. }
  335. HStack {
  336. Toggle(isOn: $state.advancedSettings) {
  337. Text("More options")
  338. }
  339. }
  340. if state.advancedSettings {
  341. HStack {
  342. Toggle(isOn: $state.smbIsOff) {
  343. Text("Always Disable SMBs")
  344. }
  345. }
  346. if !state.smbIsOff {
  347. HStack {
  348. Toggle(isOn: $state.smbIsScheduledOff) {
  349. Text("Schedule when SMBs are Off")
  350. }
  351. }
  352. if state.smbIsScheduledOff {
  353. HStack {
  354. Text("First Hour SMBs are Off (24 hours)")
  355. TextFieldWithToolBar(text: $state.start, placeholder: "0", numberFormatter: formatter)
  356. Text("hour").foregroundColor(.secondary)
  357. }
  358. HStack {
  359. Text("First Hour SMBs are Resumed (24 hours)")
  360. TextFieldWithToolBar(text: $state.end, placeholder: "0", numberFormatter: formatter)
  361. Text("hour").foregroundColor(.secondary)
  362. }
  363. }
  364. }
  365. HStack {
  366. Toggle(isOn: $state.isfAndCr) {
  367. Text("Change ISF and CR")
  368. }
  369. }
  370. if !state.isfAndCr {
  371. HStack {
  372. Toggle(isOn: $state.isf) {
  373. Text("Change ISF")
  374. }
  375. }
  376. HStack {
  377. Toggle(isOn: $state.cr) {
  378. Text("Change CR")
  379. }
  380. }
  381. }
  382. HStack {
  383. Text("SMB Minutes")
  384. TextFieldWithToolBar(text: $state.smbMinutes, placeholder: "0", numberFormatter: formatter)
  385. Text("minutes").foregroundColor(.secondary)
  386. }
  387. HStack {
  388. Text("UAM SMB Minutes")
  389. TextFieldWithToolBar(text: $state.uamMinutes, placeholder: "0", numberFormatter: formatter)
  390. Text("minutes").foregroundColor(.secondary)
  391. }
  392. }
  393. } header: {
  394. Text(header)
  395. }
  396. }
  397. @ViewBuilder private func settingsSection(header: String) -> some View {
  398. Section(header: Text(header)) {
  399. let percentString = Text("Override: \(Int(state.percentage))%")
  400. let targetString = state
  401. .target != 0 ? Text("Target: \(state.target.formatted()) \(state.units.rawValue)") : Text("")
  402. let durationString = state
  403. ._indefinite ? Text("Duration: Indefinite") : Text("Duration: \(state.duration.formatted()) minutes")
  404. let isfString = state.isf ? Text("Change ISF") : Text("")
  405. let crString = state.cr ? Text("Change CR") : Text("")
  406. let smbString = state.smbIsOff ? Text("Disable SMB") : Text("")
  407. let scheduledSMBString = state.smbIsScheduledOff ? Text("SMB Schedule On") : Text("")
  408. let maxMinutesSMBString = state
  409. .smbMinutes != 0 ? Text("\(state.smbMinutes.formatted()) SMB Basal minutes") : Text("")
  410. let maxMinutesUAMString = state
  411. .uamMinutes != 0 ? Text("\(state.uamMinutes.formatted()) UAM Basal minutes") : Text("")
  412. VStack(alignment: .leading, spacing: 2) {
  413. percentString
  414. if targetString != Text("") { targetString }
  415. if durationString != Text("") { durationString }
  416. if isfString != Text("") { isfString }
  417. if crString != Text("") { crString }
  418. if smbString != Text("") { smbString }
  419. if scheduledSMBString != Text("") { scheduledSMBString }
  420. if maxMinutesSMBString != Text("") { maxMinutesSMBString }
  421. if maxMinutesUAMString != Text("") { maxMinutesUAMString }
  422. }
  423. .foregroundColor(.secondary)
  424. .font(.caption)
  425. }
  426. }
  427. var body: some View {
  428. Form {
  429. if state.presets.isNotEmpty {
  430. Section {
  431. ForEach(fetchedProfiles.indices, id: \.self) { index in
  432. let preset = fetchedProfiles[index]
  433. profilesView(for: preset)
  434. .swipeActions {
  435. Button(role: .none) {
  436. indexToDelete = index
  437. profileNameToDelete = preset.name ?? "this profile"
  438. showDeleteAlert = true
  439. } label: {
  440. Label("Delete", systemImage: "trash")
  441. }.tint(.red)
  442. Button {
  443. selectedPreset = preset
  444. state.profileName = preset.name ?? ""
  445. isEditSheetPresented = true
  446. } label: {
  447. Label("Edit", systemImage: "square.and.pencil")
  448. }.tint(.blue)
  449. }
  450. }
  451. }
  452. header: { Text("Activate profile override") }
  453. footer: { VStack(alignment: .leading) {
  454. Text("Swipe left on a profile to edit or delete it.")
  455. }
  456. }
  457. }
  458. settingsConfig(header: "Insulin")
  459. Section {
  460. HStack {
  461. Button("Start new Profile") {
  462. showAlert.toggle()
  463. alertSring = "\(state.percentage.formatted(.number)) %, " +
  464. (
  465. state.duration > 0 || !state
  466. ._indefinite ?
  467. (
  468. state
  469. .duration
  470. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
  471. " min."
  472. ) :
  473. NSLocalizedString(" infinite duration.", comment: "")
  474. ) +
  475. (
  476. (state.target == 0 || !state.override_target) ? "" :
  477. (" Target: " + state.target.formatted() + " " + state.units.rawValue + ".")
  478. )
  479. +
  480. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  481. (
  482. state
  483. .units == .mmolL ?
  484. "\(state.computeTarget().asMmolL.formatted(.number.grouping(.never).rounded().precision(.fractionLength(1)))) mmol/L" :
  485. "\(state.computeTarget().formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))) mg/dl"
  486. )
  487. + NSLocalizedString(" Target Glucose", comment: "")
  488. )
  489. .foregroundColor(.green)
  490. .padding(.vertical)
  491. Slider(
  492. value: $state.hbt,
  493. in: 101 ... 295,
  494. step: 1
  495. ).accentColor(.green)
  496. }
  497. <<<<<<< HEAD
  498. }
  499. }.listRowBackground(Color.chart)
  500. } else {
  501. Section(header: Text("Custom")) {
  502. HStack {
  503. Text("Target")
  504. Spacer()
  505. TextFieldWithToolBar(text: $state.low, placeholder: "0", numberFormatter: formatter)
  506. Text(state.units.rawValue).foregroundColor(.secondary)
  507. }
  508. HStack {
  509. Text("Duration")
  510. Spacer()
  511. TextFieldWithToolBar(text: $state.durationTT, placeholder: "0", numberFormatter: formatter)
  512. Text("minutes").foregroundColor(.secondary)
  513. }
  514. DatePicker("Date", selection: $state.date)
  515. HStack {
  516. Button { state.enact() }
  517. label: { Text("Enact") }
  518. .disabled(state.durationTT == 0)
  519. .buttonStyle(BorderlessButtonStyle())
  520. .font(.callout)
  521. .controlSize(.mini)
  522. Button { isPromptPresented = true }
  523. label: { Text("Save as preset") }
  524. .disabled(state.durationTT == 0)
  525. =======
  526. .disabled(unChanged())
  527. .buttonStyle(BorderlessButtonStyle())
  528. .font(.callout)
  529. .controlSize(.mini)
  530. .alert(
  531. "Start Profile",
  532. isPresented: $showAlert,
  533. actions: {
  534. Button("Cancel", role: .cancel) { state.isEnabled = false }
  535. Button("Start Profile", role: .destructive) {
  536. if state._indefinite { state.duration = 0 }
  537. state.isEnabled.toggle()
  538. state.saveSettings()
  539. dismiss()
  540. }
  541. },
  542. message: {
  543. Text(alertSring)
  544. }
  545. )
  546. Button {
  547. isSheetPresented = true
  548. }
  549. label: { Text("Save as Profile") }
  550. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  551. .tint(.orange)
  552. .frame(maxWidth: .infinity, alignment: .trailing)
  553. .buttonStyle(BorderlessButtonStyle())
  554. .font(.callout)
  555. .controlSize(.mini)
  556. <<<<<<< HEAD
  557. }
  558. }.listRowBackground(Color.chart)
  559. }
  560. if state.viewPercantage {
  561. Section {
  562. HStack {
  563. Text("Duration")
  564. Spacer()
  565. TextFieldWithToolBar(text: $state.durationTT, placeholder: "0", numberFormatter: formatter)
  566. Text("minutes").foregroundColor(.secondary)
  567. }
  568. DatePicker("Date", selection: $state.date)
  569. HStack {
  570. Button { state.enact() }
  571. label: { Text("Enact") }
  572. .disabled(state.durationTT == 0)
  573. .buttonStyle(BorderlessButtonStyle())
  574. .font(.callout)
  575. .controlSize(.mini)
  576. Button { isPromptPresented = true }
  577. label: { Text("Save as preset") }
  578. .disabled(state.durationTT == 0)
  579. .tint(.orange)
  580. .frame(maxWidth: .infinity, alignment: .trailing)
  581. .buttonStyle(BorderlessButtonStyle())
  582. .controlSize(.mini)
  583. }
  584. }.listRowBackground(Color.chart)
  585. }
  586. =======
  587. .disabled(unChanged())
  588. }
  589. .sheet(isPresented: $isSheetPresented) {
  590. presetPopover
  591. }
  592. }
  593. footer: {
  594. Text(
  595. "Your profile basal insulin will be adjusted with the override percentage and your profile ISF and CR will be inversly adjusted with the percentage."
  596. )
  597. }
  598. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  599. Section {
  600. Button { state.cancel() }
  601. label: {
  602. HStack {
  603. Spacer()
  604. Text("Cancel Temp Target")
  605. Spacer()
  606. Image(systemName: "xmark.app")
  607. .font(.title)
  608. }
  609. }
  610. .frame(maxWidth: .infinity, alignment: .center)
  611. .disabled(state.storage.current() == nil)
  612. .listRowBackground(state.storage.current() == nil ? Color(.systemGray4) : Color(.systemRed))
  613. .tint(.white)
  614. }.popover(isPresented: $isPromptPresented) {
  615. Form {
  616. Section(header: Text("Enter preset name")) {
  617. TextField("Name", text: $state.newPresetName)
  618. Button {
  619. state.save()
  620. isPromptPresented = false
  621. }
  622. label: { Text("Save") }
  623. Button { isPromptPresented = false }
  624. label: { Text("Cancel") }
  625. }
  626. }
  627. }
  628. .onAppear {
  629. configureView()
  630. state.hbt = isEnabledArray.first?.hbt ?? 160
  631. }
  632. <<<<<<< HEAD
  633. }
  634. private func presetView(for preset: TempTarget) -> some View {
  635. var low = preset.targetBottom
  636. var high = preset.targetTop
  637. if state.units == .mmolL {
  638. low = low?.asMmolL
  639. high = high?.asMmolL
  640. }
  641. let isSelected = preset.id == selectedPresetID
  642. return ZStack(alignment: .trailing, content: {
  643. HStack {
  644. VStack {
  645. HStack {
  646. Text(preset.displayName)
  647. Spacer()
  648. }
  649. HStack(spacing: 2) {
  650. Text(
  651. "\(formatter.string(from: (low ?? 0) as NSNumber)!) - \(formatter.string(from: (high ?? 0) as NSNumber)!)"
  652. )
  653. .foregroundColor(.secondary)
  654. .font(.caption)
  655. Text(state.units.rawValue)
  656. .foregroundColor(.secondary)
  657. .font(.caption)
  658. Text("for")
  659. .foregroundColor(.secondary)
  660. .font(.caption)
  661. Text("\(formatter.string(from: preset.duration as NSNumber)!)")
  662. .foregroundColor(.secondary)
  663. .font(.caption)
  664. Text("min")
  665. .foregroundColor(.secondary)
  666. .font(.caption)
  667. Spacer()
  668. }.padding(.top, 2)
  669. }
  670. .contentShape(Rectangle())
  671. .onTapGesture {
  672. state.enactPreset(id: preset.id)
  673. selectedPresetID = preset.id
  674. showCheckmark.toggle()
  675. // deactivate showCheckmark after 3 seconds
  676. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  677. showCheckmark = false
  678. }
  679. }
  680. Image(systemName: "xmark.circle").foregroundColor(showCheckmark && isSelected ? Color.clear : Color.secondary)
  681. .contentShape(Rectangle())
  682. .padding(.vertical)
  683. .onTapGesture {
  684. removeAlert = Alert(
  685. title: Text("Are you sure?"),
  686. message: Text("Delete preset \"\(preset.displayName)\""),
  687. primaryButton: .destructive(Text("Delete"), action: { state.removePreset(id: preset.id) }),
  688. secondaryButton: .cancel()
  689. )
  690. isRemoveAlertPresented = true
  691. }
  692. .alert(isPresented: $isRemoveAlertPresented) {
  693. removeAlert!
  694. }
  695. }
  696. if showCheckmark && isSelected {
  697. // show checkmark to indicate if the preset was actually pressed
  698. Image(systemName: "checkmark.circle.fill")
  699. .imageScale(.large)
  700. .fontWeight(.bold)
  701. .foregroundStyle(Color.green)
  702. }
  703. })
  704. }
  705. @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
  706. let target = state.units == .mmolL ? (((preset.target ?? 0) as NSDecimalNumber) as Decimal)
  707. .asMmolL : (preset.target ?? 0) as Decimal
  708. let duration = (preset.duration ?? 0) as Decimal
  709. let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
  710. let percent = preset.percentage / 100
  711. let perpetual = preset.indefinite
  712. let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
  713. let scheduledSMBstring = (preset.smbIsOff && preset.smbIsAlwaysOff) ? "Scheduled SMBs" : ""
  714. let smbString = (preset.smbIsOff && scheduledSMBstring == "") ? "SMBs are off" : ""
  715. let targetString = target != 0 ? "\(glucoseFormatter.string(from: target as NSNumber)!)" : ""
  716. let maxMinutesSMB = (preset.smbMinutes as Decimal?) != nil ? (preset.smbMinutes ?? 0) as Decimal : 0
  717. let maxMinutesUAM = (preset.uamMinutes as Decimal?) != nil ? (preset.uamMinutes ?? 0) as Decimal : 0
  718. let isfString = preset.isf ? "ISF" : ""
  719. let crString = preset.cr ? "CR" : ""
  720. let dash = crString != "" ? "/" : ""
  721. let isfAndCRstring = isfString + dash + crString
  722. let isSelected = preset.id == selectedPresetID
  723. if name != "" {
  724. ZStack(alignment: .trailing, content: {
  725. HStack {
  726. VStack {
  727. HStack {
  728. Text(name)
  729. Spacer()
  730. }
  731. HStack(spacing: 5) {
  732. Text(percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
  733. if targetString != "" {
  734. Text(targetString)
  735. Text(targetString != "" ? state.units.rawValue : "")
  736. }
  737. if durationString != "" { Text(durationString + (perpetual ? "" : "min")) }
  738. if smbString != "" { Text(smbString).foregroundColor(.secondary).font(.caption) }
  739. if scheduledSMBstring != "" { Text(scheduledSMBstring) }
  740. if preset.advancedSettings {
  741. Text(maxMinutesSMB == 0 ? "" : maxMinutesSMB.formatted() + " SMB")
  742. Text(maxMinutesUAM == 0 ? "" : maxMinutesUAM.formatted() + " UAM")
  743. Text(isfAndCRstring)
  744. }
  745. Spacer()
  746. =======
  747. .onAppear(perform: configureView)
  748. .onAppear { state.savedSettings() }
  749. .navigationBarTitle("Profiles")
  750. .navigationBarTitleDisplayMode(.automatic)
  751. .navigationBarItems(leading: Button("Close", action: state.hideModal))
  752. .sheet(isPresented: $isEditSheetPresented) {
  753. editPresetPopover
  754. .padding()
  755. }
  756. .alert(isPresented: $showDeleteAlert) {
  757. Alert(
  758. title: Text("Delete profile override"),
  759. message: Text("Are you sure you want to delete\n\(profileNameToDelete)?"),
  760. primaryButton: .destructive(Text("Delete")) {
  761. if let index = indexToDelete {
  762. removeProfile(at: IndexSet(integer: index))
  763. }
  764. },
  765. secondaryButton: .cancel()
  766. )
  767. }
  768. }
  769. @ViewBuilder private func profilesView(for preset: OverridePresets) -> some View {
  770. let data = state.profileViewData(for: preset)
  771. if data.name != "" {
  772. HStack {
  773. VStack {
  774. HStack {
  775. Text(data.name)
  776. Spacer()
  777. }
  778. HStack(spacing: 5) {
  779. Text(data.percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
  780. if data.targetString != "" {
  781. Text(data.targetString)
  782. Text(data.targetString != "" ? state.units.rawValue : "")
  783. }
  784. if data.durationString != "" { Text(data.durationString + (data.perpetual ? "" : "min")) }
  785. if data.smbString != "" { Text(data.smbString).foregroundColor(.secondary).font(.caption) }
  786. if data.scheduledSMBString != "" { Text(data.scheduledSMBString) }
  787. if preset.advancedSettings {
  788. Text(data.maxMinutesSMB == 0 ? "" : data.maxMinutesSMB.formatted() + " SMB")
  789. Text(data.maxMinutesUAM == 0 ? "" : data.maxMinutesUAM.formatted() + " UAM")
  790. Text(data.isfAndCRString)
  791. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  792. }
  793. .padding(.top, 2)
  794. .foregroundColor(.secondary)
  795. .font(.caption)
  796. }
  797. .contentShape(Rectangle())
  798. .onTapGesture {
  799. Task {
  800. let objectID = preset.objectID
  801. await state.enactOverridePreset(withID: objectID)
  802. state.hideModal()
  803. showCheckmark.toggle()
  804. selectedPresetID = preset.id
  805. <<<<<<< HEAD
  806. // deactivate showCheckmark after 3 seconds
  807. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  808. showCheckmark = false
  809. }
  810. }
  811. }
  812. }
  813. // show checkmark to indicate if the preset was actually pressed
  814. if showCheckmark && isSelected {
  815. Image(systemName: "checkmark.circle.fill")
  816. .imageScale(.large)
  817. .fontWeight(.bold)
  818. .foregroundStyle(Color.green)
  819. } else {
  820. Image(systemName: "line.3.horizontal")
  821. .imageScale(.medium)
  822. .foregroundStyle(.secondary)
  823. }
  824. })
  825. =======
  826. private func unChanged() -> Bool {
  827. let defaultProfile = state.percentage == 100 && !state.override_target && !state.advancedSettings
  828. let noDurationSpecified = !state._indefinite && state.duration == 0
  829. let targetZeroWithOverride = state.override_target && state.target == 0
  830. let allSettingsDefault = state.percentage == 100 && !state.override_target && !state.smbIsOff && !state
  831. .smbIsScheduledOff && state.smbMinutes == state.defaultSmbMinutes && state.uamMinutes == state.defaultUamMinutes
  832. return defaultProfile || noDurationSpecified || targetZeroWithOverride || allSettingsDefault
  833. }
  834. private func hasChanges() -> Bool {
  835. guard let originalPreset = originalPreset else { return false }
  836. let targetInStateUnits: Decimal
  837. let targetInPresetUnits: Decimal
  838. if state.units == .mmolL {
  839. targetInStateUnits = state.target
  840. targetInPresetUnits = (originalPreset.target as NSDecimalNumber?)?.decimalValue.asMmolL ?? 0
  841. } else {
  842. targetInStateUnits = state.target
  843. targetInPresetUnits = (originalPreset.target as NSDecimalNumber?)?.decimalValue ?? 0
  844. }
  845. let hasChanges = state.profileName != originalPreset.name ||
  846. state.percentage != originalPreset.percentage ||
  847. state.duration != (originalPreset.duration ?? 0) as Decimal ||
  848. state._indefinite != originalPreset.indefinite ||
  849. state.override_target != (originalPreset.target != nil) ||
  850. (state.override_target && targetInStateUnits != targetInPresetUnits) ||
  851. state.smbIsOff != originalPreset.smbIsOff ||
  852. state.smbIsScheduledOff != originalPreset.smbIsScheduledOff ||
  853. state.isf != originalPreset.isf ||
  854. state.cr != originalPreset.cr ||
  855. state.smbMinutes != (originalPreset.smbMinutes ?? 0) as Decimal ||
  856. state.uamMinutes != (originalPreset.uamMinutes ?? 0) as Decimal ||
  857. state.isfAndCr != originalPreset.isfAndCr ||
  858. state.start != (originalPreset.start ?? 0) as Decimal ||
  859. state.end != (originalPreset.end ?? 0) as Decimal
  860. return hasChanges
  861. }
  862. private func removeProfile(at offsets: IndexSet) {
  863. for index in offsets {
  864. let language = fetchedProfiles[index]
  865. moc.delete(language)
  866. }
  867. do {
  868. try moc.save()
  869. } catch {
  870. // To do: add error
  871. >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
  872. }
  873. }
  874. }
  875. }