Enrich test data, fix multi-page PDF export, and polish UI

- Populate debug test data with random notes, guided reflections, and weather
- Fix PDF export to use UIPrintPageRenderer for proper multi-page pagination
- Add journal/reflection indicator icons to day list entry cells
- Fix weather card icon contrast by using secondarySystemBackground
- Align Generate Report and Export PDF button widths in ReportsView

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-11 18:19:01 -05:00
parent a9eeddf2b5
commit 6a8a66546b
5 changed files with 189 additions and 32 deletions

View File

@@ -63,15 +63,15 @@ extension DataController {
for idx in 1..<1000 {
let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
var moodValue = Int.random(in: 3...4)
if Int.random(in: 0...400) % 5 == 0 {
moodValue = Int.random(in: 0...4)
}
let mood = Self.randomMood()
let entry = MoodEntryModel(
forDate: date,
mood: Mood(rawValue: moodValue) ?? .average,
entryType: .listView
mood: mood,
entryType: .listView,
notes: Self.randomNotes(),
weatherJSON: Self.randomWeatherJSON(for: date),
reflectionJSON: Self.randomReflectionJSON(for: mood, on: date)
)
modelContext.insert(entry)
}
@@ -85,15 +85,15 @@ extension DataController {
for idx in 1...730 {
let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
var moodValue = Int.random(in: 3...4)
if Int.random(in: 0...400) % 5 == 0 {
moodValue = Int.random(in: 0...4)
}
let mood = Self.randomMood()
let entry = MoodEntryModel(
forDate: date,
mood: Mood(rawValue: moodValue) ?? .average,
entryType: .listView
mood: mood,
entryType: .listView,
notes: Self.randomNotes(),
weatherJSON: Self.randomWeatherJSON(for: date),
reflectionJSON: Self.randomReflectionJSON(for: mood, on: date)
)
modelContext.insert(entry)
}
@@ -102,6 +102,121 @@ extension DataController {
}
#endif
private static func randomMood() -> Mood {
var moodValue = Int.random(in: 3...4)
if Int.random(in: 0...400) % 5 == 0 {
moodValue = Int.random(in: 0...4)
}
return Mood(rawValue: moodValue) ?? .average
}
// ~40% of entries get notes
private static func randomNotes() -> String? {
guard Bool.random() && Bool.random() == false else { return nil }
return sampleNotes.randomElement()
}
private static let sampleNotes: [String] = [
"Had a productive morning and got a lot done.",
"Went for a long walk in the park today.",
"Feeling grateful for the little things.",
"Tough meeting at work but pushed through.",
"Spent quality time with family this evening.",
"Couldn't sleep well last night, feeling tired.",
"Great workout at the gym today!",
"Read a really interesting book chapter.",
"Cooked a new recipe and it turned out great.",
"Feeling a bit anxious about tomorrow.",
"Had coffee with an old friend, felt wonderful.",
"Rainy day but cozy inside with a good movie.",
"Got some bad news, trying to stay positive.",
"Meditation session helped clear my mind.",
"Busy day but managed to stay focused.",
"Enjoyed a quiet evening at home.",
"Felt overwhelmed by my to-do list.",
"Beautiful sunset on the drive home.",
"Tried journaling for the first time in a while.",
"Had a really meaningful conversation today.",
]
// ~25% of entries get guided reflections
private static func randomReflectionJSON(for mood: Mood, on date: Date) -> String? {
guard Int.random(in: 0...3) == 0 else { return nil }
let category = MoodCategory(from: mood)
let questions = GuidedReflection.questions(for: category)
let answers = sampleAnswers(for: category)
let responses = questions.enumerated().map { index, question in
GuidedReflection.Response(
id: index,
question: question,
answer: answers[index % answers.count]
)
}
let reflection = GuidedReflection(
moodCategory: category,
responses: responses,
completedAt: date
)
return reflection.encode()
}
private static func sampleAnswers(for category: MoodCategory) -> [String] {
switch category {
case .positive:
return [
"I felt a sense of calm and contentment throughout the day.",
"Connecting with a friend really lifted my spirits.",
"I want to keep making time for the things that bring me joy.",
]
case .neutral:
return [
"A mix of emotions today, nothing too strong in either direction.",
"Work was steady but unremarkable.",
"I'd like to be more intentional about how I spend my evenings.",
"Maybe adding a short walk after lunch could help.",
]
case .negative:
return [
"I've been worrying about things outside my control.",
"A stressful interaction this morning set the tone for the day.",
"I think I need some quiet time to recharge.",
"Taking a few deep breaths and stepping away from screens.",
]
}
}
private static let weatherConditions: [(symbol: String, condition: String)] = [
("sun.max.fill", "Clear"),
("cloud.sun.fill", "Partly Cloudy"),
("cloud.fill", "Cloudy"),
("cloud.rain.fill", "Rain"),
("cloud.heavyrain.fill", "Heavy Rain"),
("cloud.drizzle.fill", "Drizzle"),
("cloud.bolt.fill", "Thunderstorms"),
("cloud.snow.fill", "Snow"),
("cloud.fog.fill", "Foggy"),
("wind", "Windy"),
("sun.haze.fill", "Hazy"),
]
private static func randomWeatherJSON(for date: Date) -> String? {
let condition = weatherConditions.randomElement()!
let high = Double.random(in: 5...35)
let low = high - Double.random(in: 5...15)
let weather = WeatherData(
conditionSymbol: condition.symbol,
condition: condition.condition,
temperature: (high + low) / 2.0,
highTemperature: high,
lowTemperature: low,
humidity: Double.random(in: 0.2...0.95),
latitude: 37.7749,
longitude: -122.4194,
fetchedAt: date
)
return weather.encode()
}
func longestStreak() -> [MoodEntryModel] {
let descriptor = FetchDescriptor<MoodEntryModel>(
sortBy: [SortDescriptor(\.forDate, order: .forward)]

View File

@@ -6,6 +6,7 @@
//
import Foundation
import UIKit
import WebKit
@MainActor
@@ -60,7 +61,7 @@ final class ReportPDFGenerator {
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=612, initial-scale=1">
<style>
\(cssStyles)
</style>
@@ -294,7 +295,7 @@ final class ReportPDFGenerator {
let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 612, height: 792))
webView.isOpaque = false
// Load HTML and wait for it to render
// Load HTML and wait for it to finish rendering
return try await withCheckedThrowingContinuation { continuation in
let delegate = PDFNavigationDelegate { result in
switch result {
@@ -443,18 +444,29 @@ private class PDFNavigationDelegate: NSObject, WKNavigationDelegate {
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let config = WKPDFConfiguration()
config.rect = CGRect(x: 0, y: 0, width: 612, height: 792) // US Letter
// Use UIPrintPageRenderer for proper multi-page pagination.
// createPDF(configuration:) only captures a single rect it does NOT paginate.
let renderer = UIPrintPageRenderer()
renderer.addPrintFormatter(webView.viewPrintFormatter(), startingAtPageAt: 0)
webView.createPDF(configuration: config) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let data):
self?.completion(.success(data))
case .failure(let error):
self?.completion(.failure(ReportPDFGenerator.PDFError.pdfGenerationFailed(underlying: error)))
}
// US Letter size (72 points per inch: 8.5 x 11 inches)
let pageRect = CGRect(x: 0, y: 0, width: 612, height: 792)
renderer.setValue(pageRect, forKey: "paperRect")
renderer.setValue(pageRect, forKey: "printableRect")
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, pageRect, nil)
renderer.prepare(forDrawingPages: NSMakeRange(0, renderer.numberOfPages))
let bounds = UIGraphicsGetPDFContextBounds()
for i in 0..<renderer.numberOfPages {
UIGraphicsBeginPDFPage()
renderer.drawPage(at: i, in: bounds)
}
UIGraphicsEndPDFContext()
DispatchQueue.main.async { [weak self] in
self?.completion(.success(pdfData as Data))
}
}

View File

@@ -49,7 +49,7 @@ struct WeatherCardView: View {
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(.systemBackground))
.fill(Color(.secondarySystemBackground))
)
}

View File

@@ -32,6 +32,18 @@ struct EntryListView: View {
entry.moodValue == Mood.missing.rawValue
}
private var hasNotes: Bool {
if let notes = entry.notes, !notes.isEmpty { return true }
return false
}
private var hasReflection: Bool {
if let json = entry.reflectionJSON,
let reflection = GuidedReflection.decode(from: json),
reflection.answeredCount > 0 { return true }
return false
}
// MARK: - Cached Date Strings (avoids repeated ICU/Calendar operations)
private var dateCache: DateFormattingCache { DateFormattingCache.shared }
private var cachedDay: String { dateCache.string(for: entry.forDate, format: .day) }
@@ -92,6 +104,23 @@ struct EntryListView: View {
orbitStyle
}
}
.overlay(alignment: .bottomTrailing) {
if !isMissing && (hasNotes || hasReflection) {
HStack(spacing: 4) {
if hasNotes {
Image(systemName: "note.text")
.font(.caption2)
}
if hasReflection {
Image(systemName: "sparkles")
.font(.caption2)
}
}
.foregroundStyle(.secondary)
.padding(.trailing, 12)
.padding(.bottom, 6)
}
}
.accessibilityElement(children: .combine)
.accessibilityIdentifier(AccessibilityID.DayView.entryRow(dateString: cachedYearMonthDayDigits))
.accessibilityLabel(accessibilityDescription)

View File

@@ -254,6 +254,12 @@ struct ReportsView: View {
Spacer()
}
.padding()
.background(
RoundedRectangle(cornerRadius: 14)
.fill(colorScheme == .dark ? Color(.systemGray6) : .white)
)
.padding(.horizontal)
Button {
viewModel.showPrivacyConfirmation = true
@@ -269,14 +275,9 @@ struct ReportsView: View {
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.padding(.horizontal)
.accessibilityIdentifier(AccessibilityID.Reports.exportButton)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 14)
.fill(colorScheme == .dark ? Color(.systemGray6) : .white)
)
.padding(.horizontal)
}
// MARK: - Error Card