1.4.1 (build 42): contribución por snapshot, fixes Period vs Period y navegación iPad

- 151: contribución editable por snapshot en cualquier modo (quitado gate detailed)
  con diálogo de propagación al editar: solo este / adelante / atrás / todos
  (SnapshotRepository.propagateContribution, SnapshotFormViewModel.contributionChanged)
- 148: Period vs Period mostraba mal el último mes del period B — la agrupación usaba
  chartMonth(for:) que aplicaba el grace-period del check-in a snapshots históricos;
  ahora agrupa por mes calendario crudo
- 152: iPad Sources no saltaba entre fuentes — añadido .id() al SourceDetailView para
  recrear el StateObject al cambiar de selección
- 146/147: Year vs Year con forecast del año en curso (asterisco de estimado) y KPIs
  arriba / detalle debajo
- 145: card de contribución mensual en SourceDetailView
- Nuevas claves snapshot_contribution_propagate_* en los 7 idiomas

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
This commit is contained in:
alexandrev-tibco
2026-06-30 16:51:03 +02:00
parent 54dfd8a8e3
commit 7a18dd8360
48 changed files with 2854 additions and 608 deletions
@@ -148,6 +148,40 @@ class SnapshotRepository: ObservableObject {
save()
}
// MARK: - Contribution Propagation
enum ContributionPropagation {
case forward // snapshots after the pivot
case backward // snapshots before the pivot
case all // every other snapshot of the source
}
/// Applies `amount` as the contribution of other snapshots of the same source,
/// relative to `snapshot`'s date, according to `direction`. The pivot snapshot
/// itself is left untouched (it was already saved with its own value).
func propagateContribution(
_ amount: Decimal,
from snapshot: Snapshot,
direction: ContributionPropagation
) {
guard let source = snapshot.source,
let set = source.snapshots as? Set<Snapshot> else { return }
let pivot = snapshot.date
let targets = set.filter { other in
guard other.objectID != snapshot.objectID else { return false }
switch direction {
case .forward: return other.date > pivot
case .backward: return other.date < pivot
case .all: return true
}
}
let value = NSDecimalNumber(decimal: amount)
for target in targets {
target.contribution = value
}
save()
}
// MARK: - Delete
func deleteSnapshot(_ snapshot: Snapshot) {