OverrideProfilesRootView.swift 42 KB

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