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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user