AdjustmentsRootView+Overrides.swift 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import CoreData
  2. import SwiftUI
  3. extension Adjustments.RootView {
  4. @ViewBuilder func overrides() -> some View {
  5. if state.isEnabled, state.activeOverrideName.isNotEmpty {
  6. currentActiveAdjustment
  7. }
  8. if state.overridePresets.isNotEmpty {
  9. overridePresets
  10. } else {
  11. defaultText
  12. }
  13. }
  14. var overridePresets: some View {
  15. Section {
  16. ForEach(state.overridePresets) { preset in
  17. overridesView(for: preset, showCheckMark: showOverrideCheckmark) {
  18. enactOverridePreset(preset)
  19. }
  20. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  21. swipeActionsForOverrides(for: preset)
  22. }
  23. }
  24. .onMove(perform: state.reorderOverride)
  25. .confirmationDialog(
  26. "Delete the Override Preset \"\(selectedOverride?.name ?? "")\"?",
  27. isPresented: $isConfirmDeletePresented,
  28. titleVisibility: .visible
  29. ) {
  30. if let itemToDelete = selectedOverride {
  31. Button(
  32. state.currentActiveOverride == selectedOverride ? "Stop and Delete" : "Delete",
  33. role: .destructive
  34. ) {
  35. if state.currentActiveOverride == selectedOverride {
  36. Task {
  37. // Save cancelled Override in OverrideRunStored Entity
  38. // Cancel ALL active Override
  39. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  40. }
  41. }
  42. // Perform the delete action
  43. Task {
  44. await state.invokeOverridePresetDeletion(itemToDelete.objectID)
  45. }
  46. // Reset the selected item after deletion
  47. selectedOverride = nil
  48. }
  49. }
  50. Button("Cancel", role: .cancel) {
  51. // Dismiss the dialog without action
  52. selectedOverride = nil
  53. }
  54. } message: {
  55. if state.currentActiveOverride == selectedOverride {
  56. Text(
  57. state
  58. .currentActiveOverride == selectedOverride ?
  59. "This override preset is currently running. Deleting will stop it." : ""
  60. )
  61. }
  62. }
  63. .listRowBackground(Color.chart)
  64. } header: {
  65. Text("Override Presets")
  66. } footer: {
  67. HStack {
  68. Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
  69. Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
  70. }
  71. }
  72. }
  73. func enactOverridePreset(_ preset: OverrideStored) {
  74. Task {
  75. let objectID = preset.objectID
  76. await state.enactOverridePreset(withID: objectID)
  77. state.hideModal()
  78. selectedOverridePresetID = preset.id
  79. showOverrideCheckmark = true
  80. // Deactivate checkmark after 3 seconds
  81. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  82. showOverrideCheckmark = false
  83. }
  84. }
  85. }
  86. func swipeActionsForOverrides(for preset: OverrideStored) -> some View {
  87. Group {
  88. Button(role: .none) {
  89. selectedOverride = preset
  90. isConfirmDeletePresented = true
  91. } label: {
  92. Label("Delete", systemImage: "trash")
  93. .tint(.red)
  94. }
  95. Button(action: {
  96. // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
  97. selectedOverride = preset
  98. state.showOverrideEditSheet = true
  99. }, label: {
  100. Label("Edit", systemImage: "pencil")
  101. .tint(.blue)
  102. })
  103. }
  104. }
  105. var overrideLabelDivider: some View {
  106. Divider()
  107. .frame(width: 1, height: 20)
  108. }
  109. @ViewBuilder func overridesView(
  110. for preset: OverrideStored,
  111. showCheckMark _: Bool = false,
  112. onTap: (() -> Void)? = nil
  113. ) -> some View {
  114. let isSelected = preset.id == selectedOverridePresetID
  115. let name = preset.name ?? ""
  116. let indefinite = preset.indefinite
  117. let duration = preset.duration?.decimalValue ?? Decimal(0)
  118. let percentage = preset.percentage
  119. let smbMinutes = preset.smbMinutes?.decimalValue ?? Decimal(0)
  120. let uamMinutes = preset.uamMinutes?.decimalValue ?? Decimal(0)
  121. let target: String = {
  122. guard let targetValue = preset.target, targetValue != 0 else { return "" }
  123. return state.units == .mgdL ? targetValue.description : targetValue.decimalValue.formattedAsMmolL
  124. }()
  125. let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
  126. let durationString = indefinite ? "" : "\(state.formatHrMin(Int(duration)))"
  127. let scheduledSMBString: String = {
  128. guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
  129. return " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
  130. }()
  131. let smbString: String = {
  132. guard preset.smbIsOff || preset.smbIsScheduledOff else { return "" }
  133. return "SMBs Off\(scheduledSMBString)"
  134. }()
  135. let maxSmbMinsString: String = {
  136. guard smbMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
  137. smbMinutes != state.defaultSmbMinutes else { return "" }
  138. return "\(smbMinutes.formatted()) min SMB"
  139. }()
  140. let maxUamMinsString: String = {
  141. guard uamMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
  142. uamMinutes != state.defaultUamMinutes else { return "" }
  143. return "\(uamMinutes.formatted()) min UAM"
  144. }()
  145. let isfAndCrString: String = {
  146. switch (preset.isfAndCr, preset.isf, preset.cr) {
  147. case (_, true, true),
  148. (true, _, _):
  149. return " ISF/CR"
  150. case (false, true, false):
  151. return " ISF"
  152. case (false, false, true):
  153. return " CR"
  154. default:
  155. return ""
  156. }
  157. }()
  158. let percentageString = percentage != 100 ? "\(Int(percentage))%\(isfAndCrString)" : ""
  159. // Combine all labels into a single array, filtering out empty strings
  160. let labels: [String] = [
  161. durationString,
  162. percentageString,
  163. targetString,
  164. smbString,
  165. maxSmbMinsString,
  166. maxUamMinsString
  167. ].filter { !$0.isEmpty }
  168. if !name.isEmpty {
  169. ZStack(alignment: .trailing) {
  170. HStack {
  171. VStack {
  172. HStack {
  173. Text(name)
  174. Spacer()
  175. }
  176. HStack(spacing: 5) {
  177. ForEach(labels, id: \.self) { label in
  178. Text(label)
  179. if label != labels.last { // Add divider between labels
  180. overrideLabelDivider
  181. }
  182. }
  183. Spacer()
  184. }
  185. .padding(.top, 2)
  186. .foregroundColor(.secondary)
  187. .font(.caption)
  188. }
  189. .contentShape(Rectangle())
  190. .onTapGesture {
  191. onTap?()
  192. }
  193. }
  194. // show checkmark to indicate if the preset was actually pressed
  195. if showOverrideCheckmark && isSelected {
  196. Image(systemName: "checkmark.circle.fill")
  197. .imageScale(.large)
  198. .fontWeight(.bold)
  199. .foregroundStyle(Color.green)
  200. } else {
  201. Image(systemName: "line.3.horizontal")
  202. .imageScale(.medium)
  203. .foregroundStyle(.secondary)
  204. }
  205. }
  206. }
  207. }
  208. }