// // ReportsView.swift // Reflect // // AI-powered mood report generation with date range selection and PDF export. // import SwiftUI struct ReportsView: View { @StateObject private var viewModel = ReportsViewModel() @EnvironmentObject var iapManager: IAPManager @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @Environment(\.colorScheme) private var colorScheme @State private var showSubscriptionStore = false private var textColor: Color { theme.currentTheme.labelColor } var body: some View { ZStack { ScrollView { VStack(spacing: 20) { if viewModel.isAIAvailable { // Report type selector (AI only) reportTypeSelector } // Date range picker ReportDateRangePicker( startDate: $viewModel.startDate, endDate: $viewModel.endDate ) .padding(.horizontal) // Entry count / validation entryValidationCard if viewModel.isAIAvailable { // Generate button generateButton // Report ready card if viewModel.generationState == .completed { reportReadyCard } // Error message if case .failed(let message) = viewModel.generationState { errorCard(message: message) } } else { // Non-AI export path dataExportCard exportDataButton } } .padding(.vertical) .padding(.bottom, 100) } .disabled(iapManager.shouldShowPaywall) // Generating overlay if viewModel.generationState == .generating { ReportGeneratingView( progress: viewModel.progressValue, message: viewModel.progressMessage, onCancel: { viewModel.cancelGeneration() } ) } // Paywall overlay if iapManager.shouldShowPaywall { paywallOverlay } } .sheet(isPresented: $viewModel.showShareSheet) { if let url = viewModel.exportedPDFURL { ExportShareSheet(items: [url]) } } .sheet(isPresented: $showSubscriptionStore) { ReflectSubscriptionStoreView(source: "reports_gate") } .confirmationDialog( String(localized: "Privacy Notice"), isPresented: $viewModel.showPrivacyConfirmation, titleVisibility: .visible ) { Button(String(localized: "Share Report")) { if viewModel.isAIAvailable { viewModel.exportPDF() } else { viewModel.exportDataPDF() } } .accessibilityIdentifier(AccessibilityID.Reports.privacyShareButton) Button(String(localized: "Cancel"), role: .cancel) {} .accessibilityIdentifier(AccessibilityID.Reports.privacyCancelButton) } message: { Text("This report contains your personal mood data and journal notes. Only share it with people you trust.") } .onAppear { AnalyticsManager.shared.trackScreen(.reports) } } // MARK: - Report Type Selector private var reportTypeSelector: some View { VStack(alignment: .leading, spacing: 12) { Text("Report Type") .font(.headline) .foregroundColor(textColor) .padding(.horizontal) ForEach(ReportType.allCases, id: \.self) { type in Button { viewModel.reportType = type } label: { HStack(spacing: 16) { Image(systemName: type.icon) .font(.title2) .frame(width: 44, height: 44) .background(viewModel.reportType == type ? Color.accentColor.opacity(0.15) : Color(.systemGray5)) .foregroundColor(viewModel.reportType == type ? .accentColor : .gray) .clipShape(Circle()) VStack(alignment: .leading, spacing: 2) { Text(type.rawValue) .font(.headline) .foregroundColor(textColor) Text(type.description) .font(.subheadline) .foregroundStyle(.secondary) } Spacer() if viewModel.reportType == type { Image(systemName: "checkmark.circle.fill") .foregroundColor(.accentColor) } } .padding() .background( RoundedRectangle(cornerRadius: 14) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(viewModel.reportType == type ? Color.accentColor : Color.clear, lineWidth: 2) ) ) } .buttonStyle(.plain) .accessibilityIdentifier( type == .quickSummary ? AccessibilityID.Reports.quickSummaryButton : AccessibilityID.Reports.detailedReportButton ) .padding(.horizontal) } } } // MARK: - Entry Validation Card private var entryValidationCard: some View { HStack(spacing: 12) { Image(systemName: viewModel.validEntryCount >= 3 ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") .foregroundColor(viewModel.validEntryCount >= 3 ? .green : .orange) VStack(alignment: .leading, spacing: 2) { Text("\(viewModel.validEntryCount) mood entries in range") .font(.subheadline.weight(.medium)) .foregroundColor(textColor) if viewModel.validEntryCount < 3 { Text("At least 3 entries required to generate a report") .font(.caption) .foregroundStyle(.secondary) } } Spacer() } .padding() .background( RoundedRectangle(cornerRadius: 14) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) ) .padding(.horizontal) .accessibilityIdentifier(AccessibilityID.Reports.minimumEntriesWarning) } // MARK: - Data Export Card (No AI) private var dataExportCard: some View { HStack(spacing: 12) { Image(systemName: "chart.bar.doc.horizontal") .foregroundColor(.accentColor) VStack(alignment: .leading, spacing: 2) { Text(String(localized: "Export Your Data")) .font(.subheadline.weight(.medium)) .foregroundColor(textColor) Text(String(localized: "AI reports require Apple Intelligence. You can still export your mood entries as a visual PDF report with charts and statistics.")) .font(.caption) .foregroundStyle(.secondary) } Spacer() } .padding() .background( RoundedRectangle(cornerRadius: 14) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) ) .padding(.horizontal) } // MARK: - Export Data Button private var exportDataButton: some View { Button { viewModel.showPrivacyConfirmation = true } label: { HStack(spacing: 8) { Image(systemName: "square.and.arrow.up") Text("Export PDF") .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding() .background(viewModel.canExportData ? Color.accentColor : Color.gray) .foregroundColor(.white) .clipShape(RoundedRectangle(cornerRadius: 14)) } .disabled(!viewModel.canExportData) .padding(.horizontal) .accessibilityIdentifier(AccessibilityID.Reports.exportDataButton) } // MARK: - Generate Button private var generateButton: some View { Button { viewModel.generateReport() } label: { HStack(spacing: 8) { Image(systemName: "sparkles") Text("Generate Report") .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding() .background(viewModel.canGenerate ? Color.accentColor : Color.gray) .foregroundColor(.white) .clipShape(RoundedRectangle(cornerRadius: 14)) } .disabled(!viewModel.canGenerate) .padding(.horizontal) .accessibilityIdentifier(AccessibilityID.Reports.generateButton) } // MARK: - Report Ready Card private var reportReadyCard: some View { VStack(spacing: 16) { HStack(spacing: 12) { Image(systemName: "checkmark.circle.fill") .font(.title2) .foregroundColor(.green) VStack(alignment: .leading, spacing: 2) { Text("Report Ready") .font(.headline) .foregroundColor(textColor) Text("\(viewModel.reportType.rawValue) with \(viewModel.validEntryCount) entries") .font(.subheadline) .foregroundStyle(.secondary) } Spacer() } .padding() .background( RoundedRectangle(cornerRadius: 14) .fill(colorScheme == .dark ? Color(.systemGray6) : .white) ) .padding(.horizontal) Button { viewModel.showPrivacyConfirmation = true } label: { HStack(spacing: 8) { Image(systemName: "square.and.arrow.up") Text("Export PDF") .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding() .background(Color.accentColor) .foregroundColor(.white) .clipShape(RoundedRectangle(cornerRadius: 14)) } .padding(.horizontal) .accessibilityIdentifier(AccessibilityID.Reports.exportButton) } } // MARK: - Error Card private func errorCard(message: String) -> some View { HStack(spacing: 12) { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.red) VStack(alignment: .leading, spacing: 2) { Text("Generation Failed") .font(.subheadline.weight(.medium)) .foregroundColor(textColor) Text(message) .font(.caption) .foregroundStyle(.secondary) } Spacer() Button("Retry") { viewModel.generateReport() } .font(.subheadline.weight(.medium)) .accessibilityIdentifier(AccessibilityID.Reports.retryButton) } .padding() .background( RoundedRectangle(cornerRadius: 14) .fill(Color.red.opacity(0.1)) ) .padding(.horizontal) } // MARK: - Paywall Overlay private var paywallOverlay: some View { VStack(spacing: 24) { Spacer() ZStack { Circle() .fill( LinearGradient( colors: [.purple.opacity(0.2), .blue.opacity(0.2)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .frame(width: 100, height: 100) Image(systemName: "doc.text.magnifyingglass") .font(.largeTitle) .foregroundStyle( LinearGradient( colors: [.purple, .blue], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } VStack(spacing: 12) { Text("Unlock AI Reports") .font(.title2.weight(.bold)) .foregroundColor(textColor) .multilineTextAlignment(.center) Text("Generate clinical-quality mood reports to share with your therapist or track your progress over time.") .font(.body) .foregroundColor(textColor.opacity(0.7)) .multilineTextAlignment(.center) .padding(.horizontal, 32) } Button { showSubscriptionStore = true } label: { HStack { Image(systemName: "sparkles") Text("Unlock Reports") } .font(.headline.weight(.bold)) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding(.vertical, 16) .background( LinearGradient( colors: [.purple, .blue], startPoint: .leading, endPoint: .trailing ) ) .clipShape(RoundedRectangle(cornerRadius: 14)) } .padding(.horizontal, 24) .accessibilityIdentifier(AccessibilityID.Paywall.reportsUnlockButton) Spacer() } .background(theme.currentTheme.bg) } }