Files
InvestmentTrackerApp/PortfolioJournal/Views/Dashboard/QuickUpdateView.swift
T
alexandrev-tibco b48a47ce10 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>
2026-05-21 23:09:36 +02:00

128 lines
4.3 KiB
Swift

import SwiftUI
import CoreData
struct QuickUpdateView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.managedObjectContext) private var context
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \InvestmentSource.name, ascending: true)],
predicate: NSPredicate(format: "isActive == YES"),
animation: .default
) private var sources: FetchedResults<InvestmentSource>
@State private var values: [NSManagedObjectID: String] = [:]
@State private var isSaving = false
@State private var saveError: String?
var body: some View {
NavigationStack {
ZStack {
AppBackground()
if sources.isEmpty {
ContentUnavailableView(
String(localized: "quick_update_no_sources"),
systemImage: "list.bullet",
description: Text(String(localized: "quick_update_no_sources_body"))
)
} else {
List {
Section {
ForEach(sources) { source in
sourceRow(source)
}
} header: {
Text(String(localized: "quick_update_section_header"))
} footer: {
Text(String(localized: "quick_update_section_footer"))
}
}
.scrollContentBackground(.hidden)
}
}
.navigationTitle(String(localized: "quick_update_title"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(String(localized: "cancel")) { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button(String(localized: "quick_update_save")) {
saveAll()
}
.disabled(isSaving || filledValues.isEmpty)
.fontWeight(.semibold)
}
}
.alert("Error", isPresented: Binding(
get: { saveError != nil },
set: { if !$0 { saveError = nil } }
)) {
Button("OK", role: .cancel) { saveError = nil }
} message: {
Text(saveError ?? "")
}
}
}
@ViewBuilder
private func sourceRow(_ source: InvestmentSource) -> some View {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(source.name)
.font(.subheadline.weight(.medium))
if source.latestValue != .zero {
Text(source.latestValue.currencyString)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
TextField(
String(localized: "quick_update_placeholder"),
text: binding(for: source)
)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(width: 120)
.font(.subheadline)
}
}
private var filledValues: [NSManagedObjectID: String] {
values.filter { !$0.value.trimmingCharacters(in: .whitespaces).isEmpty }
}
private func binding(for source: InvestmentSource) -> Binding<String> {
Binding(
get: { values[source.objectID] ?? "" },
set: { values[source.objectID] = $0 }
)
}
private func saveAll() {
isSaving = true
let now = Date()
for source in sources {
guard let raw = values[source.objectID],
!raw.trimmingCharacters(in: .whitespaces).isEmpty,
let value = CurrencyFormatter.parseUserInput(raw) else { continue }
let snapshot = Snapshot(context: context)
snapshot.id = UUID()
snapshot.value = NSDecimalNumber(decimal: value)
snapshot.date = now
snapshot.source = source
}
do {
try context.save()
dismiss()
} catch {
saveError = error.localizedDescription
}
isSaving = false
}
}