From 0f128da154def28d927421ace6fe05c48c64ed32 Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 17 Mar 2026 23:04:50 -0500 Subject: [PATCH] Allow PDF data export when AI is unavailable on Reports screen Users without Apple Intelligence can now export their mood data as a visual PDF with charts and statistics instead of seeing a disabled Generate button. The existing ExportService.exportPDF is reused for the non-AI path, gated behind the same privacy confirmation dialog. Co-Authored-By: Claude Opus 4.6 (1M context) --- Reflect/Localizable.xcstrings | 80 +++++++++++++++++++ Shared/AccessibilityIdentifiers.swift | 1 + Shared/Views/InsightsView/ReportsView.swift | 77 ++++++++++++------ .../Views/InsightsView/ReportsViewModel.swift | 20 +++++ 4 files changed, 154 insertions(+), 24 deletions(-) diff --git a/Reflect/Localizable.xcstrings b/Reflect/Localizable.xcstrings index 823a18d..82e136b 100644 --- a/Reflect/Localizable.xcstrings +++ b/Reflect/Localizable.xcstrings @@ -8115,6 +8115,86 @@ } } }, + "AI reports require Apple Intelligence. You can still export your mood entries as a visual PDF report with charts and statistics." : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "KI-Berichte erfordern Apple Intelligence. Du kannst deine Stimmungseinträge trotzdem als visuellen PDF-Bericht mit Diagrammen und Statistiken exportieren." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Los informes con IA requieren Apple Intelligence. Aún puedes exportar tus entradas de ánimo como un informe PDF visual con gráficos y estadísticas." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Les rapports IA nécessitent Apple Intelligence. Vous pouvez toujours exporter vos entrées d'humeur sous forme de rapport PDF visuel avec graphiques et statistiques." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "AIレポートにはApple Intelligenceが必要です。気分の記録をグラフや統計付きのPDFレポートとしてエクスポートできます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "AI 보고서에는 Apple Intelligence가 필요합니다. 차트와 통계가 포함된 시각적 PDF 보고서로 기분 기록을 내보낼 수 있습니다." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Relatórios com IA requerem Apple Intelligence. Você ainda pode exportar suas entradas de humor como um relatório PDF visual com gráficos e estatísticas." + } + } + } + }, + "Export Your Data" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deine Daten exportieren" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exporta tus datos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exporter vos données" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "データをエクスポート" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "데이터 내보내기" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Exporte seus dados" + } + } + } + }, "Export PDF" : { "comment" : "A button label that triggers the export of a user's mood report as a PDF file.", "isCommentAutoGenerated" : true, diff --git a/Shared/AccessibilityIdentifiers.swift b/Shared/AccessibilityIdentifiers.swift index 98186fa..b147351 100644 --- a/Shared/AccessibilityIdentifiers.swift +++ b/Shared/AccessibilityIdentifiers.swift @@ -179,6 +179,7 @@ enum AccessibilityID { static let exportButton = "reports_export_button" static let privacyConfirmation = "reports_privacy_confirmation" static let minimumEntriesWarning = "reports_minimum_entries_warning" + static let exportDataButton = "reports_export_data_button" } // MARK: - Common diff --git a/Shared/Views/InsightsView/ReportsView.swift b/Shared/Views/InsightsView/ReportsView.swift index c84e6a9..ee6edbb 100644 --- a/Shared/Views/InsightsView/ReportsView.swift +++ b/Shared/Views/InsightsView/ReportsView.swift @@ -22,8 +22,10 @@ struct ReportsView: View { ZStack { ScrollView { VStack(spacing: 20) { - // Report type selector - reportTypeSelector + if viewModel.isAIAvailable { + // Report type selector (AI only) + reportTypeSelector + } // Date range picker ReportDateRangePicker( @@ -35,22 +37,23 @@ struct ReportsView: View { // Entry count / validation entryValidationCard - // AI unavailable warning - if !viewModel.isAIAvailable { - aiUnavailableCard - } + if viewModel.isAIAvailable { + // Generate button + generateButton - // Generate button - generateButton + // Report ready card + if viewModel.generationState == .completed { + reportReadyCard + } - // Report ready card - if viewModel.generationState == .completed { - reportReadyCard - } - - // Error message - if case .failed(let message) = viewModel.generationState { - errorCard(message: message) + // Error message + if case .failed(let message) = viewModel.generationState { + errorCard(message: message) + } + } else { + // Non-AI export path + dataExportCard + exportDataButton } } .padding(.vertical) @@ -86,7 +89,11 @@ struct ReportsView: View { titleVisibility: .visible ) { Button(String(localized: "Share Report")) { - viewModel.exportPDF() + if viewModel.isAIAvailable { + viewModel.exportPDF() + } else { + viewModel.exportDataPDF() + } } Button(String(localized: "Cancel"), role: .cancel) {} } message: { @@ -184,19 +191,19 @@ struct ReportsView: View { .accessibilityIdentifier(AccessibilityID.Reports.minimumEntriesWarning) } - // MARK: - AI Unavailable Card + // MARK: - Data Export Card (No AI) - private var aiUnavailableCard: some View { + private var dataExportCard: some View { HStack(spacing: 12) { - Image(systemName: "brain.head.profile") - .foregroundColor(.orange) + Image(systemName: "chart.bar.doc.horizontal") + .foregroundColor(.accentColor) VStack(alignment: .leading, spacing: 2) { - Text("Apple Intelligence Required") + Text(String(localized: "Export Your Data")) .font(.subheadline.weight(.medium)) .foregroundColor(textColor) - Text("AI report generation requires Apple Intelligence to be enabled in Settings.") + 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) } @@ -206,11 +213,33 @@ struct ReportsView: View { .padding() .background( RoundedRectangle(cornerRadius: 14) - .fill(Color.orange.opacity(0.1)) + .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 { diff --git a/Shared/Views/InsightsView/ReportsViewModel.swift b/Shared/Views/InsightsView/ReportsViewModel.swift index f3b0ebc..da7dfc7 100644 --- a/Shared/Views/InsightsView/ReportsViewModel.swift +++ b/Shared/Views/InsightsView/ReportsViewModel.swift @@ -55,6 +55,10 @@ class ReportsViewModel: ObservableObject { validEntryCount >= 3 && isAIAvailable } + var canExportData: Bool { + validEntryCount >= 1 + } + var daySpan: Int { let components = Calendar.current.dateComponents([.day], from: startDate, to: endDate) return (components.day ?? 0) + 1 @@ -139,6 +143,22 @@ class ReportsViewModel: ObservableObject { // MARK: - PDF Export + func exportDataPDF() { + let entries = entriesInRange + guard !entries.isEmpty else { return } + + let url = ExportService.shared.exportPDF(entries: entries) + if let url { + exportedPDFURL = url + showShareSheet = true + + AnalyticsManager.shared.track(.reportExported( + type: "data_export", + entryCount: entries.count + )) + } + } + func exportPDF() { guard let report = generatedReport else { return }