From 6a8a66546b47f119731d66ce7c15699fd0cb6e4a Mon Sep 17 00:00:00 2001 From: Trey t Date: Wed, 11 Mar 2026 18:19:01 -0500 Subject: [PATCH] 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 --- Shared/Persisence/DataControllerHelper.swift | 139 +++++++++++++++++-- Shared/Services/ReportPDFGenerator.swift | 38 +++-- Shared/Views/DayView/WeatherCardView.swift | 2 +- Shared/Views/EntryListView.swift | 29 ++++ Shared/Views/InsightsView/ReportsView.swift | 13 +- 5 files changed, 189 insertions(+), 32 deletions(-) diff --git a/Shared/Persisence/DataControllerHelper.swift b/Shared/Persisence/DataControllerHelper.swift index 7c6fe42..28fae48 100644 --- a/Shared/Persisence/DataControllerHelper.swift +++ b/Shared/Persisence/DataControllerHelper.swift @@ -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( sortBy: [SortDescriptor(\.forDate, order: .forward)] diff --git a/Shared/Services/ReportPDFGenerator.swift b/Shared/Services/ReportPDFGenerator.swift index 8ed6cce..5ebe8ea 100644 --- a/Shared/Services/ReportPDFGenerator.swift +++ b/Shared/Services/ReportPDFGenerator.swift @@ -6,6 +6,7 @@ // import Foundation +import UIKit import WebKit @MainActor @@ -60,7 +61,7 @@ final class ReportPDFGenerator { - + @@ -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.. 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) diff --git a/Shared/Views/InsightsView/ReportsView.swift b/Shared/Views/InsightsView/ReportsView.swift index 62ceb2c..c84e6a9 100644 --- a/Shared/Views/InsightsView/ReportsView.swift +++ b/Shared/Views/InsightsView/ReportsView.swift @@ -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