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:
@@ -390,6 +390,7 @@ struct AddSnapshotView: View {
|
||||
let snapshot: Snapshot?
|
||||
|
||||
@StateObject private var viewModel: SnapshotFormViewModel
|
||||
@State private var showingDuplicateAlert = false
|
||||
|
||||
init(source: InvestmentSource, snapshot: Snapshot? = nil) {
|
||||
self.source = source
|
||||
@@ -515,10 +516,47 @@ struct AddSnapshotView: View {
|
||||
viewModel.checkClipboard()
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
String(localized: "snapshot_duplicate_title"),
|
||||
isPresented: $showingDuplicateAlert
|
||||
) {
|
||||
Button(String(localized: "snapshot_duplicate_replace"), role: .destructive) {
|
||||
performSave()
|
||||
}
|
||||
Button(String(localized: "snapshot_duplicate_add")) {
|
||||
performSave()
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
} message: {
|
||||
Text(String(localized: "snapshot_duplicate_message"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveSnapshot() {
|
||||
guard snapshot == nil else {
|
||||
performSave()
|
||||
return
|
||||
}
|
||||
|
||||
// Check for existing snapshot in the same month
|
||||
let calendar = Calendar.current
|
||||
let selectedMonth = calendar.dateComponents([.year, .month], from: viewModel.date)
|
||||
let repo = SnapshotRepository()
|
||||
let existing = repo.fetchSnapshots(for: source, limitedToMonths: nil)
|
||||
let hasDuplicate = existing.contains { snap in
|
||||
let snapMonth = calendar.dateComponents([.year, .month], from: snap.date)
|
||||
return snapMonth == selectedMonth
|
||||
}
|
||||
|
||||
if hasDuplicate {
|
||||
showingDuplicateAlert = true
|
||||
} else {
|
||||
performSave()
|
||||
}
|
||||
}
|
||||
|
||||
private func performSave() {
|
||||
guard let value = viewModel.value else { return }
|
||||
|
||||
let repository = SnapshotRepository()
|
||||
@@ -536,6 +574,11 @@ struct AddSnapshotView: View {
|
||||
clearNotes: viewModel.notes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
)
|
||||
} else {
|
||||
// Capture pre-save goal state for achievement detection
|
||||
let goalRepo = GoalRepository()
|
||||
let sourceRepo = InvestmentSourceRepository()
|
||||
let prevAchievedIds = goalsAchievedBeforeSave(goalRepo: goalRepo, sourceRepo: sourceRepo)
|
||||
|
||||
repository.createSnapshot(
|
||||
for: source,
|
||||
date: viewModel.date,
|
||||
@@ -543,6 +586,9 @@ struct AddSnapshotView: View {
|
||||
contribution: viewModel.contribution,
|
||||
notes: viewModel.notes.isEmpty ? nil : viewModel.notes
|
||||
)
|
||||
|
||||
// Check if any goals became achieved
|
||||
checkGoalAchievements(goalRepo: goalRepo, sourceRepo: sourceRepo, prevAchievedIds: prevAchievedIds)
|
||||
}
|
||||
|
||||
// Reschedule notification
|
||||
@@ -553,6 +599,46 @@ struct AddSnapshotView: View {
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func goalsAchievedBeforeSave(goalRepo: GoalRepository, sourceRepo: InvestmentSourceRepository) -> Set<UUID> {
|
||||
let goals = goalRepo.goals.filter { $0.isActive }
|
||||
var achieved = Set<UUID>()
|
||||
for goal in goals {
|
||||
let currentValue: Decimal
|
||||
if let accountId = goal.account?.safeId {
|
||||
currentValue = sourceRepo.sources
|
||||
.filter { $0.account?.id == accountId }
|
||||
.reduce(Decimal.zero) { $0 + $1.latestValue }
|
||||
} else {
|
||||
currentValue = sourceRepo.sources.reduce(Decimal.zero) { $0 + $1.latestValue }
|
||||
}
|
||||
let target = goal.targetDecimal
|
||||
if target > 0 && currentValue / target >= 1 {
|
||||
achieved.insert(goal.id)
|
||||
}
|
||||
}
|
||||
return achieved
|
||||
}
|
||||
|
||||
private func checkGoalAchievements(goalRepo: GoalRepository, sourceRepo: InvestmentSourceRepository, prevAchievedIds: Set<UUID>) {
|
||||
// Reload sources to pick up new snapshot value
|
||||
let goals = goalRepo.goals.filter { $0.isActive }
|
||||
for goal in goals {
|
||||
guard !prevAchievedIds.contains(goal.id) else { continue }
|
||||
let currentValue: Decimal
|
||||
if let accountId = goal.account?.safeId {
|
||||
currentValue = sourceRepo.sources
|
||||
.filter { $0.account?.id == accountId }
|
||||
.reduce(Decimal.zero) { $0 + $1.latestValue }
|
||||
} else {
|
||||
currentValue = sourceRepo.sources.reduce(Decimal.zero) { $0 + $1.latestValue }
|
||||
}
|
||||
let target = goal.targetDecimal
|
||||
if target > 0 && currentValue / target >= 1 {
|
||||
NotificationService.shared.scheduleGoalAchievedNotification(goalName: goal.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
||||
Reference in New Issue
Block a user