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) <noreply@anthropic.com>
This commit is contained in:
@@ -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" : {
|
"Export PDF" : {
|
||||||
"comment" : "A button label that triggers the export of a user's mood report as a PDF file.",
|
"comment" : "A button label that triggers the export of a user's mood report as a PDF file.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ enum AccessibilityID {
|
|||||||
static let exportButton = "reports_export_button"
|
static let exportButton = "reports_export_button"
|
||||||
static let privacyConfirmation = "reports_privacy_confirmation"
|
static let privacyConfirmation = "reports_privacy_confirmation"
|
||||||
static let minimumEntriesWarning = "reports_minimum_entries_warning"
|
static let minimumEntriesWarning = "reports_minimum_entries_warning"
|
||||||
|
static let exportDataButton = "reports_export_data_button"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Common
|
// MARK: - Common
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ struct ReportsView: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// Report type selector
|
if viewModel.isAIAvailable {
|
||||||
reportTypeSelector
|
// Report type selector (AI only)
|
||||||
|
reportTypeSelector
|
||||||
|
}
|
||||||
|
|
||||||
// Date range picker
|
// Date range picker
|
||||||
ReportDateRangePicker(
|
ReportDateRangePicker(
|
||||||
@@ -35,22 +37,23 @@ struct ReportsView: View {
|
|||||||
// Entry count / validation
|
// Entry count / validation
|
||||||
entryValidationCard
|
entryValidationCard
|
||||||
|
|
||||||
// AI unavailable warning
|
if viewModel.isAIAvailable {
|
||||||
if !viewModel.isAIAvailable {
|
// Generate button
|
||||||
aiUnavailableCard
|
generateButton
|
||||||
}
|
|
||||||
|
|
||||||
// Generate button
|
// Report ready card
|
||||||
generateButton
|
if viewModel.generationState == .completed {
|
||||||
|
reportReadyCard
|
||||||
|
}
|
||||||
|
|
||||||
// Report ready card
|
// Error message
|
||||||
if viewModel.generationState == .completed {
|
if case .failed(let message) = viewModel.generationState {
|
||||||
reportReadyCard
|
errorCard(message: message)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Error message
|
// Non-AI export path
|
||||||
if case .failed(let message) = viewModel.generationState {
|
dataExportCard
|
||||||
errorCard(message: message)
|
exportDataButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
@@ -86,7 +89,11 @@ struct ReportsView: View {
|
|||||||
titleVisibility: .visible
|
titleVisibility: .visible
|
||||||
) {
|
) {
|
||||||
Button(String(localized: "Share Report")) {
|
Button(String(localized: "Share Report")) {
|
||||||
viewModel.exportPDF()
|
if viewModel.isAIAvailable {
|
||||||
|
viewModel.exportPDF()
|
||||||
|
} else {
|
||||||
|
viewModel.exportDataPDF()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Button(String(localized: "Cancel"), role: .cancel) {}
|
Button(String(localized: "Cancel"), role: .cancel) {}
|
||||||
} message: {
|
} message: {
|
||||||
@@ -184,19 +191,19 @@ struct ReportsView: View {
|
|||||||
.accessibilityIdentifier(AccessibilityID.Reports.minimumEntriesWarning)
|
.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) {
|
HStack(spacing: 12) {
|
||||||
Image(systemName: "brain.head.profile")
|
Image(systemName: "chart.bar.doc.horizontal")
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text("Apple Intelligence Required")
|
Text(String(localized: "Export Your Data"))
|
||||||
.font(.subheadline.weight(.medium))
|
.font(.subheadline.weight(.medium))
|
||||||
.foregroundColor(textColor)
|
.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)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
@@ -206,11 +213,33 @@ struct ReportsView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 14)
|
RoundedRectangle(cornerRadius: 14)
|
||||||
.fill(Color.orange.opacity(0.1))
|
.fill(colorScheme == .dark ? Color(.systemGray6) : .white)
|
||||||
)
|
)
|
||||||
.padding(.horizontal)
|
.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
|
// MARK: - Generate Button
|
||||||
|
|
||||||
private var generateButton: some View {
|
private var generateButton: some View {
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ class ReportsViewModel: ObservableObject {
|
|||||||
validEntryCount >= 3 && isAIAvailable
|
validEntryCount >= 3 && isAIAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var canExportData: Bool {
|
||||||
|
validEntryCount >= 1
|
||||||
|
}
|
||||||
|
|
||||||
var daySpan: Int {
|
var daySpan: Int {
|
||||||
let components = Calendar.current.dateComponents([.day], from: startDate, to: endDate)
|
let components = Calendar.current.dateComponents([.day], from: startDate, to: endDate)
|
||||||
return (components.day ?? 0) + 1
|
return (components.day ?? 0) + 1
|
||||||
@@ -139,6 +143,22 @@ class ReportsViewModel: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - PDF Export
|
// 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() {
|
func exportPDF() {
|
||||||
guard let report = generatedReport else { return }
|
guard let report = generatedReport else { return }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user