Release 1.4.0 (build 31): retención, quick update, streak, what's new
- 1A: Notificación mensual de resumen de portfolio (día 5 de cada mes) - 1B: Badge de racha mensual en Dashboard (streak counter) - 1C: Empty states mejorados en Goals y Journal con CTAs claros - 2A: Quick Update sheet — actualiza todas las fuentes desde una pantalla - 2B: Notificación batch update redirige al Quick Update sheet - 2C: Widget deep link portfoliojournal://quickupdate abre Quick Update - 3A: App Store version check banner en Settings - 3C: What's New sheet en primer launch de versión nueva - Localización completa en 7 idiomas para todas las nuevas strings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ class DashboardViewModel: ObservableObject {
|
||||
// MARK: - Chart Data
|
||||
|
||||
@Published var evolutionData: [(date: Date, value: Decimal)] = []
|
||||
@Published var updateStreak: Int = 0
|
||||
|
||||
// MARK: - Portfolio Forecast
|
||||
|
||||
@@ -204,6 +205,9 @@ class DashboardViewModel: ObservableObject {
|
||||
// Calculate portfolio forecast
|
||||
updatePortfolioForecast()
|
||||
|
||||
// Compute update streak
|
||||
updateStreak = computeStreak(from: evolutionData)
|
||||
|
||||
// Log screen view
|
||||
FirebaseService.shared.logScreenView(screenName: "Dashboard")
|
||||
}
|
||||
@@ -535,6 +539,42 @@ class DashboardViewModel: ObservableObject {
|
||||
return annualized
|
||||
}
|
||||
|
||||
private func computeStreak(from evolutionData: [(date: Date, value: Decimal)]) -> Int {
|
||||
guard !evolutionData.isEmpty else { return 0 }
|
||||
let calendar = Calendar.current
|
||||
|
||||
// Group into (year, month) set
|
||||
let monthSet: Set<DateComponents> = Set(evolutionData.map { point in
|
||||
let comps = calendar.dateComponents([.year, .month], from: point.date)
|
||||
return DateComponents(year: comps.year, month: comps.month)
|
||||
})
|
||||
|
||||
// Get sorted unique months descending
|
||||
let sortedMonths = monthSet
|
||||
.compactMap { comps -> Date? in calendar.date(from: comps) }
|
||||
.sorted(by: >)
|
||||
|
||||
guard let mostRecent = sortedMonths.first else { return 0 }
|
||||
|
||||
var streak = 1
|
||||
var current = mostRecent
|
||||
|
||||
for i in 1..<sortedMonths.count {
|
||||
guard let expected = calendar.date(byAdding: .month, value: -1, to: current) else { break }
|
||||
let prev = sortedMonths[i]
|
||||
let prevComps = calendar.dateComponents([.year, .month], from: prev)
|
||||
let expectedComps = calendar.dateComponents([.year, .month], from: expected)
|
||||
if prevComps.year == expectedComps.year && prevComps.month == expectedComps.month {
|
||||
streak += 1
|
||||
current = prev
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return streak
|
||||
}
|
||||
|
||||
// Virtual snapshot for forecast calculations
|
||||
private struct VirtualSnapshot {
|
||||
let date: Date
|
||||
|
||||
Reference in New Issue
Block a user