Primera build enviada

This commit is contained in:
2026-01-19 14:40:43 +01:00
parent c6be398e5a
commit b03d35194f
36 changed files with 1641 additions and 561 deletions
@@ -30,6 +30,12 @@ public class Account: NSManagedObject, Identifiable {
}
}
// MARK: - Constants
extension Account {
static let defaultAccountName = "Default"
}
// MARK: - Computed Properties
extension Account {
@@ -50,5 +56,9 @@ extension Account {
var currencyCode: String? {
currency?.isEmpty == false ? currency : nil
}
var isDefaultAccount: Bool {
name == Account.defaultAccountName
}
}
@@ -69,13 +69,13 @@ extension Category {
extension Category {
static let defaultCategories: [(name: String, colorHex: String, icon: String)] = [
("Stocks", "#10B981", "chart.line.uptrend.xyaxis"),
("Bonds", "#3B82F6", "building.columns.fill"),
("Real Estate", "#F59E0B", "house.fill"),
("Crypto", "#8B5CF6", "bitcoinsign.circle.fill"),
("Cash", "#6B7280", "banknote.fill"),
("ETFs", "#EC4899", "chart.bar.fill"),
("Crypto", "#8B5CF6", "bitcoinsign.circle.fill"),
("Real Estate", "#F59E0B", "house.fill"),
("Bonds", "#3B82F6", "building.columns.fill"),
("Retirement", "#14B8A6", "person.fill"),
("Other", "#64748B", "ellipsis.circle.fill")
("Other", "#64748B", "ellipsis.circle.fill"),
("ETFs", "#EC4899", "chart.bar.fill")
]
static func createDefaultCategories(in context: NSManagedObjectContext) {
+53 -6
View File
@@ -179,6 +179,55 @@ class CoreDataStack: ObservableObject {
private init() {}
// MARK: - Cleanup Duplicates
/// Removes duplicate objects that have the same UUID, keeping only the oldest one.
/// This fixes data corruption from race conditions during object creation.
func cleanupDuplicateObjects() {
let context = viewContext
context.performAndWait {
// Clean up duplicate Goals
cleanupDuplicates(entityName: "Goal", idKey: "id", context: context)
// Clean up duplicate Accounts (already handled in AccountRepository but added here for safety)
cleanupDuplicates(entityName: "Account", idKey: "id", context: context)
// Clean up duplicate InvestmentSources
cleanupDuplicates(entityName: "InvestmentSource", idKey: "id", context: context)
// Clean up duplicate Categories
cleanupDuplicates(entityName: "Category", idKey: "id", context: context)
if context.hasChanges {
try? context.save()
}
}
}
private func cleanupDuplicates(entityName: String, idKey: String, context: NSManagedObjectContext) {
let request = NSFetchRequest<NSManagedObject>(entityName: entityName)
request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)]
guard let objects = try? context.fetch(request) else { return }
var seenIds = Set<UUID>()
var objectsToDelete: [NSManagedObject] = []
for object in objects {
guard let objectId = object.value(forKey: idKey) as? UUID else { continue }
if seenIds.contains(objectId) {
objectsToDelete.append(object)
} else {
seenIds.insert(objectId)
}
}
if !objectsToDelete.isEmpty {
print("Cleaning up \(objectsToDelete.count) duplicate \(entityName) objects")
for object in objectsToDelete {
context.delete(object)
}
}
}
// MARK: - Save Context
func save() {
@@ -232,12 +281,10 @@ class CoreDataStack: ObservableObject {
/// Forces SQLite to checkpoint the WAL file, ensuring all changes are written to the main database file.
/// This is necessary for the widget to see changes, as it opens the database read-only.
private func checkpointWAL() {
if viewContext.hasChanges {
try? viewContext.save()
}
let coordinator = persistentContainer.persistentStoreCoordinator
coordinator.performAndWait {
viewContext.performAndWait {
if viewContext.hasChanges {
try? viewContext.save()
}
viewContext.refreshAllObjects()
}
}
@@ -201,3 +201,49 @@ struct PortfolioSummary {
)
}
}
// MARK: - Portfolio Forecast
struct PortfolioForecast {
let currentValue: Decimal
let forecastValue: Decimal
let forecastDate: Date
let confidenceLower: Decimal
let confidenceUpper: Decimal
let monthlyGrowthRate: Decimal
let annualizedGrowthRate: Decimal
var formattedForecastValue: String {
CurrencyFormatter.format(forecastValue, style: .currency, maximumFractionDigits: 0)
}
var formattedCurrentValue: String {
CurrencyFormatter.format(currentValue, style: .currency, maximumFractionDigits: 0)
}
var formattedConfidenceRange: String {
let lower = CurrencyFormatter.format(confidenceLower, style: .currency, maximumFractionDigits: 0)
let upper = CurrencyFormatter.format(confidenceUpper, style: .currency, maximumFractionDigits: 0)
return "\(lower) \(upper)"
}
var formattedForecastDate: String {
forecastDate.monthYearString
}
var formattedGrowthPercentage: String {
guard currentValue > 0 else { return "+0%" }
let growthPct = ((forecastValue - currentValue) / currentValue) * 100
let prefix = growthPct >= 0 ? "+" : ""
return String(format: "\(prefix)%.1f%%", NSDecimalNumber(decimal: growthPct).doubleValue)
}
var formattedAnnualizedGrowthRate: String {
let prefix = annualizedGrowthRate >= 0 ? "+" : ""
return String(format: "\(prefix)%.1f%%", NSDecimalNumber(decimal: annualizedGrowthRate * 100).doubleValue)
}
var isPositiveGrowth: Bool {
forecastValue >= currentValue
}
}