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:
@@ -87,6 +87,7 @@ class ImportService {
|
||||
let targetDate: Date?
|
||||
let isActive: Bool
|
||||
let accountName: String?
|
||||
let categoryName: String?
|
||||
}
|
||||
|
||||
struct ImportedJournalEntry {
|
||||
@@ -386,8 +387,16 @@ Personal,Real Estate,Rental Property,2024-01-01,82000,80000,Estimated value
|
||||
let trimmedName = importedGoal.name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmedName.isEmpty, importedGoal.targetAmount != nil else { continue }
|
||||
let resolvedAccount = importedGoal.accountName.flatMap { accountsByName[$0] }
|
||||
if fetchGoal(named: trimmedName, account: resolvedAccount, in: context) == nil {
|
||||
let key = "\(trimmedName.lowercased())|\(resolvedAccount?.name ?? "")"
|
||||
let resolvedCategory = importedGoal.categoryName.flatMap {
|
||||
resolveExistingCategory(named: $0, lookup: categoryLookup)
|
||||
}
|
||||
if fetchGoal(
|
||||
named: trimmedName,
|
||||
account: resolvedAccount,
|
||||
category: resolvedCategory,
|
||||
in: context
|
||||
) == nil {
|
||||
let key = "\(trimmedName.lowercased())|\(resolvedAccount?.name ?? "")|\(resolvedCategory?.name ?? "")"
|
||||
if !plannedGoalKeys.contains(key) {
|
||||
plannedGoalKeys.insert(key)
|
||||
preview.goalsToCreate += 1
|
||||
@@ -666,12 +675,14 @@ Personal,Real Estate,Rental Property,2024-01-01,82000,80000,Estimated value
|
||||
.flatMap { ISO8601DateFormatter().date(from: $0) }
|
||||
let isActive = goalDict["isActive"] as? Bool ?? true
|
||||
let accountName = goalDict["account"] as? String
|
||||
let categoryName = goalDict["category"] as? String
|
||||
return ImportedGoal(
|
||||
name: name,
|
||||
targetAmount: targetAmount,
|
||||
targetDate: targetDate,
|
||||
isActive: isActive,
|
||||
accountName: accountName
|
||||
accountName: accountName,
|
||||
categoryName: categoryName
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -863,12 +874,21 @@ Personal,Real Estate,Rental Property,2024-01-01,82000,80000,Estimated value
|
||||
guard let targetAmount = importedGoal.targetAmount else { continue }
|
||||
|
||||
let resolvedAccount = importedGoal.accountName.flatMap { accountsByName[$0] }
|
||||
if fetchGoal(named: trimmedName, account: resolvedAccount, in: context) == nil {
|
||||
let resolvedCategory = importedGoal.categoryName.flatMap {
|
||||
resolveExistingCategory(named: $0, lookup: categoryLookup)
|
||||
}
|
||||
if fetchGoal(
|
||||
named: trimmedName,
|
||||
account: resolvedAccount,
|
||||
category: resolvedCategory,
|
||||
in: context
|
||||
) == nil {
|
||||
let goal = Goal(context: context)
|
||||
goal.name = trimmedName
|
||||
goal.targetAmount = NSDecimalNumber(decimal: targetAmount)
|
||||
goal.targetDate = importedGoal.targetDate
|
||||
goal.account = resolvedAccount
|
||||
goal.category = resolvedCategory
|
||||
goal.isActive = importedGoal.isActive
|
||||
}
|
||||
}
|
||||
@@ -1002,22 +1022,27 @@ Personal,Real Estate,Rental Property,2024-01-01,82000,80000,Estimated value
|
||||
return try? context.fetch(request).first
|
||||
}
|
||||
|
||||
/// Fetch an existing goal by name (case-insensitive) within the same account.
|
||||
/// Fetch an existing goal by name (case-insensitive) within the same scope
|
||||
/// (account + category).
|
||||
private func fetchGoal(
|
||||
named name: String,
|
||||
account: Account?,
|
||||
category: Category?,
|
||||
in context: NSManagedObjectContext
|
||||
) -> Goal? {
|
||||
let request: NSFetchRequest<Goal> = Goal.fetchRequest()
|
||||
var predicates: [NSPredicate] = [NSPredicate(format: "name ==[c] %@", name)]
|
||||
if let account = account {
|
||||
request.predicate = NSPredicate(
|
||||
format: "name ==[c] %@ AND account == %@", name, account
|
||||
)
|
||||
predicates.append(NSPredicate(format: "account == %@", account))
|
||||
} else {
|
||||
request.predicate = NSPredicate(
|
||||
format: "name ==[c] %@ AND account == nil", name
|
||||
)
|
||||
predicates.append(NSPredicate(format: "account == nil"))
|
||||
}
|
||||
if let category = category {
|
||||
predicates.append(NSPredicate(format: "category == %@", category))
|
||||
} else {
|
||||
predicates.append(NSPredicate(format: "category == nil"))
|
||||
}
|
||||
request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
|
||||
request.fetchLimit = 1
|
||||
return try? context.fetch(request).first
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user