OverrideProfilesRootView.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. import CoreData
  2. import SwiftUI
  3. import Swinject
  4. extension OverrideProfilesConfig {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. @StateObject var state = StateModel()
  8. @State private var isEditing = false
  9. @State private var showAlert = false
  10. @State private var showingDetail = false
  11. @State private var alertString = ""
  12. @State private var selectedPreset: OverridePresets?
  13. @State private var isEditSheetPresented: Bool = false
  14. @State private var alertSring = ""
  15. @State var isSheetPresented: Bool = false
  16. @Environment(\.dismiss) var dismiss
  17. @Environment(\.managedObjectContext) var moc
  18. @FetchRequest(
  19. entity: OverridePresets.entity(),
  20. sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], predicate: NSPredicate(
  21. format: "name != %@", "" as String
  22. )
  23. ) var fetchedProfiles: FetchedResults<OverridePresets>
  24. private var formatter: NumberFormatter {
  25. let formatter = NumberFormatter()
  26. formatter.numberStyle = .decimal
  27. formatter.maximumFractionDigits = 0
  28. return formatter
  29. }
  30. private var glucoseFormatter: NumberFormatter {
  31. let formatter = NumberFormatter()
  32. formatter.numberStyle = .decimal
  33. formatter.maximumFractionDigits = 0
  34. if state.units == .mmolL {
  35. formatter.maximumFractionDigits = 1
  36. }
  37. formatter.roundingMode = .halfUp
  38. return formatter
  39. }
  40. var presetPopover: some View {
  41. Form {
  42. Section {
  43. TextField("Override preset name", text: $state.profileName)
  44. } header: { Text("Enter a name") }
  45. Section(header: Text("Settings to save")) {
  46. let percentString = Text("Override: \(Int(state.percentage))%")
  47. let targetString = state
  48. .target != 0 ? Text("Target: \(state.target.formatted()) \(state.units.rawValue)") :
  49. Text("")
  50. let durationString = state
  51. ._indefinite ? Text("Duration: Indefinite") :
  52. Text("Duration: \(state.duration.formatted()) minutes")
  53. let isfString = state.isf ? Text("Change ISF") : Text("")
  54. let crString = state.cr ? Text("Change CR") : Text("")
  55. let smbString = state.smbIsOff ? Text("Disable SMB") : Text("")
  56. let scheduledSMBString = state.smbIsScheduledOff ? Text("SMB Schedule On") : Text("")
  57. let maxMinutesSMBString = state
  58. .smbMinutes != 0 ? Text("\(state.smbMinutes.formatted()) SMB Basal minutes") :
  59. Text("")
  60. let maxMinutesUAMString = state
  61. .uamMinutes != 0 ? Text("\(state.uamMinutes.formatted()) UAM Basal minutes") :
  62. Text("")
  63. VStack(alignment: .leading, spacing: 2) {
  64. percentString
  65. if targetString != Text("") { targetString }
  66. if durationString != Text("") { durationString }
  67. if isfString != Text("") { isfString }
  68. if crString != Text("") { crString }
  69. if smbString != Text("") { smbString }
  70. if scheduledSMBString != Text("") { scheduledSMBString }
  71. if maxMinutesSMBString != Text("") { maxMinutesSMBString }
  72. if maxMinutesUAMString != Text("") { maxMinutesUAMString }
  73. }
  74. .foregroundColor(.secondary)
  75. .font(.caption)
  76. }
  77. Section {
  78. Button("Save") {
  79. state.savePreset()
  80. isSheetPresented = false
  81. }
  82. .disabled(state.profileName.isEmpty || fetchedProfiles.filter({ $0.name == state.profileName }).isNotEmpty)
  83. Button("Cancel") {
  84. isSheetPresented = false
  85. }
  86. }
  87. }
  88. }
  89. var editPresetPopover: some View {
  90. Form {
  91. Section {
  92. TextField("Override preset name", text: $state.profileName)
  93. } header: { Text("Keep or change name?") }
  94. Section(header: Text("New settings to save")) {
  95. let percentString = Text("Override: \(Int(state.percentage))%")
  96. let targetString = state
  97. .target != 0 ? Text("Target: \(state.target.formatted()) \(state.units.rawValue)") : Text("")
  98. let durationString = state
  99. ._indefinite ? Text("Duration: Indefinite") : Text("Duration: \(state.duration.formatted()) minutes")
  100. let isfString = state.isf ? Text("Change ISF") : Text("")
  101. let crString = state.cr ? Text("Change CR") : Text("")
  102. let smbString = state.smbIsOff ? Text("Disable SMB") : Text("")
  103. let scheduledSMBString = state.smbIsScheduledOff ? Text("SMB Schedule On") : Text("")
  104. let maxMinutesSMBString = state
  105. .smbMinutes != 0 ? Text("\(state.smbMinutes.formatted()) SMB Basal minutes") : Text("")
  106. let maxMinutesUAMString = state
  107. .uamMinutes != 0 ? Text("\(state.uamMinutes.formatted()) UAM Basal minutes") : Text("")
  108. VStack(alignment: .leading, spacing: 2) {
  109. percentString
  110. if targetString != Text("") { targetString }
  111. if durationString != Text("") { durationString }
  112. if isfString != Text("") { isfString }
  113. if crString != Text("") { crString }
  114. if smbString != Text("") { smbString }
  115. if scheduledSMBString != Text("") { scheduledSMBString }
  116. if maxMinutesSMBString != Text("") { maxMinutesSMBString }
  117. if maxMinutesUAMString != Text("") { maxMinutesUAMString }
  118. }
  119. .foregroundColor(.secondary)
  120. .font(.caption)
  121. }
  122. Section {
  123. Button("Save") {
  124. guard let selectedPreset = selectedPreset else { return }
  125. state.updatePreset(selectedPreset)
  126. isEditSheetPresented = false
  127. }
  128. .disabled(state.profileName.isEmpty)
  129. Button("Cancel") {
  130. isEditSheetPresented = false
  131. }
  132. }
  133. }
  134. }
  135. var body: some View {
  136. Form {
  137. if state.presets.isNotEmpty {
  138. Section {
  139. ForEach(fetchedProfiles.indices, id: \.self) { index in
  140. let preset = fetchedProfiles[index]
  141. profilesView(for: preset)
  142. .swipeActions {
  143. Button(role: .destructive) {
  144. removeProfile(at: IndexSet(integer: index))
  145. } label: {
  146. Label("Ta bort", systemImage: "trash")
  147. }
  148. Button {
  149. selectedPreset = preset
  150. state.profileName = preset.name ?? ""
  151. isEditSheetPresented = true
  152. } label: {
  153. Label("Redigera", systemImage: "square.and.pencil")
  154. }.tint(.blue)
  155. }
  156. }
  157. }
  158. header: { Text("Activate preset override") }
  159. footer: { VStack(alignment: .leading) {
  160. Text("Swipe left on a preset to edit or delete it.")
  161. Text("When you want to edit a preset:")
  162. HStack(alignment: .top) {
  163. Text(" •")
  164. Text(
  165. "First use the override configurator below and select the settings you want to include."
  166. )
  167. }
  168. HStack(alignment: .top) {
  169. Text(" •")
  170. Text(
  171. "Then swipe left on the preset you want to change and click the edit symbol."
  172. )
  173. }
  174. HStack(alignment: .top) {
  175. Text(" •")
  176. Text(
  177. "In the pop-up: Use the existing preset name or enter a new name and click save - Done!"
  178. )
  179. }
  180. }
  181. }
  182. }
  183. Section {
  184. VStack {
  185. Slider(
  186. value: $state.percentage,
  187. in: 10 ... 200,
  188. step: 1,
  189. onEditingChanged: { editing in
  190. isEditing = editing
  191. }
  192. ).accentColor(state.percentage >= 130 ? .red : .blue)
  193. Text("\(state.percentage.formatted(.number)) %")
  194. .foregroundColor(
  195. state
  196. .percentage >= 130 ? .red :
  197. (isEditing ? .orange : .blue)
  198. )
  199. .font(.largeTitle)
  200. Spacer()
  201. Toggle(isOn: $state._indefinite) {
  202. Text("Enable indefinitely")
  203. }
  204. }
  205. if !state._indefinite {
  206. HStack {
  207. Text("Duration")
  208. DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: false)
  209. Text("minutes").foregroundColor(.secondary)
  210. }
  211. }
  212. HStack {
  213. Toggle(isOn: $state.override_target) {
  214. Text("Override Profile Target")
  215. }
  216. }
  217. if state.override_target {
  218. HStack {
  219. Text("Target Glucose")
  220. DecimalTextField("0", value: $state.target, formatter: glucoseFormatter, cleanInput: false)
  221. Text(state.units.rawValue).foregroundColor(.secondary)
  222. }
  223. }
  224. HStack {
  225. Toggle(isOn: $state.advancedSettings) {
  226. Text("More options")
  227. }
  228. }
  229. if state.advancedSettings {
  230. HStack {
  231. Toggle(isOn: $state.smbIsOff) {
  232. Text("Always Disable SMBs")
  233. }
  234. }
  235. if !state.smbIsOff {
  236. HStack {
  237. Toggle(isOn: $state.smbIsScheduledOff) {
  238. Text("Schedule when SMBs are Off")
  239. }
  240. }
  241. if state.smbIsScheduledOff {
  242. HStack {
  243. Text("First Hour SMBs are Off (24 hours)")
  244. DecimalTextField("0", value: $state.start, formatter: formatter, cleanInput: false)
  245. Text("hour").foregroundColor(.secondary)
  246. }
  247. HStack {
  248. Text("First Hour SMBs are Resumed (24 hours)")
  249. DecimalTextField("0", value: $state.end, formatter: formatter, cleanInput: false)
  250. Text("hour").foregroundColor(.secondary)
  251. }
  252. }
  253. }
  254. HStack {
  255. Toggle(isOn: $state.isfAndCr) {
  256. Text("Change ISF and CR")
  257. }
  258. }
  259. if !state.isfAndCr {
  260. HStack {
  261. Toggle(isOn: $state.isf) {
  262. Text("Change ISF")
  263. }
  264. }
  265. HStack {
  266. Toggle(isOn: $state.cr) {
  267. Text("Change CR")
  268. }
  269. }
  270. }
  271. HStack {
  272. Text("SMB Minutes")
  273. DecimalTextField(
  274. "0",
  275. value: $state.smbMinutes,
  276. formatter: formatter,
  277. cleanInput: false
  278. )
  279. Text("minutes").foregroundColor(.secondary)
  280. }
  281. HStack {
  282. Text("UAM SMB Minutes")
  283. DecimalTextField(
  284. "0",
  285. value: $state.uamMinutes,
  286. formatter: formatter,
  287. cleanInput: false
  288. )
  289. Text("minutes").foregroundColor(.secondary)
  290. }
  291. }
  292. HStack {
  293. Button("Start new Profile") {
  294. showAlert.toggle()
  295. alertSring = "\(state.percentage.formatted(.number)) %, " +
  296. (
  297. state.duration > 0 || !state
  298. ._indefinite ?
  299. (
  300. state
  301. .duration
  302. .formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) +
  303. " min."
  304. ) :
  305. NSLocalizedString(" infinite duration.", comment: "")
  306. ) +
  307. (
  308. (state.target == 0 || !state.override_target) ? "" :
  309. (" Target: " + state.target.formatted() + " " + state.units.rawValue + ".")
  310. )
  311. +
  312. (
  313. state
  314. .smbIsOff ?
  315. NSLocalizedString(
  316. " SMBs are disabled either by schedule or during the entire duration.",
  317. comment: ""
  318. ) : ""
  319. )
  320. +
  321. "\n\n"
  322. +
  323. NSLocalizedString(
  324. "Starting this override will change your Profiles and/or your Target Glucose used for looping during the entire selected duration. Tapping ”Start Profile” will start your new profile or edit your current active profile.",
  325. comment: ""
  326. )
  327. }
  328. .disabled(unChanged())
  329. .buttonStyle(BorderlessButtonStyle())
  330. .font(.callout)
  331. .controlSize(.mini)
  332. .alert(
  333. "Start Profile",
  334. isPresented: $showAlert,
  335. actions: {
  336. Button("Cancel", role: .cancel) { state.isEnabled = false }
  337. Button("Start Profile", role: .destructive) {
  338. if state._indefinite { state.duration = 0 }
  339. state.isEnabled.toggle()
  340. state.saveSettings()
  341. dismiss()
  342. }
  343. },
  344. message: {
  345. Text(alertSring)
  346. }
  347. )
  348. Button {
  349. isSheetPresented = true
  350. }
  351. label: { Text("Save as Profile") }
  352. .tint(.orange)
  353. .frame(maxWidth: .infinity, alignment: .trailing)
  354. .buttonStyle(BorderlessButtonStyle())
  355. .controlSize(.mini)
  356. .disabled(unChanged())
  357. }
  358. .sheet(isPresented: $isSheetPresented) {
  359. presetPopover
  360. }
  361. }
  362. header: { Text("Insulin") }
  363. footer: {
  364. Text(
  365. "Your profile basal insulin will be adjusted with the override percentage and your profile ISF and CR will be inversly adjusted with the percentage."
  366. )
  367. }
  368. Button("Return to Normal") {
  369. state.cancelProfile()
  370. dismiss()
  371. }
  372. .frame(maxWidth: .infinity, alignment: .center)
  373. .buttonStyle(BorderlessButtonStyle())
  374. .disabled(!state.isEnabled)
  375. .tint(.red)
  376. }
  377. .onAppear(perform: configureView)
  378. .onAppear { state.savedSettings() }
  379. .navigationBarTitle("Profiles")
  380. .navigationBarTitleDisplayMode(.automatic)
  381. .navigationBarItems(leading: Button("Close", action: state.hideModal))
  382. .sheet(isPresented: $isEditSheetPresented) {
  383. editPresetPopover
  384. .padding()
  385. }
  386. }
  387. @ViewBuilder private func profilesView(for preset: OverridePresets) -> some View {
  388. let target = state.units == .mmolL ? (((preset.target ?? 0) as NSDecimalNumber) as Decimal)
  389. .asMmolL : (preset.target ?? 0) as Decimal
  390. let duration = (preset.duration ?? 0) as Decimal
  391. let name = ((preset.name ?? "") == "") || (preset.name?.isEmpty ?? true) ? "" : preset.name!
  392. let percent = preset.percentage / 100
  393. let perpetual = preset.indefinite
  394. let durationString = perpetual ? "" : "\(formatter.string(from: duration as NSNumber)!)"
  395. let scheduledSMBstring = (preset.smbIsOff && preset.smbIsScheduledOff) ? "Scheduled SMBs" : ""
  396. let smbString = (preset.smbIsOff && scheduledSMBstring == "") ? "SMBs are off" : ""
  397. let targetString = target != 0 ? "\(glucoseFormatter.string(from: target as NSNumber)!)" : ""
  398. let maxMinutesSMB = (preset.smbMinutes as Decimal?) != nil ? (preset.smbMinutes ?? 0) as Decimal : 0
  399. let maxMinutesUAM = (preset.uamMinutes as Decimal?) != nil ? (preset.uamMinutes ?? 0) as Decimal : 0
  400. let isfString = preset.isf ? "ISF" : ""
  401. let crString = preset.cr ? "CR" : ""
  402. let dash = crString != "" ? "/" : ""
  403. let isfAndCRstring = isfString + dash + crString
  404. if name != "" {
  405. HStack {
  406. VStack {
  407. HStack {
  408. Text(name)
  409. Spacer()
  410. }
  411. HStack(spacing: 5) {
  412. Text(percent.formatted(.percent.grouping(.never).rounded().precision(.fractionLength(0))))
  413. if targetString != "" {
  414. Text(targetString)
  415. Text(targetString != "" ? state.units.rawValue : "")
  416. }
  417. if durationString != "" { Text(durationString + (perpetual ? "" : "min")) }
  418. if smbString != "" { Text(smbString).foregroundColor(.secondary).font(.caption) }
  419. if scheduledSMBstring != "" { Text(scheduledSMBstring) }
  420. if preset.advancedSettings {
  421. Text(maxMinutesSMB == 0 ? "" : maxMinutesSMB.formatted() + " SMB")
  422. Text(maxMinutesUAM == 0 ? "" : maxMinutesUAM.formatted() + " UAM")
  423. Text(isfAndCRstring)
  424. }
  425. Spacer()
  426. }
  427. .padding(.top, 2)
  428. .foregroundColor(.secondary)
  429. .font(.caption)
  430. }
  431. .contentShape(Rectangle())
  432. .onTapGesture {
  433. state.selectProfile(id_: preset.id ?? "")
  434. state.hideModal()
  435. }
  436. }
  437. }
  438. }
  439. private func unChanged() -> Bool {
  440. let defaultProfile = state.percentage == 100 && !state.override_target && !state.advancedSettings
  441. let noDurationSpecified = !state._indefinite && state.duration == 0
  442. let targetZeroWithOverride = state.override_target && state.target == 0
  443. let allSettingsDefault = state.percentage == 100 && !state.override_target && !state.smbIsOff && !state
  444. .smbIsScheduledOff && state.smbMinutes == state.defaultSmbMinutes && state.uamMinutes == state.defaultUamMinutes
  445. return defaultProfile || noDurationSpecified || targetZeroWithOverride || allSettingsDefault
  446. }
  447. private func removeProfile(at offsets: IndexSet) {
  448. for index in offsets {
  449. let language = fetchedProfiles[index]
  450. moc.delete(language)
  451. }
  452. do {
  453. try moc.save()
  454. } catch {
  455. // To do: add error
  456. }
  457. }
  458. }
  459. }