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
@@ -83,14 +83,28 @@ class SettingsViewModel: ObservableObject {
currencyCode = settings.currency
inputMode = InputMode(rawValue: settings.inputMode) ?? .simple
// Load statistics
totalSources = sourceRepository.sourceCount
totalCategories = categoryRepository.categories.count
totalSnapshots = sourceRepository.sources.reduce(0) { $0 + $1.snapshotCount }
// Load statistics directly from database to avoid async race conditions
loadStatistics()
FirebaseService.shared.logScreenView(screenName: "Settings")
}
private func loadStatistics() {
let context = CoreDataStack.shared.viewContext
// Fetch source count directly
let sourceRequest: NSFetchRequest<InvestmentSource> = InvestmentSource.fetchRequest()
totalSources = (try? context.count(for: sourceRequest)) ?? 0
// Fetch category count directly
let categoryRequest: NSFetchRequest<Category> = Category.fetchRequest()
totalCategories = (try? context.count(for: categoryRequest)) ?? 0
// Fetch snapshot count directly
let snapshotRequest: NSFetchRequest<Snapshot> = Snapshot.fetchRequest()
totalSnapshots = (try? context.count(for: snapshotRequest)) ?? 0
}
// MARK: - Premium Actions
func upgradeToPremium() {
@@ -142,6 +156,16 @@ class SettingsViewModel: ObservableObject {
// MARK: - Export
@Published var isExporting = false
@Published var exportProgress: Double = 0
@Published var exportStatus = ""
@Published var shareItem: ShareItem?
struct ShareItem: Identifiable {
let id = UUID()
let url: URL
}
func exportData(format: ExportService.ExportFormat) {
guard freemiumValidator.canExport() else {
showingPaywall = true
@@ -149,19 +173,79 @@ class SettingsViewModel: ObservableObject {
return
}
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let viewController = windowScene.windows.first?.rootViewController else {
return
}
isExporting = true
exportProgress = 0
exportStatus = "Preparing export..."
ExportService.shared.share(
format: format,
sources: sourceRepository.sources,
categories: categoryRepository.categories,
from: viewController
)
Task {
// Fetch data directly from database
let context = CoreDataStack.shared.viewContext
let sources = fetchAllSources(in: context)
let categories = fetchAllCategories(in: context)
await MainActor.run {
exportProgress = 0.3
exportStatus = "Generating \(format.rawValue) file..."
}
let content: String
let fileName: String
switch format {
case .csv:
content = ExportService.shared.exportToCSV(sources: sources, categories: categories)
fileName = "investment_tracker_export.csv"
case .json:
content = ExportService.shared.exportToJSON(sources: sources, categories: categories)
fileName = "investment_tracker_export.json"
}
await MainActor.run {
exportProgress = 0.7
exportStatus = "Saving file..."
}
// Create temporary file
let tempURL = FileManager.default.temporaryDirectory
.appendingPathComponent(fileName)
do {
try content.write(to: tempURL, atomically: true, encoding: .utf8)
await MainActor.run {
exportProgress = 1.0
exportStatus = "Export complete"
isExporting = false
showingExportOptions = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.shareItem = ShareItem(url: tempURL)
}
FirebaseService.shared.logExportAttempt(format: format.rawValue, success: true)
}
} catch {
await MainActor.run {
isExporting = false
errorMessage = "Export failed: \(error.localizedDescription)"
FirebaseService.shared.logExportAttempt(format: format.rawValue, success: false)
}
}
}
}
private func fetchAllSources(in context: NSManagedObjectContext) -> [InvestmentSource] {
let request: NSFetchRequest<InvestmentSource> = InvestmentSource.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \InvestmentSource.name, ascending: true)]
return (try? context.fetch(request)) ?? []
}
private func fetchAllCategories(in context: NSManagedObjectContext) -> [Category] {
let request: NSFetchRequest<Category> = Category.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Category.name, ascending: true)]
return (try? context.fetch(request)) ?? []
}
// Share sheet is presented in SettingsView using shareItem.
var canExport: Bool {
freemiumValidator.canExport()
}
@@ -202,41 +286,79 @@ class SettingsViewModel: ObservableObject {
// MARK: - Data Management
func resetAllData() {
isLoading = true
errorMessage = nil
let context = CoreDataStack.shared.viewContext
let entityNames = [
"Snapshot",
"InvestmentSource",
"Category",
"Asset",
"Account",
"Goal",
"Transaction",
"PredictionCache"
]
// Delete all snapshots, sources, and categories
for source in sourceRepository.sources {
context.delete(source)
}
for category in categoryRepository.categories {
context.delete(category)
}
let assetRequest: NSFetchRequest<Asset> = Asset.fetchRequest()
let accountRequest: NSFetchRequest<Account> = Account.fetchRequest()
let goalRequest: NSFetchRequest<Goal> = Goal.fetchRequest()
if let assets = try? context.fetch(assetRequest) {
assets.forEach { context.delete($0) }
}
if let accounts = try? context.fetch(accountRequest) {
accounts.forEach { context.delete($0) }
}
if let goals = try? context.fetch(goalRequest) {
goals.forEach { context.delete($0) }
context.performAndWait {
var deletedObjectIDs: [NSManagedObjectID] = []
for name in entityNames {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: name)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs
if let result = try? context.execute(deleteRequest) as? NSBatchDeleteResult,
let objectIDs = result.result as? [NSManagedObjectID] {
deletedObjectIDs.append(contentsOf: objectIDs)
}
}
if !deletedObjectIDs.isEmpty {
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: [NSDeletedObjectsKey: deletedObjectIDs],
into: [context]
)
}
}
CoreDataStack.shared.save()
context.reset()
// Clear notifications
context.performAndWait {
// Recreate default categories
let categoryCount = (try? context.count(for: Category.fetchRequest())) ?? 0
if categoryCount == 0 {
Category.createDefaultCategories(in: context)
}
// Recreate default account
let accountCount = (try? context.count(for: Account.fetchRequest())) ?? 0
if accountCount == 0 {
let defaultCurrency = AppSettings.getOrCreate(in: context).currency
let account = Account(context: context)
account.name = Account.defaultAccountName
account.currency = defaultCurrency
account.inputMode = InputMode.simple.rawValue
account.notificationFrequency = NotificationFrequency.monthly.rawValue
account.customFrequencyMonths = 1
account.sortOrder = 0
}
let settings = AppSettings.getOrCreate(in: context)
settings.selectedAccountId = nil
settings.showAllAccounts = true
try? context.save()
}
// Clear notifications + monthly check-ins
notificationService.cancelAllReminders()
MonthlyCheckInStore.clearAll()
CoreDataStack.shared.refreshWidgetData()
// Recreate default categories
categoryRepository.createDefaultCategoriesIfNeeded()
_ = AccountRepository().createDefaultAccountIfNeeded()
// Reload data
loadSettings()
isLoading = false
successMessage = "All data has been reset"
NotificationCenter.default.post(name: .didResetData, object: nil)
}
// MARK: - Computed Properties