Desactiva auto-dedup (pérdida de datos cross-device) + forward-fill en gráficas de evolution
CRÍTICO: el auto-dedup de build 68 borraba entidades padre con regla Cascade (Account/Category). Al propagarse el delete por CloudKit, otros dispositivos aplicaban la cascada al importar ANTES de la reasignación de hijos → wipe de sources/snapshots. Vació iPhone y iPad. Quitadas las llamadas automáticas a cleanupLogicalDuplicates() en processRemoteChanges y en la carga; el UUID-dedup seguro se mantiene. Métodos quedan definidos para un rediseño CloudKit-safe. Gráficas: forward-fill por source en evolution/allocation/period para que el total de cada mes refleje el último valor de CADA source (no solo las actualizadas ese mes) y el último punto coincida con el total del portfolio. 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:
@@ -735,17 +735,25 @@ class ChartsViewModel: ObservableObject {
|
||||
// MARK: - Chart Calculations
|
||||
|
||||
private func calculateEvolutionData(from snapshots: [Snapshot]) {
|
||||
let sortedSnapshots = snapshots.sorted { $0.date < $1.date }
|
||||
let groupedByMonth = Dictionary(grouping: sortedSnapshots) { snapshot -> DateComponents in
|
||||
let groupedByMonth = Dictionary(grouping: snapshots) { snapshot -> DateComponents in
|
||||
chartMonth(for: snapshot.date)
|
||||
}
|
||||
|
||||
var series: [(date: Date, value: Decimal)] = []
|
||||
series.reserveCapacity(groupedByMonth.count)
|
||||
let sortedMonthKeys = groupedByMonth.keys.sorted {
|
||||
(Calendar.current.date(from: $0) ?? .distantPast) < (Calendar.current.date(from: $1) ?? .distantPast)
|
||||
}
|
||||
|
||||
for (key, monthSnapshots) in groupedByMonth {
|
||||
var series: [(date: Date, value: Decimal)] = []
|
||||
series.reserveCapacity(sortedMonthKeys.count)
|
||||
|
||||
// Forward-fill per source so a month's total includes every source's last
|
||||
// known value, not only the sources updated that month (which would make the
|
||||
// line dip and disagree with the portfolio total). See DashboardViewModel.
|
||||
var currentValueBySource: [UUID: Decimal] = [:]
|
||||
|
||||
for key in sortedMonthKeys {
|
||||
var latestBySource: [UUID: Snapshot] = [:]
|
||||
for snapshot in monthSnapshots {
|
||||
for snapshot in groupedByMonth[key] ?? [] {
|
||||
guard let sourceId = snapshot.source?.id else { continue }
|
||||
if let existing = latestBySource[sourceId] {
|
||||
if snapshot.date > existing.date {
|
||||
@@ -755,14 +763,15 @@ class ChartsViewModel: ObservableObject {
|
||||
latestBySource[sourceId] = snapshot
|
||||
}
|
||||
}
|
||||
for (sourceId, snapshot) in latestBySource {
|
||||
currentValueBySource[sourceId] = snapshot.decimalValue
|
||||
}
|
||||
|
||||
let total = latestBySource.values.reduce(Decimal.zero) { $0 + $1.decimalValue }
|
||||
let total = currentValueBySource.values.reduce(Decimal.zero, +)
|
||||
let date = Calendar.current.date(from: key) ?? Date()
|
||||
series.append((date: date, value: total))
|
||||
}
|
||||
|
||||
series.sort { $0.date < $1.date }
|
||||
|
||||
evolutionData = downsampleSeries(series, maxPoints: maxChartPoints)
|
||||
}
|
||||
|
||||
@@ -852,12 +861,16 @@ class ChartsViewModel: ObservableObject {
|
||||
var globalCategoryTotals: [String: (total: Decimal, color: String)] = [:]
|
||||
var monthlyData: [(date: Date, categories: [String: (value: Decimal, color: String)])] = []
|
||||
|
||||
// Forward-filled across months: a source keeps its last known value in months
|
||||
// where it wasn't updated, so category allocations don't distort when a single
|
||||
// source is missing that month.
|
||||
var sourceLatest: [UUID: Snapshot] = [:]
|
||||
|
||||
for monthKey in sortedMonths {
|
||||
guard let monthSnapshots = groupedByMonth[monthKey],
|
||||
let monthDate = Calendar.current.date(from: monthKey) else { continue }
|
||||
|
||||
var categoryTotals: [String: (value: Decimal, color: String)] = [:]
|
||||
var sourceLatest: [UUID: Snapshot] = [:]
|
||||
|
||||
for snapshot in monthSnapshots {
|
||||
guard let sourceId = snapshot.source?.id else { continue }
|
||||
@@ -1541,10 +1554,15 @@ class ChartsViewModel: ObservableObject {
|
||||
let grouped = Dictionary(grouping: periodSnapshots) { snap -> DateComponents in
|
||||
Calendar.current.dateComponents([.year, .month], from: snap.date)
|
||||
}
|
||||
let sortedKeys = grouped.keys.sorted {
|
||||
(Calendar.current.date(from: $0) ?? .distantPast) < (Calendar.current.date(from: $1) ?? .distantPast)
|
||||
}
|
||||
var monthlyTotalsArr: [(date: Date, value: Double)] = []
|
||||
for (key, snaps) in grouped {
|
||||
var latestBySource: [UUID: Snapshot] = [:]
|
||||
for snap in snaps {
|
||||
// Forward-fill per source across the period so an un-updated source keeps
|
||||
// its last value instead of dropping the month's total.
|
||||
var latestBySource: [UUID: Snapshot] = [:]
|
||||
for key in sortedKeys {
|
||||
for snap in grouped[key] ?? [] {
|
||||
guard let sourceId = snap.source?.id else { continue }
|
||||
if let existing = latestBySource[sourceId] {
|
||||
if snap.date > existing.date { latestBySource[sourceId] = snap }
|
||||
@@ -1556,7 +1574,6 @@ class ChartsViewModel: ObservableObject {
|
||||
let date = Calendar.current.date(from: key) ?? Date()
|
||||
monthlyTotalsArr.append((date: date, value: total))
|
||||
}
|
||||
monthlyTotalsArr.sort { $0.date < $1.date }
|
||||
guard let firstValue = monthlyTotalsArr.first?.value, firstValue > 0 else { return nil }
|
||||
|
||||
let points = monthlyTotalsArr.enumerated().map { index, item in
|
||||
@@ -1669,17 +1686,23 @@ class ChartsViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func monthlyTotalsByMonthYear(from snapshots: [Snapshot]) -> [(date: Date, totalValue: Decimal)] {
|
||||
let sortedSnapshots = snapshots.sorted { $0.date < $1.date }
|
||||
let groupedByMonth = Dictionary(grouping: sortedSnapshots) { snapshot -> DateComponents in
|
||||
let groupedByMonth = Dictionary(grouping: snapshots) { snapshot -> DateComponents in
|
||||
chartMonth(for: snapshot.date)
|
||||
}
|
||||
|
||||
var totals: [(date: Date, totalValue: Decimal)] = []
|
||||
totals.reserveCapacity(groupedByMonth.count)
|
||||
let sortedMonthKeys = groupedByMonth.keys.sorted {
|
||||
(Calendar.current.date(from: $0) ?? .distantPast) < (Calendar.current.date(from: $1) ?? .distantPast)
|
||||
}
|
||||
|
||||
for (key, monthSnapshots) in groupedByMonth {
|
||||
var latestBySource: [UUID: Snapshot] = [:]
|
||||
for snapshot in monthSnapshots {
|
||||
var totals: [(date: Date, totalValue: Decimal)] = []
|
||||
totals.reserveCapacity(sortedMonthKeys.count)
|
||||
|
||||
// Forward-fill per source so each month's total reflects every source's last
|
||||
// known value (matches monthlyTotals and the portfolio total).
|
||||
var latestBySource: [UUID: Snapshot] = [:]
|
||||
|
||||
for key in sortedMonthKeys {
|
||||
for snapshot in groupedByMonth[key] ?? [] {
|
||||
guard let sourceId = snapshot.source?.id else { continue }
|
||||
if let existing = latestBySource[sourceId] {
|
||||
if snapshot.date > existing.date {
|
||||
@@ -1695,7 +1718,7 @@ class ChartsViewModel: ObservableObject {
|
||||
totals.append((date: date, totalValue: total))
|
||||
}
|
||||
|
||||
return totals.sorted { $0.date < $1.date }
|
||||
return totals
|
||||
}
|
||||
|
||||
private func monthlyReturnSeries(
|
||||
|
||||
Reference in New Issue
Block a user