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:
@@ -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"] = [
|
||||
|
||||
Reference in New Issue
Block a user