1.6.0: auto-rellenar huecos (snapshots estimados) + widgets/lock screen enriquecidos

Auto-relleno (#5): SnapshotRepository.autoFillGap crea snapshots interpolados
linealmente entre los dos snapshots que bordean el hueco, marcados isEstimated.
deleteEstimatedSnapshots para borrado en bloque. Botón 'Autocompletar N meses'
(wand.and.stars) en DataGapsSheet + 'Añadir manualmente'. Idempotente. Strings 7 idiomas.

Widgets/Lock Screen (#8, vía agente): familias enriquecidas — systemSmall/Medium/Large
con net worth + cambio desde check-in + racha + próximo check-in + progreso de objetivo;
lock screen accessoryCircular (gauge objetivo/milestone), accessoryRectangular, accessoryInline.
Deep link quickupdate preservado. Solo InvestmentWidget.swift.

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-25 12:13:51 +02:00
parent 1a3fce0df4
commit 922710c53f
10 changed files with 524 additions and 74 deletions
@@ -123,6 +123,56 @@ class SnapshotRepository: ObservableObject {
return snapshot
}
// MARK: - Gap Auto-Fill (estimated snapshots)
/// Creates ESTIMATED snapshots for the missing months of an internal gap,
/// linearly interpolated between the source's value at `from` and at `to`.
/// They are marked `isEstimated` so they read as clearly distinct from real
/// data and can be removed in bulk. Idempotent (skips months that already have
/// a snapshot). Returns how many were created.
@discardableResult
func autoFillGap(source: InvestmentSource, from: Date, to: Date, missingMonthDates: [Date]) -> Int {
let snaps = source.snapshotsArray
func value(atMonth month: Date) -> Decimal? {
snaps.filter { $0.date.startOfMonth == month.startOfMonth }
.max(by: { $0.date < $1.date })?.decimalValue
}
guard let v0 = value(atMonth: from),
let v1 = value(atMonth: to),
!missingMonthDates.isEmpty else { return 0 }
let steps = missingMonthDates.count + 1 // segments between v0 and v1
var created = 0
for (i, month) in missingMonthDates.enumerated() {
let monthStart = month.startOfMonth
if snaps.contains(where: { $0.date.startOfMonth == monthStart }) { continue }
let fraction = Decimal(i + 1) / Decimal(steps)
let interpolated = v0 + (v1 - v0) * fraction
let snapshot = Snapshot(context: context)
snapshot.source = source
snapshot.date = monthStart
snapshot.value = NSDecimalNumber(decimal: interpolated)
snapshot.isEstimated = true
snapshot.notes = String(localized: "gaps_estimated_note")
created += 1
}
if created > 0 { save() }
return created
}
/// Deletes all estimated (auto-filled) snapshots, optionally scoped to one
/// source. Returns how many were removed.
@discardableResult
func deleteEstimatedSnapshots(for source: InvestmentSource? = nil) -> Int {
let request = NSFetchRequest<Snapshot>(entityName: "Snapshot")
request.predicate = NSPredicate(format: "isEstimated == YES")
guard let estimated = try? context.fetch(request) else { return 0 }
let targets = source == nil ? estimated : estimated.filter { $0.source?.id == source?.id }
for snapshot in targets { context.delete(snapshot) }
if !targets.isEmpty { save() }
return targets.count
}
// MARK: - Update
func updateSnapshot(