Files
InvestmentTrackerApp/PortfolioJournal/Views/Dashboard/QuickUpdateView.swift
T
alexandrev-tibco 7a18dd8360 1.4.1 (build 42): contribución por snapshot, fixes Period vs Period y navegación iPad
- 151: contribución editable por snapshot en cualquier modo (quitado gate detailed)
  con diálogo de propagación al editar: solo este / adelante / atrás / todos
  (SnapshotRepository.propagateContribution, SnapshotFormViewModel.contributionChanged)
- 148: Period vs Period mostraba mal el último mes del period B — la agrupación usaba
  chartMonth(for:) que aplicaba el grace-period del check-in a snapshots históricos;
  ahora agrupa por mes calendario crudo
- 152: iPad Sources no saltaba entre fuentes — añadido .id() al SourceDetailView para
  recrear el StateObject al cambiar de selección
- 146/147: Year vs Year con forecast del año en curso (asterisco de estimado) y KPIs
  arriba / detalle debajo
- 145: card de contribución mensual en SourceDetailView
- Nuevas claves snapshot_contribution_propagate_* en los 7 idiomas

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
2026-06-30 17:01:50 +02:00

175 lines
6.4 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 contributions: [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 ?? "")
}
.onAppear { prefillContributions() }
}
}
@ViewBuilder
private func sourceRow(_ source: InvestmentSource) -> some View {
VStack(spacing: 6) {
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: valueBinding(for: source)
)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(width: 120)
.font(.subheadline)
}
if contributions[source.objectID] != nil {
HStack {
Text(String(localized: "quick_update_contribution_label"))
.font(.caption)
.foregroundColor(.secondary)
Spacer()
TextField(
String(localized: "quick_update_contribution_placeholder"),
text: contributionBinding(for: source)
)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(width: 120)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
private var filledValues: [NSManagedObjectID: String] {
values.filter { !$0.value.trimmingCharacters(in: .whitespaces).isEmpty }
}
private func valueBinding(for source: InvestmentSource) -> Binding<String> {
Binding(
get: { values[source.objectID] ?? "" },
set: { values[source.objectID] = $0 }
)
}
private func contributionBinding(for source: InvestmentSource) -> Binding<String> {
Binding(
get: { contributions[source.objectID] ?? "" },
set: { contributions[source.objectID] = $0 }
)
}
private func prefillContributions() {
for source in sources {
if let amount = MonthlyContributionStore.contribution(for: source.id) {
contributions[source.objectID] = String(format: "%.2f", NSDecimalNumber(decimal: amount).doubleValue)
}
}
}
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
if let contribRaw = contributions[source.objectID],
!contribRaw.trimmingCharacters(in: .whitespaces).isEmpty,
let contrib = CurrencyFormatter.parseUserInput(contribRaw) {
snapshot.contribution = NSDecimalNumber(decimal: contrib)
}
}
do {
try context.save()
// Reschedule source reminders so they reflect the new snapshots
for source in sources where values[source.objectID].map({ !$0.isEmpty }) == true {
NotificationService.shared.scheduleReminder(for: source)
}
dismiss()
} catch {
saveError = error.localizedDescription
}
isSaving = false
}
}