// // ExportView.swift // Reflect // // Export mood data to CSV or PDF formats. // import SwiftUI struct ExportView: View { @Environment(\.dismiss) private var dismiss @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system private var textColor: Color { theme.currentTheme.labelColor } @State private var selectedFormat: ExportFormat = .csv @State private var selectedRange: DateRange = .allTime @State private var isExporting = false @State private var exportedURL: URL? @State private var showShareSheet = false @State private var showError = false @State private var errorMessage = "" private let entries: [MoodEntryModel] enum ExportFormat: String, CaseIterable { case csv = "CSV" case pdf = "PDF" var icon: String { switch self { case .csv: return "tablecells" case .pdf: return "doc.richtext" } } var description: String { switch self { case .csv: return "Spreadsheet format for data analysis" case .pdf: return "Formatted report with insights" } } } enum DateRange: String, CaseIterable { case lastWeek = "Last 7 Days" case lastMonth = "Last 30 Days" case last3Months = "Last 3 Months" case lastYear = "Last Year" case allTime = "All Time" var days: Int? { switch self { case .lastWeek: return 7 case .lastMonth: return 30 case .last3Months: return 90 case .lastYear: return 365 case .allTime: return nil } } } init(entries: [MoodEntryModel]) { self.entries = entries } private var filteredEntries: [MoodEntryModel] { guard let days = selectedRange.days else { return entries } let cutoffDate = Calendar.current.date(byAdding: .day, value: -days, to: Date()) ?? Date() return entries.filter { $0.forDate >= cutoffDate } } private var validEntries: [MoodEntryModel] { filteredEntries.filter { ![.missing, .placeholder].contains($0.mood) } } var body: some View { NavigationStack { ScrollView { VStack(spacing: 24) { // Preview stats statsCard // Format selection formatSelection // Date range selection dateRangeSelection // Export button exportButton } .padding() } .background(Color(.systemGroupedBackground)) .navigationTitle("Export Data") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } .accessibilityIdentifier(AccessibilityID.Export.cancelButton) } } .sheet(isPresented: $showShareSheet) { if let url = exportedURL { ExportShareSheet(items: [url]) } } .alert("Export Failed", isPresented: $showError) { Button("OK", role: .cancel) { } .accessibilityIdentifier(AccessibilityID.Export.alertOKButton) } message: { Text(errorMessage) } } } private var statsCard: some View { VStack(spacing: 16) { HStack { Image(systemName: "chart.bar.doc.horizontal") .font(.title2) .foregroundColor(.accentColor) Text("Export Preview") .font(.headline) .foregroundColor(textColor) Spacer() } Divider() HStack(spacing: 32) { VStack(spacing: 4) { Text("\(validEntries.count)") .font(.title) .fontWeight(.bold) .foregroundColor(textColor) Text("Entries") .font(.caption) .foregroundStyle(.secondary) } VStack(spacing: 4) { Text(dateRangeText) .font(.title3) .fontWeight(.semibold) .foregroundColor(textColor) Text("Date Range") .font(.caption) .foregroundStyle(.secondary) } Spacer() } } .padding() .background( RoundedRectangle(cornerRadius: 16) .fill(Color(.systemBackground)) ) } private var dateRangeText: String { guard !validEntries.isEmpty else { return "No data" } let sorted = validEntries.sorted { $0.forDate < $1.forDate } guard let first = sorted.first, let last = sorted.last else { return "No data" } let formatter = DateFormatter() formatter.dateFormat = "MMM d" if Calendar.current.isDate(first.forDate, equalTo: last.forDate, toGranularity: .day) { return formatter.string(from: first.forDate) } return "\(formatter.string(from: first.forDate)) - \(formatter.string(from: last.forDate))" } private var formatSelection: some View { VStack(alignment: .leading, spacing: 12) { Text("Format") .font(.headline) .foregroundColor(textColor) ForEach(ExportFormat.allCases, id: \.self) { format in Button { selectedFormat = format } label: { HStack(spacing: 16) { Image(systemName: format.icon) .font(.title2) .frame(width: 44, height: 44) .background(selectedFormat == format ? Color.accentColor.opacity(0.15) : Color(.systemGray5)) .foregroundColor(selectedFormat == format ? .accentColor : .gray) .clipShape(Circle()) VStack(alignment: .leading, spacing: 2) { Text(format.rawValue) .font(.headline) .foregroundColor(textColor) Text(format.description) .font(.subheadline) .foregroundStyle(.secondary) } Spacer() if selectedFormat == format { Image(systemName: "checkmark.circle.fill") .foregroundColor(.accentColor) } } .padding() .background( RoundedRectangle(cornerRadius: 14) .fill(Color(.systemBackground)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(selectedFormat == format ? Color.accentColor : Color.clear, lineWidth: 2) ) ) } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Export.formatButton(format.rawValue)) } } } private var dateRangeSelection: some View { VStack(alignment: .leading, spacing: 12) { Text("Date Range") .font(.headline) .foregroundColor(textColor) VStack(spacing: 0) { ForEach(DateRange.allCases, id: \.self) { range in Button { selectedRange = range } label: { HStack { Text(range.rawValue) .foregroundColor(textColor) Spacer() if selectedRange == range { Image(systemName: "checkmark") .foregroundColor(.accentColor) } } .padding() .background(Color(.systemBackground)) } .buttonStyle(.plain) .accessibilityIdentifier(AccessibilityID.Export.rangeButton(range.rawValue)) if range != DateRange.allCases.last { Divider() .padding(.leading) } } } .clipShape(RoundedRectangle(cornerRadius: 14)) } } private var exportButton: some View { Button { performExport() } label: { HStack(spacing: 8) { if isExporting { ProgressView() .tint(.white) } else { Image(systemName: "square.and.arrow.up") } Text(isExporting ? "Exporting..." : "Export \(selectedFormat.rawValue)") .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding() .background(validEntries.isEmpty ? Color.gray : Color.accentColor) .foregroundColor(.white) .clipShape(RoundedRectangle(cornerRadius: 14)) } .disabled(isExporting || validEntries.isEmpty) .accessibilityIdentifier(AccessibilityID.Export.exportButton) .padding(.top, 8) } private func performExport() { isExporting = true Task { let url: URL? switch selectedFormat { case .csv: url = ExportService.shared.exportCSV(entries: validEntries) case .pdf: url = ExportService.shared.exportPDF(entries: validEntries, title: "Reflect Mood Report") } await MainActor.run { isExporting = false if let url = url { exportedURL = url showShareSheet = true } else { errorMessage = "Failed to create export file. Please try again." showError = true } } } } } // MARK: - Export Share Sheet struct ExportShareSheet: UIViewControllerRepresentable { let items: [Any] func makeUIViewController(context: Context) -> UIActivityViewController { UIActivityViewController(activityItems: items, applicationActivities: nil) } func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} }