diff --git a/Coinly/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/UserInterfaceState.xcuserstate b/Coinly/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/UserInterfaceState.xcuserstate index 62271ff..87bf53e 100644 Binary files a/Coinly/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/UserInterfaceState.xcuserstate and b/Coinly/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Coinly/Coinly/App/CoinlyApp.swift b/Coinly/Coinly/App/CoinlyApp.swift index 574d930..511c816 100644 --- a/Coinly/Coinly/App/CoinlyApp.swift +++ b/Coinly/Coinly/App/CoinlyApp.swift @@ -1,20 +1,15 @@ -// -// CoinlyApp.swift -// Coinly -// -// Created by Vadym Samoilenko on 03/03/2025. -// - import SwiftUI @main struct CoinlyApp: App { let persistenceController = PersistenceController.shared - + var body: some Scene { WindowGroup { - ContentView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) + NavigationView { + ContentView() + } + .environment(\.managedObjectContext, persistenceController.container.viewContext) } } } diff --git a/Coinly/Coinly/Assets.xcassets/.DS_Store b/Coinly/Coinly/Assets.xcassets/.DS_Store new file mode 100644 index 0000000..7b1cda7 Binary files /dev/null and b/Coinly/Coinly/Assets.xcassets/.DS_Store differ diff --git a/Coinly/Coinly/Assets.xcassets/AppIcon.appiconset/Contents.json b/Coinly/Coinly/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..3817ef4 100644 --- a/Coinly/Coinly/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Coinly/Coinly/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "image_fx_-2.jpg", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/Coinly/Coinly/Assets.xcassets/AppIcon.appiconset/image_fx_-2.jpg b/Coinly/Coinly/Assets.xcassets/AppIcon.appiconset/image_fx_-2.jpg new file mode 100644 index 0000000..4bf7d2a Binary files /dev/null and b/Coinly/Coinly/Assets.xcassets/AppIcon.appiconset/image_fx_-2.jpg differ diff --git a/Coinly/Coinly/Assets.xcassets/Colors/.DS_Store b/Coinly/Coinly/Assets.xcassets/Colors/.DS_Store new file mode 100644 index 0000000..e8b0c79 Binary files /dev/null and b/Coinly/Coinly/Assets.xcassets/Colors/.DS_Store differ diff --git a/Coinly/Coinly/Assets.xcassets/Colors/AccentGreen.colorset/Contents.json b/Coinly/Coinly/Assets.xcassets/Colors/AccentGreen.colorset/Contents.json new file mode 100644 index 0000000..d3d8f6f --- /dev/null +++ b/Coinly/Coinly/Assets.xcassets/Colors/AccentGreen.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "89", + "green" : "199", + "red" : "52" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "89", + "green" : "199", + "red" : "52" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "88", + "green" : "209", + "red" : "48" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Coinly/Coinly/Assets.xcassets/Colors/AccentRed.colorset/Contents.json b/Coinly/Coinly/Assets.xcassets/Colors/AccentRed.colorset/Contents.json new file mode 100644 index 0000000..2e4d745 --- /dev/null +++ b/Coinly/Coinly/Assets.xcassets/Colors/AccentRed.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "48", + "green" : "59", + "red" : "255" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "48", + "green" : "59", + "red" : "255" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "58", + "green" : "69", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Coinly/Coinly/Assets.xcassets/Colors/CardBackground.colorset/Contents.json b/Coinly/Coinly/Assets.xcassets/Colors/CardBackground.colorset/Contents.json new file mode 100644 index 0000000..2fbbaf8 --- /dev/null +++ b/Coinly/Coinly/Assets.xcassets/Colors/CardBackground.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.950", + "blue" : "30", + "green" : "28", + "red" : "28" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Coinly/Coinly/Assets.xcassets/Colors/CardShadow.colorset/Contents.json b/Coinly/Coinly/Assets.xcassets/Colors/CardShadow.colorset/Contents.json new file mode 100644 index 0000000..d5853bf --- /dev/null +++ b/Coinly/Coinly/Assets.xcassets/Colors/CardShadow.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0", + "green" : "0", + "red" : "0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0", + "green" : "0", + "red" : "0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.200", + "blue" : "0", + "green" : "0", + "red" : "0" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Coinly/Coinly/Assets.xcassets/Colors/Contents.json b/Coinly/Coinly/Assets.xcassets/Colors/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Coinly/Coinly/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Coinly/Coinly/Assets.xcassets/Colors/SecondaryBackground.colorset/Contents.json b/Coinly/Coinly/Assets.xcassets/Colors/SecondaryBackground.colorset/Contents.json new file mode 100644 index 0000000..b1801d4 --- /dev/null +++ b/Coinly/Coinly/Assets.xcassets/Colors/SecondaryBackground.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "247", + "green" : "242", + "red" : "242" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "247", + "green" : "242", + "red" : "242" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "30", + "green" : "28", + "red" : "28" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Coinly/Coinly/Common/Theme/Theme.swift b/Coinly/Coinly/Common/Theme/Theme.swift new file mode 100644 index 0000000..e62fb25 --- /dev/null +++ b/Coinly/Coinly/Common/Theme/Theme.swift @@ -0,0 +1,41 @@ +// +// Theme.swift +// Coinly +// +// Created by Vadym Samoilenko on 03/03/2025. +// + + +import SwiftUI + +enum Theme { + // Основные цвета + static let background = Color(.systemGroupedBackground) + static let secondaryBackground = Color(.secondarySystemGroupedBackground) + + // Цвета для карточек + static let cardBackground = Color(.systemBackground) + static let cardShadow = Color.black.opacity(0.05) + + // Акцентные цвета + static let primary = Color.accentColor + static let positive = Color("AccentGreen", bundle: nil) // Fallback to system green + static let negative = Color("AccentRed", bundle: nil) // Fallback to system red + + // Цвета для типов счетов + static let debitCard = Color.blue + static let credit = Color.purple + static let cash = Color.green + static let savings = Color.mint + + // Цвета текста + static let primaryText = Color(.label) + static let secondaryText = Color(.secondaryLabel) + + // Градиенты + static let primaryGradient = LinearGradient( + colors: [.blue, .blue.opacity(0.8)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) +} \ No newline at end of file diff --git a/Coinly/Coinly/Core/Data/CoreData/AccountEntity+Extensions.swift b/Coinly/Coinly/Core/Data/CoreData/AccountEntity+Extensions.swift new file mode 100644 index 0000000..f357b31 --- /dev/null +++ b/Coinly/Coinly/Core/Data/CoreData/AccountEntity+Extensions.swift @@ -0,0 +1,45 @@ +import CoreData + +extension AccountEntity { + // Convert CoreData entity to domain model + var toDomain: Account { + Account( + id: id ?? UUID(), + name: name ?? "", + type: AccountType(rawValue: typeRawValue ?? "") ?? .debitCard, + currency: Currency.all.first { $0.code == currencyCode } ?? Currency.current(), + balance: (balance as? Decimal) ?? 0, + initialBalance: (initialBalance as? Decimal) ?? 0, + creditLimit: creditLimit as? Decimal, + interestRate: interestRate as? Decimal, + icon: icon, + notes: notes, + isArchived: isArchived, + excludeFromStatistics: excludeFromStatistics, + createdAt: createdAt ?? Date(), + updatedAt: updatedAt ?? Date() + ) + } + + // Update entity from domain model + func update(from account: Account, context: NSManagedObjectContext) { + self.id = account.id + self.name = account.name + self.typeRawValue = account.type.rawValue + self.currencyCode = account.currency.code + self.balance = NSDecimalNumber(decimal: account.balance) + self.initialBalance = NSDecimalNumber(decimal: account.initialBalance) + if let creditLimit = account.creditLimit { + self.creditLimit = NSDecimalNumber(decimal: creditLimit) + } + if let interestRate = account.interestRate { + self.interestRate = NSDecimalNumber(decimal: interestRate) + } + self.icon = account.icon + self.notes = account.notes + self.isArchived = account.isArchived + self.excludeFromStatistics = account.excludeFromStatistics + self.createdAt = account.createdAt + self.updatedAt = account.updatedAt + } +} diff --git a/Coinly/Coinly/Core/Data/CoreData/Coinly.xcdatamodeld/Coinly.xcdatamodel/contents b/Coinly/Coinly/Core/Data/CoreData/Coinly.xcdatamodeld/Coinly.xcdatamodel/contents index 9ed2921..4dde64c 100644 --- a/Coinly/Coinly/Core/Data/CoreData/Coinly.xcdatamodeld/Coinly.xcdatamodel/contents +++ b/Coinly/Coinly/Core/Data/CoreData/Coinly.xcdatamodeld/Coinly.xcdatamodel/contents @@ -1,9 +1,19 @@ - - - + + + + + + + + + + + + + + + + - - - \ No newline at end of file diff --git a/Coinly/Coinly/Core/Data/CoreData/CoreDataStack.swift b/Coinly/Coinly/Core/Data/CoreData/CoreDataStack.swift new file mode 100644 index 0000000..da3a953 --- /dev/null +++ b/Coinly/Coinly/Core/Data/CoreData/CoreDataStack.swift @@ -0,0 +1,42 @@ +// +// CoreDataStack.swift +// Coinly +// +// Created by Vadym Samoilenko on 03/03/2025. +// + + +import CoreData + +final class CoreDataStack { + static let shared = CoreDataStack() + + private init() {} + + lazy var persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: "Coinly") + container.loadPersistentStores { description, error in + if let error = error { + fatalError("Unable to load persistent stores: \(error)") + } + } + container.viewContext.automaticallyMergesChangesFromParent = true + return container + }() + + var viewContext: NSManagedObjectContext { + persistentContainer.viewContext + } + + func saveContext() { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } +} \ No newline at end of file diff --git a/Coinly/Coinly/Core/Data/CoreData/Persistence.swift b/Coinly/Coinly/Core/Data/CoreData/Persistence.swift index c6fb682..648a00c 100644 --- a/Coinly/Coinly/Core/Data/CoreData/Persistence.swift +++ b/Coinly/Coinly/Core/Data/CoreData/Persistence.swift @@ -1,57 +1,51 @@ -// -// Persistence.swift -// Coinly -// -// Created by Vadym Samoilenko on 03/03/2025. -// - import CoreData struct PersistenceController { static let shared = PersistenceController() - - @MainActor - static let preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - for _ in 0..<10 { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() - } + + // MARK: - Preview Support + static var preview: PersistenceController = { + let controller = PersistenceController(inMemory: true) + let viewContext = controller.container.viewContext + + // Create sample data + let account = AccountEntity(context: viewContext) + account.id = UUID() + account.name = "Sample Account" + account.typeRawValue = AccountType.debitCard.rawValue // Изменили с checking на debitCard + account.currencyCode = Currency.current().code + account.balance = NSDecimalNumber(decimal: Decimal(1000)) + account.initialBalance = NSDecimalNumber(decimal: Decimal(1000)) + account.createdAt = Date() + account.updatedAt = Date() + account.isArchived = false + account.excludeFromStatistics = false + do { try viewContext.save() } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. let nsError = error as NSError fatalError("Unresolved error \(nsError), \(nsError.userInfo)") } - return result + + return controller }() - + let container: NSPersistentContainer - + init(inMemory: Bool = false) { container = NSPersistentContainer(name: "Coinly") + if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } - container.loadPersistentStores(completionHandler: { (storeDescription, error) in + + container.loadPersistentStores { (storeDescription, error) in if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ fatalError("Unresolved error \(error), \(error.userInfo)") } - }) + } + container.viewContext.automaticallyMergesChangesFromParent = true } } diff --git a/Coinly/Coinly/Core/Data/Repositories/AccountRepository.swift b/Coinly/Coinly/Core/Data/Repositories/AccountRepository.swift new file mode 100644 index 0000000..3b3cd1c --- /dev/null +++ b/Coinly/Coinly/Core/Data/Repositories/AccountRepository.swift @@ -0,0 +1,63 @@ +import CoreData +import Combine + +protocol AccountRepositoryProtocol { + func getAccounts() async throws -> [Account] + func getAccount(by id: UUID) async throws -> Account? + func save(_ account: Account) async throws + func delete(_ account: Account) async throws +} + +final class AccountRepository: AccountRepositoryProtocol { + private let context: NSManagedObjectContext + + init(context: NSManagedObjectContext = PersistenceController.shared.container.viewContext) { + self.context = context + } + + func getAccounts() async throws -> [Account] { + let request = AccountEntity.fetchRequest() + request.sortDescriptors = [ + NSSortDescriptor(keyPath: \AccountEntity.name, ascending: true) + ] + + let entities = try context.fetch(request) + print("Fetched entities: \(entities.count)") // Debug print + return entities.map { $0.toDomain } + } + + func getAccount(by id: UUID) async throws -> Account? { + let request = AccountEntity.fetchRequest() + request.predicate = NSPredicate(format: "id == %@", id as CVarArg) + request.fetchLimit = 1 + + let entities = try context.fetch(request) + return entities.first?.toDomain + } + + func save(_ account: Account) async throws { + let request = AccountEntity.fetchRequest() + request.predicate = NSPredicate(format: "id == %@", account.id as CVarArg) + request.fetchLimit = 1 + + let entities = try context.fetch(request) + let entity = entities.first ?? AccountEntity(context: context) + entity.update(from: account, context: context) + + try context.save() + print("Saved account: \(account.name)") // Debug print + } + + func delete(_ account: Account) async throws { + let request = AccountEntity.fetchRequest() + request.predicate = NSPredicate(format: "id == %@", account.id as CVarArg) + request.fetchLimit = 1 + + let entities = try context.fetch(request) + if let entity = entities.first { + context.delete(entity) + try context.save() + print("Deleted account: \(account.name)") // Debug print + } + } +} diff --git a/Coinly/Coinly/Core/Domain/Models/Account.swift b/Coinly/Coinly/Core/Domain/Models/Account.swift index c160904..2e2e101 100644 --- a/Coinly/Coinly/Core/Domain/Models/Account.swift +++ b/Coinly/Coinly/Core/Domain/Models/Account.swift @@ -149,7 +149,7 @@ extension Account { ), Account( name: "Main Bank Account", - type: .checking, + type: .debitCard, currency: .preview, balance: 5000, icon: "building.columns" diff --git a/Coinly/Coinly/Core/Domain/Models/AccountType.swift b/Coinly/Coinly/Core/Domain/Models/AccountType.swift index 88bb3d9..43b36c2 100644 --- a/Coinly/Coinly/Core/Domain/Models/AccountType.swift +++ b/Coinly/Coinly/Core/Domain/Models/AccountType.swift @@ -3,7 +3,7 @@ import Foundation enum AccountType: String, CaseIterable, Identifiable, Codable { // MARK: - Cases case cash = "Cash" // Наличные - case checking = "Checking" // Текущий счет + case debitCard = "Debit Card" // Дебетовая карта case savings = "Savings" // Сберегательный счет case investmentAccount = "Investment" // Инвестиционный счет case credit = "Credit" // Кредитная карта @@ -19,21 +19,21 @@ enum AccountType: String, CaseIterable, Identifiable, Codable { var icon: String { switch self { case .cash: return "banknote" - case .checking: return "building.columns" - case .savings: return "sparkles" + case .debitCard: return "creditcard" + case .savings: return "building.columns.fill" case .investmentAccount: return "chart.line.uptrend.xyaxis" - case .credit: return "creditcard" - case .loan: return "handshake" - case .deposit: return "safe" - case .debtToMe: return "arrow.left.circle" - case .myDebt: return "arrow.right.circle" + case .credit: return "creditcard.fill" + case .loan: return "dollarsign.circle.fill" + case .deposit: return "vault.fill" + case .debtToMe: return "arrow.left.circle.fill" + case .myDebt: return "arrow.right.circle.fill" } } var color: String { switch self { case .cash: return "green" - case .checking: return "blue" + case .debitCard: return "blue" case .savings: return "purple" case .investmentAccount: return "orange" case .credit: return "red" @@ -43,6 +43,7 @@ enum AccountType: String, CaseIterable, Identifiable, Codable { case .myDebt: return "brown" } } + // MARK: - Business Logic var allowsNegativeBalance: Bool { @@ -56,7 +57,7 @@ enum AccountType: String, CaseIterable, Identifiable, Codable { var requiresInterestRate: Bool { switch self { - case .credit, .loan, .deposit: + case .credit, .loan: return true default: return false @@ -83,7 +84,7 @@ enum AccountType: String, CaseIterable, Identifiable, Codable { // MARK: - Grouping static var basicAccounts: [AccountType] { - [.cash, .checking, .savings] + [.cash, .debitCard, .savings] } static var investmentAccounts: [AccountType] { @@ -103,8 +104,8 @@ enum AccountType: String, CaseIterable, Identifiable, Codable { switch self { case .cash: return NSLocalizedString("Physical money in your wallet", comment: "Cash description") - case .checking: - return NSLocalizedString("Everyday banking account", comment: "Checking description") + case .debitCard: + return NSLocalizedString("Your main bank card", comment: "Debit card description") case .savings: return NSLocalizedString("Account for saving money", comment: "Savings description") case .investmentAccount: @@ -131,7 +132,7 @@ extension AccountType { } static var preview: AccountType { - .checking + .debitCard } } #endif diff --git a/Coinly/Coinly/Core/Domain/Models/Currency.swift b/Coinly/Coinly/Core/Domain/Models/Currency.swift index 8963cfc..c4625ac 100644 --- a/Coinly/Coinly/Core/Domain/Models/Currency.swift +++ b/Coinly/Coinly/Core/Domain/Models/Currency.swift @@ -40,16 +40,18 @@ extension Currency { extension Currency { /// Получение валюты для текущего региона или GBP по умолчанию static func current(for locale: Locale = .current) -> Currency { + print("Getting currency for locale: \(locale.identifier)") + + // Пробуем получить валюту из локали if let currencyCode = locale.currency?.identifier, let currency = all.first(where: { $0.code == currencyCode }) { + print("Found local currency: \(currencyCode)") return currency } - return defaultCurrency - } - - /// Британский фунт как валюта по умолчанию - static var defaultCurrency: Currency { - Currency( + + // Если не удалось определить, возвращаем GBP как дефолтную + print("Using default GBP currency") + return Currency( code: "GBP", numericCode: "826", name: "British Pound Sterling", @@ -64,9 +66,10 @@ extension Currency { let locales = Locale.availableIdentifiers.map(Locale.init) return Locale.Currency.isoCurrencies.compactMap { currency in - guard let locale = locales.first(where: { $0.currency?.identifier == currency.identifier }) else { return nil } + guard let locale = locales.first(where: { $0.currency?.identifier == currency.identifier }), + let name = locale.localizedString(forCurrencyCode: currency.identifier) + else { return nil } - let name = locale.localizedString(forCurrencyCode: currency.identifier) ?? currency.identifier let symbol = locale.currencySymbol ?? currency.identifier return Currency( @@ -77,7 +80,8 @@ extension Currency { fractionDigits: Currency.getFractionDigits(for: currency.identifier), minorUnit: Currency.getMinorUnit(for: currency.identifier) ) - }.sorted { $0.code < $1.code } + } + .sorted { $0.code < $1.code } } private static func getFractionDigits(for currencyCode: String) -> Int { @@ -102,16 +106,17 @@ extension Currency { extension Currency { static var sampleData: [Currency] { [ - defaultCurrency, + Currency(code: "GBP", numericCode: "826", name: "British Pound Sterling", symbol: "£", fractionDigits: 2, minorUnit: 100), Currency(code: "USD", numericCode: "840", name: "US Dollar", symbol: "$", fractionDigits: 2, minorUnit: 100), Currency(code: "EUR", numericCode: "978", name: "Euro", symbol: "€", fractionDigits: 2, minorUnit: 100), Currency(code: "UAH", numericCode: "980", name: "Hryvnia", symbol: "₴", fractionDigits: 2, minorUnit: 100), - Currency(code: "JPY", numericCode: "392", name: "Yen", symbol: "¥", fractionDigits: 0, minorUnit: 1) + Currency(code: "JPY", numericCode: "392", name: "Japanese Yen", symbol: "¥", fractionDigits: 0, minorUnit: 1) ] } static var preview: Currency { - defaultCurrency + // GBP как дефолтная валюта для превью + sampleData[0] } } #endif diff --git a/Coinly/Coinly/Features/Accounts/ViewModels/AccountViewModel.swift b/Coinly/Coinly/Features/Accounts/ViewModels/AccountViewModel.swift index 399afb6..fedc5d4 100644 --- a/Coinly/Coinly/Features/Accounts/ViewModels/AccountViewModel.swift +++ b/Coinly/Coinly/Features/Accounts/ViewModels/AccountViewModel.swift @@ -1,21 +1,18 @@ -// -// AccountViewModel.swift -// Coinly -// -// Created by Vadym Samoilenko on 03/03/2025. -// - - import Foundation import Combine @MainActor final class AccountViewModel: ObservableObject { - // MARK: - Published Properties @Published private(set) var accounts: [Account] = [] @Published private(set) var isLoading = false @Published private(set) var error: Error? + private let repository: AccountRepositoryProtocol + + init(repository: AccountRepositoryProtocol = AccountRepository()) { + self.repository = repository + } + // MARK: - Computed Properties var activeAccounts: [Account] { accounts.filter { !$0.isArchived } @@ -33,7 +30,61 @@ final class AccountViewModel: ObservableObject { Dictionary(grouping: activeAccounts) { $0.currency } } - // MARK: - Total Balance Calculations + // MARK: - Account Operations + func loadAccounts() async { + isLoading = true + error = nil + + do { + accounts = try await repository.getAccounts() + print("Loaded accounts: \(accounts.count)") // Debug print + } catch { + self.error = error + print("Error loading accounts: \(error)") // Debug print + } + + isLoading = false + } + + func addAccount(_ account: Account) async throws { + try await repository.save(account) + await loadAccounts() + print("Added new account: \(account.name)") // Debug print + } + + func updateAccount(_ account: Account) async throws { + try await repository.save(account) + await loadAccounts() + print("Updated account: \(account.name)") // Debug print + } + + func deleteAccount(_ account: Account) async throws { + try await repository.delete(account) + await loadAccounts() + print("Deleted account: \(account.name)") // Debug print + } + + func archiveAccount(_ account: Account) async throws { + var updatedAccount = account + updatedAccount.archive() + try await updateAccount(updatedAccount) + } + + func unarchiveAccount(_ account: Account) async throws { + var updatedAccount = account + updatedAccount.unarchive() + try await updateAccount(updatedAccount) + } + + // MARK: - Filtering & Calculations + func accounts(for type: AccountType) -> [Account] { + activeAccounts.filter { $0.type == type } + } + + func accounts(for currency: Currency) -> [Account] { + activeAccounts.filter { $0.currency == currency } + } + func totalBalance(for currency: Currency) -> Decimal { activeAccounts .filter { $0.currency == currency && !$0.excludeFromStatistics } @@ -50,81 +101,8 @@ final class AccountViewModel: ObservableObject { .reduce(0) { $0 + $1.balance } } - // MARK: - Account Operations - func addAccount(_ account: Account) async throws { - // TODO: Implement repository call - accounts.append(account) - } - - func updateAccount(_ account: Account) async throws { - guard let index = accounts.firstIndex(where: { $0.id == account.id }) else { - throw AccountError.accountNotFound - } - accounts[index] = account - // TODO: Implement repository call - } - - func deleteAccount(_ account: Account) async throws { - accounts.removeAll { $0.id == account.id } - // TODO: Implement repository call - } - - func archiveAccount(_ account: Account) async throws { - var updatedAccount = account - updatedAccount.archive() - try await updateAccount(updatedAccount) - } - - func unarchiveAccount(_ account: Account) async throws { - var updatedAccount = account - updatedAccount.unarchive() - try await updateAccount(updatedAccount) - } - - // MARK: - Data Loading - func loadAccounts() async { - isLoading = true - error = nil - - do { - // TODO: Implement repository call - // Temporary using sample data - accounts = Account.sampleData - } catch { - self.error = error - } - - isLoading = false - } - - // MARK: - Filtering - func accounts(for type: AccountType) -> [Account] { - activeAccounts.filter { $0.type == type } - } - - func accounts(for currency: Currency) -> [Account] { - activeAccounts.filter { $0.currency == currency } - } -} - -// MARK: - Errors -enum AccountError: LocalizedError { - case accountNotFound - case invalidAmount - case insufficientFunds - case exceedsCreditLimit - - var errorDescription: String? { - switch self { - case .accountNotFound: - return NSLocalizedString("Account not found", comment: "") - case .invalidAmount: - return NSLocalizedString("Invalid amount", comment: "") - case .insufficientFunds: - return NSLocalizedString("Insufficient funds", comment: "") - case .exceedsCreditLimit: - return NSLocalizedString("Exceeds credit limit", comment: "") - } + func totalDebtFormatted(currency: Currency) -> String { + currency.format(totalDebt(for: currency)) } } @@ -137,4 +115,4 @@ extension AccountViewModel { return viewModel } } -#endif \ No newline at end of file +#endif diff --git a/Coinly/Coinly/Features/Accounts/Views/AccountDetailView.swift b/Coinly/Coinly/Features/Accounts/Views/AccountDetailView.swift new file mode 100644 index 0000000..d95253d --- /dev/null +++ b/Coinly/Coinly/Features/Accounts/Views/AccountDetailView.swift @@ -0,0 +1,203 @@ +// +// AccountDetailView.swift +// Coinly +// +// Created by Vadym Samoilenko on 03/03/2025. +// + + +import SwiftUI + +struct AccountDetailView: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var viewModel = AccountViewModel() + @State private var showingEditSheet = false + @State private var showingDeleteAlert = false + @State private var showingArchiveAlert = false + + let account: Account + + var body: some View { + List { + // Balance Section + Section { + VStack(spacing: 8) { + Text(account.formattedBalance) + .font(.system(.title, design: .rounded)) + .foregroundColor(balanceColor) + + Text("Current Balance") + .font(.subheadline) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + + if let creditLimit = account.creditLimit { + HStack { + Text("Credit Limit") + Spacer() + Text(account.currency.format(creditLimit)) + } + + HStack { + Text("Available Credit") + Spacer() + Text(account.formattedAvailableBalance) + .foregroundColor(account.availableBalance > 0 ? .green : .red) + } + } + + if let interestRate = account.interestRate { + HStack { + Text("Interest Rate") + Spacer() + Text(formatInterestRate(interestRate)) + } + } + } + + // Account Details Section + Section("Account Details") { + DetailRow(title: "Name", value: account.name) + DetailRow(title: "Type", value: account.type.localizedName) + DetailRow(title: "Currency", value: account.currency.code) + + if let notes = account.notes { + DetailRow(title: "Notes", value: notes) + } + + if account.excludeFromStatistics { + Text("Excluded from Statistics") + .foregroundColor(.secondary) + } + } + + // Statistics Section + Section("Statistics") { + DetailRow( + title: "Initial Balance", + value: account.currency.format(account.initialBalance) + ) + + DetailRow( + title: "Created", + value: formatDate(account.createdAt) + ) + + DetailRow( + title: "Last Modified", + value: formatDate(account.updatedAt) + ) + } + + // Actions Section + Section { + Button(action: { showingEditSheet = true }) { + Label("Edit Account", systemImage: "pencil") + } + + Button(action: { showingArchiveAlert = true }) { + Label( + account.isArchived ? "Unarchive Account" : "Archive Account", + systemImage: account.isArchived ? "archivebox.circle" : "archivebox" + ) + } + + Button(role: .destructive, action: { showingDeleteAlert = true }) { + Label("Delete Account", systemImage: "trash") + } + } + } + .navigationTitle(account.name) + .navigationBarTitleDisplayMode(.inline) + .sheet(isPresented: $showingEditSheet) { + AccountFormView(editingAccount: account) + } + .alert("Delete Account", isPresented: $showingDeleteAlert) { + Button("Cancel", role: .cancel) { } + Button("Delete", role: .destructive) { deleteAccount() } + } message: { + Text("Are you sure you want to delete this account? This action cannot be undone.") + } + .alert(account.isArchived ? "Unarchive Account" : "Archive Account", + isPresented: $showingArchiveAlert) { + Button("Cancel", role: .cancel) { } + Button(account.isArchived ? "Unarchive" : "Archive") { toggleArchive() } + } message: { + Text(account.isArchived ? + "Do you want to unarchive this account?" : + "Archived accounts are hidden from the main view. You can unarchive them later.") + } + } + + // MARK: - Computed Properties + private var balanceColor: Color { + if account.isOverdrawn { + return .red + } + return account.balance >= 0 ? .primary : .red + } + + // MARK: - Actions + private func deleteAccount() { + Task { + try? await viewModel.deleteAccount(account) + dismiss() + } + } + + private func toggleArchive() { + Task { + if account.isArchived { + try? await viewModel.unarchiveAccount(account) + } else { + try? await viewModel.archiveAccount(account) + } + dismiss() + } + } + + // MARK: - Helper Methods + private func formatInterestRate(_ rate: Decimal) -> String { + let percentage = rate * 100 + return String(format: "%.1f%%", NSDecimalNumber(decimal: percentage).doubleValue) + } + + private func formatDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + return formatter.string(from: date) + } +} + +// MARK: - Supporting Views +private struct DetailRow: View { + let title: String + let value: String + + var body: some View { + HStack { + Text(title) + Spacer() + Text(value) + .foregroundColor(.secondary) + } + } +} + +// MARK: - Preview +#if DEBUG +struct AccountDetailView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + AccountDetailView(account: .preview) + } + + NavigationView { + AccountDetailView(account: Account.sampleData[3]) // Credit card preview + } + } +} +#endif \ No newline at end of file diff --git a/Coinly/Coinly/Features/Accounts/Views/AccountFormView.swift b/Coinly/Coinly/Features/Accounts/Views/AccountFormView.swift new file mode 100644 index 0000000..a82acad --- /dev/null +++ b/Coinly/Coinly/Features/Accounts/Views/AccountFormView.swift @@ -0,0 +1,247 @@ +import SwiftUI + +struct AccountFormView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.managedObjectContext) private var viewContext + @StateObject private var viewModel = AccountViewModel() + + // Form State + @State private var name = "" + @State private var type = AccountType.debitCard + @State private var currency = Currency.current() + @State private var balance: Decimal? = 0 + @State private var creditLimit: Decimal? = nil + @State private var interestRate: Decimal? = nil + @State private var notes = "" + @State private var icon: String? = nil + @State private var excludeFromStatistics = false + + // Validation + @State private var showingErrors = false + @State private var errorMessage = "" + + // Edit Mode + var editingAccount: Account? + + var body: some View { + NavigationView { + Form { + // Basic Information + Section("Basic Information") { + TextField("Account Name", text: $name) + .textContentType(.name) + + Picker("Account Type", selection: $type) { + ForEach(AccountType.allCases) { type in + Label( + type.localizedName, + systemImage: type.icon + ).tag(type) + } + } + .onChange(of: type) { newType in + // Reset specific fields when type changes + if !newType.requiresInterestRate { + interestRate = nil + } + if newType != .credit { + creditLimit = nil + } + } + + Picker("Currency", selection: $currency) { + ForEach(Currency.all) { currency in + Text("\(currency.code) - \(currency.localizedName())") + .tag(currency) + } + } + } + + // Balance Section + Section("Balance") { + HStack { + Text("Initial Balance") + Spacer() + TextField("0", + value: Binding( + get: { balance ?? 0 }, + set: { balance = $0 } + ), + format: .currency(code: currency.code)) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + } + + if type == .credit { + HStack { + Text("Credit Limit") + Spacer() + TextField("0", + value: Binding( + get: { creditLimit ?? 0 }, + set: { creditLimit = $0 } + ), + format: .currency(code: currency.code)) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + } + } + + if type.requiresInterestRate { + HStack { + Text("Interest Rate") + Spacer() + TextField("0%", + value: Binding( + get: { interestRate ?? 0 }, + set: { interestRate = $0 } + ), + format: .percent) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + } + } + } + + // Additional Settings + Section { + Toggle("Exclude from Statistics", isOn: $excludeFromStatistics) + + TextField("Notes", text: $notes, axis: .vertical) + .lineLimit(3) + } + + if showingErrors { + Section { + Text(errorMessage) + .foregroundColor(Theme.negative) + } + } + } + .navigationTitle(editingAccount == nil ? "New Account" : "Edit Account") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Save") { + save() + } + } + } + .onAppear { + if let account = editingAccount { + loadAccount(account) + } else { + // При создании нового счета устанавливаем валюту по умолчанию + currency = getDefaultCurrency() + } + } + } + } + + // MARK: - Actions + private func save() { + guard validate() else { return } + + print("Saving account: \(name)") + print("Balance: \(balance ?? 0)") + print("Currency: \(currency.code)") + + let account = Account( + id: editingAccount?.id ?? UUID(), + name: name, + type: type, + currency: currency, + balance: balance ?? 0, + initialBalance: balance ?? 0, + creditLimit: type == .credit ? creditLimit : nil, + interestRate: type.requiresInterestRate ? interestRate : nil, + icon: icon, + notes: notes.isEmpty ? nil : notes, + excludeFromStatistics: excludeFromStatistics, + createdAt: editingAccount?.createdAt ?? Date(), + updatedAt: Date() + ) + + Task { + do { + if editingAccount != nil { + try await viewModel.updateAccount(account) + } else { + try await viewModel.addAccount(account) + } + print("Account saved successfully: \(account.name)") + await viewModel.loadAccounts() + dismiss() + } catch { + print("Error saving account: \(error)") + showError(error.localizedDescription) + } + } + } + + private func loadAccount(_ account: Account) { + name = account.name + type = account.type + currency = account.currency + balance = account.balance + creditLimit = account.creditLimit + interestRate = account.interestRate + notes = account.notes ?? "" + icon = account.icon + excludeFromStatistics = account.excludeFromStatistics + } + + private func getDefaultCurrency() -> Currency { + // Для всех типов счетов используем валюту региона или GBP по умолчанию + return Currency.current() + } + + // MARK: - Validation + private func validate() -> Bool { + if name.trimmed.isEmpty { + showError("Please enter account name") + return false + } + + if type == .credit && creditLimit == nil { + showError("Please enter credit limit") + return false + } + + if type.requiresInterestRate && interestRate == nil { + showError("Please enter interest rate") + return false + } + + return true + } + + private func showError(_ message: String) { + errorMessage = message + showingErrors = true + } +} + +// MARK: - Helpers +private extension String { + var trimmed: String { + self.trimmingCharacters(in: .whitespacesAndNewlines) + } +} + +// MARK: - Preview +#if DEBUG +struct AccountFormView_Previews: PreviewProvider { + static var previews: some View { + AccountFormView() + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + + AccountFormView(editingAccount: Account.preview) + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} +#endif diff --git a/Coinly/Coinly/Features/Accounts/Views/AccountListView.swift b/Coinly/Coinly/Features/Accounts/Views/AccountListView.swift new file mode 100644 index 0000000..678de73 --- /dev/null +++ b/Coinly/Coinly/Features/Accounts/Views/AccountListView.swift @@ -0,0 +1,163 @@ +import SwiftUI +import CoreData + +struct AccountListView: View { + @Environment(\.managedObjectContext) private var viewContext + @StateObject private var viewModel = AccountViewModel() + @State private var showingAddAccount = false + @State private var showArchived = false + + var body: some View { + ScrollView { + VStack(spacing: 24) { + // Total Balance Card + if !viewModel.activeAccounts.isEmpty { + totalBalanceCard + } + + // Account Groups + VStack(spacing: 24) { + // Basic Accounts + ForEach(AccountType.basicAccounts, id: \.self) { type in + accountGroup(for: type) + } + + // Investment Accounts + if !viewModel.accounts(for: .investmentAccount).isEmpty { + accountGroup(for: .investmentAccount) + } + + // Credit & Debt Accounts + if hasDebtAccounts { + VStack(alignment: .leading, spacing: 16) { + Text("CREDIT & DEBT") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(Theme.secondaryText) + .padding(.horizontal, 16) + + ForEach(AccountType.debtAccounts, id: \.self) { type in + if !viewModel.accounts(for: type).isEmpty { + accountGroup(for: type) + } + } + } + } + } + } + .padding(.top) + } + .background(Theme.background) + .navigationTitle("Accounts") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { showingAddAccount = true }) { + Image(systemName: "plus.circle.fill") + .font(.title2) + .foregroundColor(Theme.primary) + } + } + } + .sheet(isPresented: $showingAddAccount) { + AccountFormView() + .environment(\.managedObjectContext, viewContext) + } + .refreshable { + await viewModel.loadAccounts() + } + .task { + await viewModel.loadAccounts() + } + } + + private var totalBalanceCard: some View { + VStack(spacing: 8) { + Text("Total Balance") + .font(.subheadline) + .foregroundColor(Theme.secondaryText) + + ForEach(Array(viewModel.accountsByCurrency.keys), id: \.self) { currency in + VStack(spacing: 4) { + Text(viewModel.totalBalanceFormatted(currency: currency)) + .font(.system(size: 34, weight: .medium, design: .rounded)) + .foregroundColor(Theme.primaryText) + + Text(currency.code) + .font(.caption) + .foregroundColor(Theme.secondaryText) + } + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 24) + .background( + RoundedRectangle(cornerRadius: 24) + .fill(Theme.cardBackground) + .shadow(color: Theme.cardShadow, radius: 15, x: 0, y: 5) + ) + .padding(.horizontal, 16) + } + + private func accountGroup(for type: AccountType) -> some View { + let accounts = viewModel.accounts(for: type) + if accounts.isEmpty { return EmptyView().erasedToAnyView() } + + return VStack(alignment: .leading, spacing: 16) { + // Group Header + HStack { + Image(systemName: type.icon) + .foregroundColor(accountTypeColor(for: type)) + Text(type.localizedName.uppercased()) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(Theme.secondaryText) + } + .padding(.horizontal, 16) + + // Account Cards + VStack(spacing: 12) { + ForEach(accounts) { account in + NavigationLink { + AccountDetailView(account: account) + .environment(\.managedObjectContext, viewContext) + } label: { + AccountRowView(account: account) + } + .buttonStyle(PlainButtonStyle()) + } + } + } + } + + private func accountTypeColor(for type: AccountType) -> Color { + switch type { + case .debitCard: return Theme.debitCard + case .credit: return Theme.credit + case .cash: return Theme.cash + case .savings: return Theme.savings + default: return Theme.primary + } + } + + private var hasDebtAccounts: Bool { + AccountType.debtAccounts.contains { !viewModel.accounts(for: $0).isEmpty } + } +} + +extension View { + func erasedToAnyView() -> AnyView { + AnyView(self) + } +} + +// MARK: - Preview +#if DEBUG +struct AccountListView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + AccountListView() + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } + } +} +#endif diff --git a/Coinly/Coinly/Features/Accounts/Views/AccountRowView.swift b/Coinly/Coinly/Features/Accounts/Views/AccountRowView.swift index 4b81edb..a1df056 100644 --- a/Coinly/Coinly/Features/Accounts/Views/AccountRowView.swift +++ b/Coinly/Coinly/Features/Accounts/Views/AccountRowView.swift @@ -1,11 +1,3 @@ -// -// AccountRowView.swift -// Coinly -// -// Created by Vadym Samoilenko on 03/03/2025. -// - - import SwiftUI struct AccountRowView: View { @@ -14,30 +6,50 @@ struct AccountRowView: View { var body: some View { HStack(spacing: 16) { // Icon - Image(systemName: account.icon ?? account.type.icon) - .font(.title2) - .foregroundColor(Color(account.type.color)) - .frame(width: 32) + ZStack { + Circle() + .fill(iconBackgroundColor) + .frame(width: 48, height: 48) + + Image(systemName: account.icon ?? account.type.icon) + .font(.system(size: 20)) + .foregroundColor(.white) + } // Account Info VStack(alignment: .leading, spacing: 4) { Text(account.name) - .font(.body) - .foregroundColor(.primary) + .font(.system(.body, design: .rounded)) + .fontWeight(.medium) + .foregroundColor(Theme.primaryText) - HStack { + if account.type == .credit { + VStack(alignment: .leading, spacing: 2) { + Text(account.type.localizedName) + .font(.caption) + .foregroundColor(Theme.secondaryText) + + HStack(spacing: 6) { + if let interestRate = account.interestRate { + Text(formatInterestRate(interestRate)) + .font(.caption2) + .foregroundColor(Theme.secondaryText) + } + + if let creditLimit = account.creditLimit { + Text("•") + .font(.caption2) + .foregroundColor(Theme.secondaryText) + Text("Limit: \(account.currency.format(creditLimit))") + .font(.caption2) + .foregroundColor(Theme.secondaryText) + } + } + } + } else { Text(account.type.localizedName) .font(.caption) - .foregroundColor(.secondary) - - if account.type.requiresInterestRate, - let interestRate = account.interestRate { - Text("•") - .foregroundColor(.secondary) - Text(formatInterestRate(interestRate)) - .font(.caption) - .foregroundColor(.secondary) - } + .foregroundColor(Theme.secondaryText) } } @@ -46,28 +58,45 @@ struct AccountRowView: View { // Balance VStack(alignment: .trailing, spacing: 4) { Text(account.formattedBalance) - .font(.body) + .font(.system(.body, design: .rounded)) + .fontWeight(.medium) .foregroundColor(balanceColor) - if let creditLimit = account.creditLimit { - Text(formatAvailableCredit(creditLimit)) + if account.type == .credit { + Text("Available: \(account.currency.format(account.creditLimit ?? 0 - account.balance))") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(Theme.secondaryText) } } } - .padding(.vertical, 8) + .padding(.vertical, 16) + .padding(.horizontal, 16) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(Theme.cardBackground) + .shadow(color: Theme.cardShadow, radius: 10, x: 0, y: 3) + ) .padding(.horizontal, 16) - .background(Color(UIColor.systemBackground)) - .opacity(account.isArchived ? 0.6 : 1.0) } // MARK: - Computed Properties private var balanceColor: Color { - if account.isOverdrawn { - return .red + if account.balance > 0 { + return Theme.positive + } else if account.balance < 0 { + return Theme.negative + } + return Theme.primaryText + } + + private var iconBackgroundColor: Color { + switch account.type { + case .debitCard: return Theme.debitCard + case .credit: return Theme.credit + case .cash: return Theme.cash + case .savings: return Theme.savings + default: return Theme.primary } - return account.balance >= 0 ? .primary : .red } // MARK: - Helper Methods @@ -75,14 +104,6 @@ struct AccountRowView: View { let percentage = rate * 100 return String(format: "%.1f%%", NSDecimalNumber(decimal: percentage).doubleValue) } - - private func formatAvailableCredit(_ limit: Decimal) -> String { - let available = limit - account.balance - return String( - format: NSLocalizedString("Available: %@", comment: "Available credit"), - account.currency.format(available) - ) - } } // MARK: - Preview @@ -90,30 +111,13 @@ struct AccountRowView: View { struct AccountRowView_Previews: PreviewProvider { static var previews: some View { Group { - // Regular account AccountRowView(account: Account.sampleData[0]) - - // Credit card AccountRowView(account: Account.sampleData[3]) - - // Archived account - AccountRowView(account: Account( - name: "Archived Account", - type: .savings, - currency: .preview, - balance: 1000, - isArchived: true - )) - - // Overdrawn account - AccountRowView(account: Account( - name: "Overdrawn Account", - type: .checking, - currency: .preview, - balance: -500 - )) + .preferredColorScheme(.dark) } .previewLayout(.sizeThatFits) + .padding() + .background(Theme.background) } } -#endif \ No newline at end of file +#endif diff --git a/Coinly/Coinly/Features/Accounts/Views/ContentView.swift b/Coinly/Coinly/Features/Accounts/Views/ContentView.swift index 5cd5c55..1ba01d2 100644 --- a/Coinly/Coinly/Features/Accounts/Views/ContentView.swift +++ b/Coinly/Coinly/Features/Accounts/Views/ContentView.swift @@ -1,86 +1,16 @@ -// -// ContentView.swift -// Coinly -// -// Created by Vadym Samoilenko on 03/03/2025. -// - import SwiftUI import CoreData struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], - animation: .default) - private var items: FetchedResults - + var body: some View { - NavigationView { - List { - ForEach(items) { item in - NavigationLink { - Text("Item at \(item.timestamp!, formatter: itemFormatter)") - } label: { - Text(item.timestamp!, formatter: itemFormatter) - } - } - .onDelete(perform: deleteItems) - } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - EditButton() - } - ToolbarItem { - Button(action: addItem) { - Label("Add Item", systemImage: "plus") - } - } - } - Text("Select an item") - } - } - - private func addItem() { - withAnimation { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } - - private func deleteItems(offsets: IndexSet) { - withAnimation { - offsets.map { items[$0] }.forEach(viewContext.delete) - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } + AccountListView() + .environment(\.managedObjectContext, viewContext) } } -private let itemFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - return formatter -}() - #Preview { - ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + ContentView() + .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) }