1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
import SwiftUI
struct ScheduledReportsView: View {
@Environment(\.appDensity) private var appDensity
@State private var settings = ScheduledReportStorage.loadSettings()
@State private var logs = ScheduledReportStorage.loadLogs()
@State private var isGenerating = false
@State private var statusMessage: String?
var body: some View {
List {
Section("Overview") {
if !FeatureAccessService.hasAccess(to: .automatedMonitoring) {
Text("Scheduled reports require Pro.")
.font(appDensity.font(.caption))
.foregroundStyle(Color(.appTextSecondary))
}
Toggle("Scheduled Reports", isOn: Binding(
get: { settings.isEnabled },
set: { newValue in
settings.isEnabled = newValue
saveSettings()
}
))
.disabled(!FeatureAccessService.hasAccess(to: .automatedMonitoring))
Picker("Cadence", selection: Binding(
get: { settings.cadence },
set: { newValue in
settings.cadence = newValue
saveSettings()
}
)) {
ForEach(ScheduledReportCadence.allCases) { cadence in
Text(cadence.title).tag(cadence)
}
}
.disabled(!FeatureAccessService.hasAccess(to: .automatedMonitoring))
Picker("Format", selection: Binding(
get: { settings.format },
set: { newValue in
settings.format = newValue
saveSettings()
}
)) {
ForEach([DomainExportFormat.markdown, .pdf, .json]) { format in
Text(format.title).tag(format)
}
}
.disabled(!FeatureAccessService.hasAccess(to: .automatedMonitoring))
LabeledContent(
"Last Generated",
value: settings.lastGeneratedAt?.formatted(date: .abbreviated, time: .shortened) ?? "Never"
)
if let statusMessage {
Text(statusMessage)
.font(appDensity.font(.caption))
.foregroundStyle(Color(.appTextSecondary))
}
Button(isGenerating ? "Generating…" : "Generate Now") {
Task { await generateNow() }
}
.disabled(isGenerating || !FeatureAccessService.hasAccess(to: .automatedMonitoring))
}
.listRowBackground(Color(.appSurface))
if logs.isEmpty {
Section {
EmptyStateCardView(
title: "No Reports Yet",
message: "Generated reports for your watchlist appear here after a manual or scheduled run.",
suggestion: "Tap Generate Now, or enable Scheduled Reports above.",
systemImage: "doc.text",
showsCardBackground: false
)
}
.listRowBackground(Color(.appSurface))
} else {
Section("Recent Reports") {
ForEach(logs) { log in
Button {
share(log)
} label: {
VStack(alignment: .leading, spacing: 2) {
Text("\(log.domainCount) domains • \(log.format.title)")
.foregroundStyle(.primary)
Text(log.generatedAt.formatted(date: .abbreviated, time: .shortened))
.font(.caption)
.foregroundStyle(Color(.appTextSecondary))
}
}
}
}
.listRowBackground(Color(.appSurface))
}
}
.scrollContentBackground(.hidden)
.background(Color(.appBackground))
.navigationTitle("Scheduled Reports")
}
private func saveSettings() {
ScheduledReportStorage.saveSettings(settings)
ScheduledReportScheduler.shared.syncSchedule()
}
private func generateNow() async {
isGenerating = true
let outcome = await ScheduledReportService.shared.generateReport(trigger: .manual, requireEnabledSetting: false)
statusMessage = outcome.message
settings = ScheduledReportStorage.loadSettings()
logs = ScheduledReportStorage.loadLogs()
isGenerating = false
}
private func share(_ log: ScheduledReportLog) {
let url = ScheduledReportStorage.reportsDirectory.appendingPathComponent(log.fileName)
guard let data = try? Data(contentsOf: url) else {
statusMessage = "That report is no longer available on disk."
return
}
ExportPresenter.share(filename: log.fileName, data: data)
}
}
|