MealView.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. // LoopFollow
  2. // MealView.swift
  3. import HealthKit
  4. import LocalAuthentication
  5. import SwiftUI
  6. struct MealView: View {
  7. @Environment(\.presentationMode) private var presentationMode
  8. @State private var carbs = HKQuantity(unit: .gram(), doubleValue: 0.0)
  9. @State private var protein = HKQuantity(unit: .gram(), doubleValue: 0.0)
  10. @State private var fat = HKQuantity(unit: .gram(), doubleValue: 0.0)
  11. @State private var bolusAmount = HKQuantity(unit: .internationalUnit(), doubleValue: 0.0)
  12. private let pushNotificationManager = PushNotificationManager()
  13. @ObservedObject private var maxCarbs = Storage.shared.maxCarbs
  14. @ObservedObject private var maxProtein = Storage.shared.maxProtein
  15. @ObservedObject private var maxFat = Storage.shared.maxFat
  16. @ObservedObject private var mealWithBolus = Storage.shared.mealWithBolus
  17. @ObservedObject private var mealWithFatProtein = Storage.shared.mealWithFatProtein
  18. @ObservedObject private var maxBolus = Storage.shared.maxBolus
  19. @FocusState private var carbsFieldIsFocused: Bool
  20. @FocusState private var proteinFieldIsFocused: Bool
  21. @FocusState private var fatFieldIsFocused: Bool
  22. @FocusState private var bolusFieldIsFocused: Bool
  23. @State private var showAlert: Bool = false
  24. @State private var alertType: AlertType? = nil
  25. @State private var alertMessage: String? = nil
  26. @State private var isLoading: Bool = false
  27. @State private var statusMessage: String? = nil
  28. @State private var selectedTime: Date? = nil
  29. @State private var isScheduling: Bool = false
  30. enum AlertType {
  31. case confirmMeal
  32. case statusSuccess
  33. case statusFailure
  34. case validationError
  35. }
  36. var body: some View {
  37. NavigationView {
  38. VStack {
  39. Form {
  40. Section(header: Text("Meal Data")) {
  41. HKQuantityInputView(
  42. label: "Carbs",
  43. quantity: $carbs,
  44. unit: .gram(),
  45. maxLength: 4,
  46. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  47. maxValue: maxCarbs.value,
  48. isFocused: $carbsFieldIsFocused,
  49. onValidationError: { message in
  50. handleValidationError(message)
  51. }
  52. )
  53. if mealWithFatProtein.value {
  54. HKQuantityInputView(
  55. label: "Protein",
  56. quantity: $protein,
  57. unit: .gram(),
  58. maxLength: 4,
  59. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  60. maxValue: maxProtein.value,
  61. isFocused: $proteinFieldIsFocused,
  62. onValidationError: { message in
  63. handleValidationError(message)
  64. }
  65. )
  66. HKQuantityInputView(
  67. label: "Fat",
  68. quantity: $fat,
  69. unit: .gram(),
  70. maxLength: 4,
  71. minValue: HKQuantity(unit: .gram(), doubleValue: 0),
  72. maxValue: maxFat.value,
  73. isFocused: $fatFieldIsFocused,
  74. onValidationError: { message in
  75. handleValidationError(message)
  76. }
  77. )
  78. }
  79. if mealWithBolus.value {
  80. HKQuantityInputView(
  81. label: "Bolus Amount",
  82. quantity: $bolusAmount,
  83. unit: .internationalUnit(),
  84. maxLength: 4,
  85. minValue: HKQuantity(unit: .internationalUnit(), doubleValue: 0),
  86. maxValue: maxBolus.value,
  87. isFocused: $bolusFieldIsFocused,
  88. onValidationError: { message in
  89. handleValidationError(message)
  90. }
  91. )
  92. }
  93. }
  94. Section(header: Text("Schedule")) {
  95. Toggle("Schedule for later", isOn: $isScheduling)
  96. if isScheduling {
  97. DatePicker(
  98. "Select Time",
  99. selection: Binding(
  100. get: { self.selectedTime ?? Date() },
  101. set: { self.selectedTime = $0 }
  102. ),
  103. displayedComponents: .hourAndMinute
  104. )
  105. .datePickerStyle(CompactDatePickerStyle())
  106. if bolusAmount.doubleValue(for: .internationalUnit()) > 0 {
  107. Text("Note: The meal will be scheduled, but the bolus is enacted immediately.")
  108. }
  109. }
  110. }
  111. LoadingButtonView(
  112. buttonText: "Send Meal",
  113. progressText: "Sending Meal Data...",
  114. isLoading: isLoading,
  115. action: {
  116. carbsFieldIsFocused = false
  117. proteinFieldIsFocused = false
  118. fatFieldIsFocused = false
  119. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  120. guard carbs.doubleValue(for: .gram()) != 0 ||
  121. protein.doubleValue(for: .gram()) != 0 ||
  122. fat.doubleValue(for: .gram()) != 0
  123. else {
  124. return
  125. }
  126. if !showAlert {
  127. alertType = .confirmMeal
  128. showAlert = true
  129. }
  130. }
  131. },
  132. isDisabled: isButtonDisabled
  133. )
  134. }
  135. .navigationTitle("Meal")
  136. .navigationBarTitleDisplayMode(.inline)
  137. }
  138. .onAppear {
  139. selectedTime = nil
  140. isScheduling = false
  141. }
  142. .alert(isPresented: $showAlert) {
  143. switch alertType {
  144. case .confirmMeal:
  145. let carbsAmount = carbs.doubleValue(for: HKUnit.gram())
  146. let proteinAmount = protein.doubleValue(for: HKUnit.gram())
  147. let fatAmount = fat.doubleValue(for: HKUnit.gram())
  148. let bolusAmount = bolusAmount.doubleValue(for: .internationalUnit())
  149. var message = "Are you sure you want to send the meal data"
  150. if let selectedTime = selectedTime {
  151. let timeFormatter = DateFormatter()
  152. timeFormatter.timeStyle = .short
  153. let timeString = timeFormatter.string(from: selectedTime)
  154. message += " for \(timeString)?"
  155. } else {
  156. message += " now?"
  157. }
  158. if carbsAmount > 0 {
  159. message += String(format: "\nCarbs: %.0f g", carbsAmount)
  160. }
  161. if proteinAmount > 0 {
  162. message += String(format: "\nProtein: %.0f g", proteinAmount)
  163. }
  164. if fatAmount > 0 {
  165. message += String(format: "\nFat: %.0f g", fatAmount)
  166. }
  167. if bolusAmount > 0 {
  168. message += String(format: "\nBolus: %.2f U", bolusAmount)
  169. }
  170. return Alert(
  171. title: Text("Confirm Meal"),
  172. message: Text(message),
  173. primaryButton: .default(Text("Confirm"), action: {
  174. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  175. if bolusAmount > 0 {
  176. AuthService.authenticate(reason: "Confirm your identity to send bolus.") { result in
  177. DispatchQueue.main.async {
  178. switch result {
  179. case .success:
  180. self.sendMealCommand()
  181. case let .unavailable(message):
  182. self.alertMessage = message
  183. self.alertType = .validationError
  184. self.showAlert = true
  185. case .failed:
  186. self.alertMessage = "Authentication failed"
  187. self.alertType = .validationError
  188. self.showAlert = true
  189. case .canceled:
  190. // User canceled, no alert
  191. break
  192. }
  193. }
  194. }
  195. } else {
  196. self.sendMealCommand()
  197. }
  198. }
  199. }),
  200. secondaryButton: .cancel()
  201. )
  202. case .statusSuccess:
  203. return Alert(
  204. title: Text("Status"),
  205. message: Text(statusMessage ?? ""),
  206. dismissButton: .default(Text("OK"), action: {
  207. presentationMode.wrappedValue.dismiss()
  208. })
  209. )
  210. case .statusFailure:
  211. return Alert(
  212. title: Text("Status"),
  213. message: Text(statusMessage ?? ""),
  214. dismissButton: .default(Text("OK"))
  215. )
  216. case .validationError:
  217. return Alert(
  218. title: Text("Validation Error"),
  219. message: Text(alertMessage ?? ""),
  220. dismissButton: .default(Text("OK"))
  221. )
  222. case .none:
  223. return Alert(title: Text("Unknown Alert"))
  224. }
  225. }
  226. }
  227. }
  228. private var isButtonDisabled: Bool {
  229. return isLoading
  230. }
  231. private func sendMealCommand() {
  232. isLoading = true
  233. var scheduledDate: Date? = nil
  234. if isScheduling, let selectedTime = selectedTime {
  235. let calendar = Calendar.current
  236. let now = Date()
  237. let selectedDateComponents = calendar.dateComponents([.hour, .minute], from: selectedTime)
  238. let currentSecond = calendar.component(.second, from: now)
  239. scheduledDate = calendar.date(bySettingHour: selectedDateComponents.hour ?? 0,
  240. minute: selectedDateComponents.minute ?? 0,
  241. second: currentSecond,
  242. of: now) ?? now
  243. }
  244. pushNotificationManager.sendMealPushNotification(
  245. carbs: carbs,
  246. protein: protein,
  247. fat: fat,
  248. bolusAmount: bolusAmount,
  249. scheduledTime: scheduledDate
  250. ) { success, errorMessage in
  251. DispatchQueue.main.async {
  252. isLoading = false
  253. if success {
  254. statusMessage = "Meal command sent successfully."
  255. LogManager.shared.log(
  256. category: .apns,
  257. message: "sendMealPushNotification succeeded - Carbs: \(carbs.doubleValue(for: .gram())) g, Protein: \(protein.doubleValue(for: .gram())) g, Fat: \(fat.doubleValue(for: .gram())) g, Bolus: \(bolusAmount.doubleValue(for: .internationalUnit())) U, Scheduled: \(scheduledDate != nil ? formatDate(scheduledDate!) : "now")"
  258. )
  259. // Reset meal values and scheduled data after success
  260. carbs = HKQuantity(unit: .gram(), doubleValue: 0.0)
  261. protein = HKQuantity(unit: .gram(), doubleValue: 0.0)
  262. fat = HKQuantity(unit: .gram(), doubleValue: 0.0)
  263. selectedTime = nil
  264. isScheduling = false
  265. alertType = .statusSuccess
  266. } else {
  267. statusMessage = errorMessage ?? "Failed to send meal command."
  268. LogManager.shared.log(
  269. category: .apns,
  270. message: "sendMealPushNotification failed with error: \(errorMessage ?? "unknown error")"
  271. )
  272. alertType = .statusFailure
  273. }
  274. showAlert = true
  275. }
  276. }
  277. }
  278. private func formatDate(_ date: Date) -> String {
  279. let formatter = DateFormatter()
  280. formatter.timeStyle = .short
  281. return formatter.string(from: date)
  282. }
  283. private func handleValidationError(_ message: String) {
  284. alertMessage = message
  285. alertType = .validationError
  286. showAlert = true
  287. }
  288. }