DateRangePicker.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. // LoopFollow
  2. // DateRangePicker.swift
  3. import SwiftUI
  4. struct DateRangePicker: View {
  5. @Binding var startDate: Date
  6. @Binding var endDate: Date
  7. var availability: DataAvailabilityInfo?
  8. var onDateChange: () -> Void
  9. @State private var isExpanded = false
  10. @State private var showStartDatePicker = false
  11. @State private var showEndDatePicker = false
  12. private var dateFormatter: DateFormatter {
  13. let formatter = DateFormatter()
  14. formatter.dateStyle = .medium
  15. formatter.timeStyle = .none
  16. dateTimeUtils.applyDisplayTimeZone(to: formatter)
  17. return formatter
  18. }
  19. private var compactDateFormatter: DateFormatter {
  20. let formatter = DateFormatter()
  21. formatter.dateStyle = .short
  22. formatter.timeStyle = .none
  23. dateTimeUtils.applyDisplayTimeZone(to: formatter)
  24. return formatter
  25. }
  26. private var dayCount: Int {
  27. let calendar = dateTimeUtils.displayCalendar()
  28. let startDay = calendar.startOfDay(for: startDate)
  29. let endDay = calendar.startOfDay(for: endDate)
  30. return (calendar.dateComponents([.day], from: startDay, to: endDay).day ?? 0) + 1
  31. }
  32. private var lastFullDay: Date {
  33. StatsDateRange.lastComplete(days: 1).end
  34. }
  35. var body: some View {
  36. VStack(spacing: 0) {
  37. Button(action: {
  38. withAnimation {
  39. isExpanded.toggle()
  40. showStartDatePicker = false
  41. showEndDatePicker = false
  42. }
  43. }) {
  44. HStack {
  45. Image(systemName: "calendar")
  46. .foregroundColor(.blue)
  47. VStack(alignment: .leading, spacing: 2) {
  48. Text("\(compactDateFormatter.string(from: startDate)) - \(compactDateFormatter.string(from: endDate))")
  49. .font(.subheadline)
  50. .foregroundColor(.primary)
  51. Text("(\(dayCount) days)")
  52. .font(.caption2)
  53. .foregroundColor(.secondary)
  54. }
  55. Spacer()
  56. if let availability = availability {
  57. HStack(spacing: 4) {
  58. Image(systemName: statusIcon(for: availability.dataQuality))
  59. .font(.caption)
  60. .foregroundColor(statusColor(for: availability.dataQuality))
  61. Text(String(format: "%.0f%%", availability.coveragePercentage))
  62. .font(.caption)
  63. .fontWeight(.medium)
  64. .foregroundColor(statusColor(for: availability.dataQuality))
  65. }
  66. }
  67. Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
  68. .font(.caption)
  69. .foregroundColor(.secondary)
  70. }
  71. .padding(.horizontal, 16)
  72. .padding(.vertical, 12)
  73. }
  74. .background(Color.secondary.opacity(0.1))
  75. if isExpanded {
  76. VStack(spacing: 12) {
  77. HStack(spacing: 16) {
  78. VStack(alignment: .leading, spacing: 4) {
  79. Text("Start Date")
  80. .font(.caption)
  81. .foregroundColor(.secondary)
  82. Button(action: {
  83. showStartDatePicker.toggle()
  84. showEndDatePicker = false
  85. }) {
  86. HStack {
  87. Image(systemName: "calendar")
  88. .font(.caption)
  89. .foregroundColor(.blue)
  90. Text(dateFormatter.string(from: startDate))
  91. .font(.subheadline)
  92. .foregroundColor(.primary)
  93. }
  94. .padding(.horizontal, 10)
  95. .padding(.vertical, 6)
  96. .background(Color.secondary.opacity(0.15))
  97. .cornerRadius(6)
  98. }
  99. }
  100. Spacer()
  101. VStack(alignment: .leading, spacing: 4) {
  102. Text("End Date")
  103. .font(.caption)
  104. .foregroundColor(.secondary)
  105. Button(action: {
  106. showEndDatePicker.toggle()
  107. showStartDatePicker = false
  108. }) {
  109. HStack {
  110. Image(systemName: "calendar")
  111. .font(.caption)
  112. .foregroundColor(.blue)
  113. Text(dateFormatter.string(from: endDate))
  114. .font(.subheadline)
  115. .foregroundColor(.primary)
  116. }
  117. .padding(.horizontal, 10)
  118. .padding(.vertical, 6)
  119. .background(Color.secondary.opacity(0.15))
  120. .cornerRadius(6)
  121. }
  122. }
  123. }
  124. .padding(.top, 8)
  125. if showStartDatePicker {
  126. DatePicker(
  127. String(localized: "Select Start Date"),
  128. selection: $startDate,
  129. in: ...endDate,
  130. displayedComponents: [.date]
  131. )
  132. .datePickerStyle(.graphical)
  133. .onChange(of: startDate) { _ in
  134. startDate = dateTimeUtils.displayCalendar().startOfDay(for: startDate)
  135. showStartDatePicker = false
  136. onDateChange()
  137. }
  138. }
  139. if showEndDatePicker {
  140. DatePicker(
  141. String(localized: "Select End Date"),
  142. selection: $endDate,
  143. in: startDate ... lastFullDay,
  144. displayedComponents: [.date]
  145. )
  146. .datePickerStyle(.graphical)
  147. .onChange(of: endDate) { _ in
  148. let calendar = dateTimeUtils.displayCalendar()
  149. let startOfNextDay = calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: endDate)) ?? endDate
  150. endDate = calendar.date(byAdding: .second, value: -1, to: startOfNextDay) ?? endDate
  151. showEndDatePicker = false
  152. onDateChange()
  153. }
  154. }
  155. Divider()
  156. HStack(spacing: 8) {
  157. QuickSelectButton(title: "7d") {
  158. setDateRange(days: 7)
  159. }
  160. QuickSelectButton(title: "14d") {
  161. setDateRange(days: 14)
  162. }
  163. QuickSelectButton(title: "30d") {
  164. setDateRange(days: 30)
  165. }
  166. QuickSelectButton(title: "90d") {
  167. setDateRange(days: 90)
  168. }
  169. }
  170. if let availability = availability {
  171. VStack(spacing: 6) {
  172. HStack {
  173. Text("Data Availability")
  174. .font(.subheadline)
  175. .fontWeight(.medium)
  176. Spacer()
  177. Text(String(format: "%.1f%%", availability.coveragePercentage))
  178. .font(.subheadline)
  179. .fontWeight(.semibold)
  180. .foregroundColor(statusColor(for: availability.dataQuality))
  181. }
  182. HStack {
  183. Text("\(availability.actualReadings) of \(availability.totalExpectedReadings) expected readings")
  184. .font(.caption)
  185. .foregroundColor(.secondary)
  186. Spacer()
  187. if availability.missingIntervals > 0 {
  188. Text("\(availability.missingIntervals) gaps")
  189. .font(.caption)
  190. .foregroundColor(.orange)
  191. }
  192. }
  193. Text("Expected: 1 CGM reading every 5 minutes")
  194. .font(.caption2)
  195. .foregroundColor(.secondary)
  196. .frame(maxWidth: .infinity, alignment: .leading)
  197. }
  198. .padding(12)
  199. }
  200. }
  201. .padding(.horizontal, 16)
  202. .padding(.bottom, 12)
  203. .background(Color.secondary.opacity(0.05))
  204. }
  205. }
  206. .background(Color.secondary.opacity(0.05))
  207. .cornerRadius(12)
  208. }
  209. private func setDateRange(days: Int) {
  210. let range = StatsDateRange.lastComplete(days: days)
  211. startDate = range.start
  212. endDate = range.end
  213. showStartDatePicker = false
  214. showEndDatePicker = false
  215. onDateChange()
  216. }
  217. private func statusIcon(for quality: DataAvailabilityInfo.DataQuality) -> String {
  218. switch quality {
  219. case .excellent:
  220. return "checkmark.circle.fill"
  221. case .good:
  222. return "checkmark.circle"
  223. case .fair:
  224. return "exclamationmark.triangle"
  225. case .poor:
  226. return "xmark.circle"
  227. }
  228. }
  229. private func statusColor(for quality: DataAvailabilityInfo.DataQuality) -> Color {
  230. switch quality {
  231. case .excellent:
  232. return .green
  233. case .good:
  234. return .blue
  235. case .fair:
  236. return .orange
  237. case .poor:
  238. return .red
  239. }
  240. }
  241. }
  242. struct QuickSelectButton: View {
  243. let title: String
  244. let action: () -> Void
  245. var body: some View {
  246. Button(action: action) {
  247. Text(title)
  248. .font(.caption)
  249. .fontWeight(.medium)
  250. .padding(.horizontal, 12)
  251. .padding(.vertical, 6)
  252. .background(Color.blue.opacity(0.1))
  253. .foregroundColor(.blue)
  254. .cornerRadius(6)
  255. }
  256. }
  257. }