Goals por categoría: ámbito de categoría en objetivos (build 88) #46

Un Goal ya podía acotarse por cuenta; ahora también por categoría de
fuente, de modo que se puede fijar "100.000 € en Cash" y que el progreso
cuente solo las fuentes de esa categoría.

- CoreData: relación Goal.category ↔ Category.goals (opcional, Nullify)
- Goal.includes(_:)/relevantSources(from:)/scopeKey centralizan el filtro
  de ámbito (cuenta Y categoría) que antes estaba duplicado en
  GoalsViewModel, AddSourceView y NotificationService
- GoalEditorView: sección "Ámbito" con selector de categoría
- Chip de categoría en la lista de Goals y en la tarjeta del Dashboard
- ETA del Dashboard para goals con ámbito: usa la proyección del propio
  goal en vez de la evolución global de la cartera (que daría "objetivo
  alcanzado" en cuanto la cartera total superara el importe)
- GoalRepository e ImportService deduplican por (nombre, cuenta, categoría)
- Export/Import de goals incluye "category"
- Nuevas claves localizadas en los 7 idiomas

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcDn5ccuRFV83q7xWWokBT
This commit is contained in:
alexandrev-tibco
2026-09-12 09:32:02 +02:00
parent eaf0c8f93b
commit d88cc59627
21 changed files with 256 additions and 77 deletions
@@ -403,7 +403,14 @@ struct DashboardView: View {
paceStatusProvider: goalsViewModel.paceStatus(for:),
etaProvider: { goal in
let currentValue = goalsViewModel.totalValue(for: goal)
return viewModel.goalEtaText(for: goal, currentValue: currentValue)
guard goal.account != nil || goal.category != nil else {
return viewModel.goalEtaText(for: goal, currentValue: currentValue)
}
return viewModel.goalEtaText(
for: goal,
currentValue: currentValue,
projection: goalsViewModel.projection(for: goal)
)
}
)
}
@@ -1576,8 +1583,19 @@ struct GoalsSummaryCard: View {
lineWidth: 5
)
VStack(alignment: .leading, spacing: 3) {
Text(goal.name)
.font(.subheadline.weight(.semibold))
HStack(spacing: 6) {
Text(goal.name)
.font(.subheadline.weight(.semibold))
if let category = goal.category {
HStack(spacing: 3) {
Image(systemName: category.icon)
.font(.system(size: 9))
Text(category.name)
.font(.caption2.weight(.semibold))
}
.foregroundColor(category.color)
}
}
HStack(spacing: 4) {
Text(currentValue.compactCurrencyString)
.font(.caption.weight(.semibold))