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:
alexandrev-tibco
2026-07-22 19:58:13 +02:00
parent 20b8a32ea3
commit 99dc855467
3 changed files with 83 additions and 50 deletions
@@ -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(