Migrate journal entries to CoreData for iCloud sync (build 33)

- Add JournalEntry CoreData entity (syncable=YES): id, monthKey, note, moodRaw, rating, completionTime, createdAt
- Rewrite MonthlyCheckInStore: CoreData as primary storage, same public API
- One-time migration from UserDefaults triggered after store loads
- MonthlyCheckInCard: @FetchRequest on JournalEntry — reactive to iCloud sync
- MonthlyCheckInView: onReceive NSManagedObjectContextObjectsDidChange to refresh @State vars on sync
- Stats cards: observe NSManagedObjectContextObjectsDidChange instead of UserDefaults
- ChartsViewModel.completedMonthKeys: remove MonthlyCheckInStore dependency (data-driven)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-05-26 15:05:53 +02:00
parent 0b01d6f563
commit 270edeb536
8 changed files with 236 additions and 276 deletions
@@ -0,0 +1,38 @@
import Foundation
import CoreData
@objc(JournalEntry)
public class JournalEntry: NSManagedObject, Identifiable {
@nonobjc public class func fetchRequest() -> NSFetchRequest<JournalEntry> {
return NSFetchRequest<JournalEntry>(entityName: "JournalEntry")
}
@NSManaged public var id: UUID?
@NSManaged public var monthKey: String?
@NSManaged public var note: String?
@NSManaged public var moodRaw: String?
@NSManaged public var rating: Int16
@NSManaged public var completionTime: Date?
@NSManaged public var createdAt: Date?
public override func awakeFromInsert() {
super.awakeFromInsert()
id = UUID()
createdAt = Date()
}
var mood: MonthlyCheckInMood? {
get { moodRaw.flatMap { MonthlyCheckInMood(rawValue: $0) } }
set { moodRaw = newValue?.rawValue }
}
var ratingValue: Int? {
get { rating > 0 ? Int(rating) : nil }
set { rating = Int16(newValue.map { min(max(1, $0), 5) } ?? 0) }
}
var completionDate: Date? {
get { completionTime }
set { completionTime = newValue }
}
}
@@ -102,4 +102,13 @@
<attribute name="value" optional="YES" attributeType="Decimal"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InvestmentSource" inverseName="snapshots" inverseEntity="InvestmentSource"/>
</entity>
<entity name="JournalEntry" representedClassName="JournalEntry" syncable="YES">
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="monthKey" optional="YES" attributeType="String"/>
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="moodRaw" optional="YES" attributeType="String"/>
<attribute name="rating" optional="YES" attributeType="Integer 16" usesScalarValueType="YES"/>
<attribute name="completionTime" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
</model>
@@ -186,6 +186,7 @@ class CoreDataStack: ObservableObject {
}
DispatchQueue.main.async {
self?.isLoaded = true
MonthlyCheckInStore.migrateIfNeeded()
}
}