Export/import JSON lossless (v3): goals, monthly contributions, journal, ratings

El JSON v2 perdía Goals, source.monthlyContribution, category.allocationTarget y
todo el Journal (nota/mood/rating/completionTime). Nuevo formato v3:
- source: + monthlyContribution, customFrequencyMonths
- category: + allocationTarget
- top-level: + goals[] (name/targetAmount/targetDate/isActive/account)
- top-level: + journalEntries[] (monthKey/note/mood/rating/completionTime/createdAt)

Import parsea y aplica todo, creando Goals y JournalEntry DIRECTAMENTE en el
contexto de background (GoalRepository es @MainActor → no usable desde el import).
Dedup: goals por nombre+cuenta, journal por monthKey → reimport idempotente.
Retrocompatible con JSON v2. CSV sin cambios.

Guard: si el import trae journal, se salta el marcado de completions derivado de
snapshots (evita JournalEntry duplicados por el desfase de merge viewContext).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
This commit is contained in:
alexandrev-tibco
2026-07-23 20:13:21 +02:00
parent b60e3041cb
commit df9309895a
2 changed files with 295 additions and 12 deletions
+60 -2
View File
@@ -1,5 +1,6 @@
import Foundation
import UIKit
import CoreData
class ExportService {
static let shared = ExportService()
@@ -63,7 +64,7 @@ class ExportService {
) -> String {
var exportData: [String: Any] = [:]
exportData["exportDate"] = ISO8601DateFormatter().string(from: Date())
exportData["version"] = 2
exportData["version"] = 3
exportData["currency"] = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency
let accounts = Dictionary(grouping: sources) { $0.account?.id.uuidString ?? "default" }
@@ -92,14 +93,23 @@ class ExportService {
"icon": category?.icon ?? "chart.pie.fill"
]
if let target = category?.allocationTarget?.doubleValue, target > 0 {
categoryDict["allocationTarget"] = target
}
var sourcesArray: [[String: Any]] = []
for source in categorySources {
var sourceDict: [String: Any] = [
"name": source.name,
"isActive": source.isActive,
"notificationFrequency": source.notificationFrequency
"notificationFrequency": source.notificationFrequency,
"customFrequencyMonths": Int(source.customFrequencyMonths)
]
if let contribution = source.monthlyContribution {
sourceDict["monthlyContribution"] = contribution.doubleValue
}
var snapshotsArray: [[String: Any]] = []
for snapshot in source.snapshotsArray {
var snapshotDict: [String: Any] = [
@@ -134,6 +144,54 @@ class ExportService {
exportData["accounts"] = accountsArray
// Goals (all accounts)
let isoFormatter = ISO8601DateFormatter()
let goalContext = CoreDataStack.shared.viewContext
let goalRequest: NSFetchRequest<Goal> = Goal.fetchRequest()
let allGoals = (try? goalContext.fetch(goalRequest)) ?? []
var goalsArray: [[String: Any]] = []
for goal in allGoals {
var goalDict: [String: Any] = [
"name": goal.name,
"isActive": goal.isActive,
"account": goal.account?.name as Any
]
if let amount = goal.targetAmount {
goalDict["targetAmount"] = amount.doubleValue
}
if let date = goal.targetDate {
goalDict["targetDate"] = isoFormatter.string(from: date)
}
goalsArray.append(goalDict)
}
exportData["goals"] = goalsArray
// Journal entries (all)
let journalRequest: NSFetchRequest<JournalEntry> = JournalEntry.fetchRequest()
let allJournalEntries = (try? goalContext.fetch(journalRequest)) ?? []
var journalArray: [[String: Any]] = []
for entry in allJournalEntries {
guard let monthKey = entry.monthKey, !monthKey.isEmpty else { continue }
var entryDict: [String: Any] = ["monthKey": monthKey]
if let note = entry.note, !note.isEmpty {
entryDict["note"] = note
}
if let mood = entry.moodRaw, !mood.isEmpty {
entryDict["mood"] = mood
}
if entry.rating > 0 {
entryDict["rating"] = Int(entry.rating)
}
if let completionTime = entry.completionTime {
entryDict["completionTime"] = isoFormatter.string(from: completionTime)
}
if let createdAt = entry.createdAt {
entryDict["createdAt"] = isoFormatter.string(from: createdAt)
}
journalArray.append(entryDict)
}
exportData["journalEntries"] = journalArray
// Add summary
let totalValue = sources.reduce(Decimal.zero) { $0 + $1.latestValue }
exportData["summary"] = [