diff --git a/Coinly/App/ContentView.swift b/Coinly/App/ContentView.swift index 093187b..4a47ab8 100644 --- a/Coinly/App/ContentView.swift +++ b/Coinly/App/ContentView.swift @@ -7,10 +7,12 @@ struct ContentView: View { var body: some View { TabView { - DashboardView( - store: transactionsStore, - accountsStore: accountsStore - ) + NavigationView { + DashboardView( + store: transactionsStore, + accountsStore: accountsStore + ) + } .tabItem { Label("Dashboard", systemImage: "chart.pie.fill") } @@ -26,18 +28,11 @@ struct ContentView: View { } } .environmentObject(settings) - .preferredColorScheme(settings.isDarkMode ? .dark : .light) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { - Group { - ContentView() - .preferredColorScheme(.light) - - ContentView() - .preferredColorScheme(.dark) - } + ContentView() } } diff --git a/Coinly/Features/Accounts/AccountDetailView.swift b/Coinly/Features/Accounts/AccountDetailView.swift new file mode 100644 index 0000000..44fe8e0 --- /dev/null +++ b/Coinly/Features/Accounts/AccountDetailView.swift @@ -0,0 +1,101 @@ +import SwiftUI + +struct AccountDetailView: View { + @ObservedObject var account: AccountModel + let onDelete: () -> Void + + @Environment(\.dismiss) private var dismiss + @State private var showingEditSheet = false + @State private var showingDeleteAlert = false + + var body: some View { + List { + Section("Account Details") { + HStack { + Text("Balance") + Spacer() + Text(account.formattedBalance) + .foregroundColor(.secondary) + } + + if let limit = account.creditLimit { + HStack { + Text("Credit Limit") + Spacer() + Text(limit.formatAsCurrency()) + .foregroundColor(.secondary) + } + + if let available = account.availableCredit { + HStack { + Text("Available Credit") + Spacer() + Text(available.formatAsCurrency()) + .foregroundColor(.secondary) + } + } + } + + HStack { + Text("Currency") + Spacer() + Text(account.currency.rawValue) + .foregroundColor(.secondary) + } + } + } + .navigationTitle(account.name) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Menu { + Button { + showingEditSheet = true + } label: { + Label("Edit", systemImage: "pencil") + } + + Button(role: .destructive) { + showingDeleteAlert = true + } label: { + Label("Delete", systemImage: "trash") + } + } label: { + Image(systemName: "ellipsis.circle") + } + } + } + .sheet(isPresented: $showingEditSheet) { + NavigationView { + AddAccountView { updatedAccount in + account.name = updatedAccount.name + account.type = updatedAccount.type + account.balance = updatedAccount.balance + account.currency = updatedAccount.currency + account.creditLimit = updatedAccount.creditLimit + dismiss() + } + } + } + .alert("Delete Account", isPresented: $showingDeleteAlert) { + Button("Cancel", role: .cancel) { } + Button("Delete", role: .destructive) { + onDelete() + dismiss() + } + } message: { + Text("Are you sure you want to delete this account? This action cannot be undone.") + } + } +} + +struct AccountDetailView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + AccountDetailView( + account: AccountModel.sampleData[0], + onDelete: {} + ) + } + .environmentObject(AppSettings.shared) + } +} diff --git a/Coinly/Features/Accounts/AccountListView.swift b/Coinly/Features/Accounts/AccountListView.swift index eb6dab8..55e9f18 100644 --- a/Coinly/Features/Accounts/AccountListView.swift +++ b/Coinly/Features/Accounts/AccountListView.swift @@ -1,50 +1,17 @@ -// -// AccountListView.swift -// Coinly -// -// Created by Vadym Samoilenko on 02/03/2025. -// - - import SwiftUI struct AccountListView: View { - @EnvironmentObject private var settings: AppSettings + @ObservedObject var store: AccountsStore + @Environment(\.dismiss) private var dismiss @State private var showingAddAccount = false - @State private var selectedAccountType: AccountType? - - private let accounts = [ - AccountModel( - name: "Cash Wallet", - type: .wallet, - currency: .usd, - balance: 1000 - ), - AccountModel( - name: "Main Bank Account", - type: .bankAccount, - currency: .usd, - balance: 5000 - ), - AccountModel( - name: "Credit Card", - type: .creditCard, - currency: .usd, - balance: 2000, - creditLimit: 10000, - interestRate: 19.99, - dueDate: Calendar.current.date(byAdding: .day, value: 15, to: Date()) - ) - ] var body: some View { List { ForEach(AccountType.allCases, id: \.self) { type in Section { - let typeAccounts = accounts.filter { $0.type == type } + let typeAccounts = store.getAccounts(of: type) if typeAccounts.isEmpty { Button { - selectedAccountType = type showingAddAccount = true } label: { HStack { @@ -57,6 +24,7 @@ struct AccountListView: View { } else { ForEach(typeAccounts) { account in AccountRowView(account: account) + .listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)) } } } header: { @@ -68,8 +36,16 @@ struct AccountListView: View { } } } + .listStyle(InsetGroupedListStyle()) .navigationTitle("Accounts") + .navigationBarTitleDisplayMode(.large) .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Close") { + dismiss() + } + } + ToolbarItem(placement: .navigationBarTrailing) { Button { showingAddAccount = true @@ -79,13 +55,20 @@ struct AccountListView: View { } } .sheet(isPresented: $showingAddAccount) { - if let type = selectedAccountType { - AddAccountView(accountType: type) - } else { - SelectAccountTypeView { selectedType in - selectedAccountType = selectedType + NavigationView { + AddAccountView { newAccount in + store.addAccount(newAccount) } } } } } + +struct AccountListView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + AccountListView(store: AccountsStore()) + } + .environmentObject(AppSettings.shared) + } +} diff --git a/Coinly/Features/Accounts/AddAccountView.swift b/Coinly/Features/Accounts/AddAccountView.swift index 93d357a..88c17a2 100644 --- a/Coinly/Features/Accounts/AddAccountView.swift +++ b/Coinly/Features/Accounts/AddAccountView.swift @@ -1,120 +1,77 @@ -// -// AddAccountView.swift -// Coinly -// -// Created by Vadym Samoilenko on 02/03/2025. -// - - import SwiftUI struct AddAccountView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var settings: AppSettings - let accountType: AccountType + let onSave: (AccountModel) -> Void - @State private var name = "" - @State private var balance = "" + @State private var name: String = "" + @State private var type: AccountType = .wallet + @State private var balance: String = "" @State private var currency: AppSettings.Currency = .usd - @State private var isActive = true - - // Credit Card fields - @State private var creditLimit = "" - @State private var interestRate = "" - @State private var dueDate = Date() - @State private var minimumPayment = "" - - // Deposit fields - @State private var depositEndDate = Date() - @State private var depositInterestRate = "" - @State private var isAutoRenewable = false - - // Debt fields - @State private var creditorName = "" - @State private var debtInterestRate = "" - - private var isValid: Bool { - !name.isEmpty && !balance.isEmpty - } + @State private var creditLimit: String = "" var body: some View { - NavigationView { - Form { - Section("Basic Information") { - TextField("Account Name", text: $name) - + Form { + Section("Basic Information") { + TextField("Account Name", text: $name) + + Picker("Type", selection: $type) { + ForEach(AccountType.allCases, id: \.self) { type in + Text(type.rawValue).tag(type) + } + } + + HStack { + Text(currency.symbol) + TextField("Balance", text: $balance) + .keyboardType(.decimalPad) + } + + Picker("Currency", selection: $currency) { + ForEach(AppSettings.Currency.allCases, id: \.self) { currency in + Text(currency.rawValue).tag(currency) + } + } + } + + if type == .creditCard { + Section("Credit Card Details") { HStack { - Text(settings.currency.symbol) - .foregroundColor(.secondary) - TextField("Balance", text: $balance) - .keyboardType(.decimalPad) - } - - Picker("Currency", selection: $currency) { - ForEach(AppSettings.Currency.allCases, id: \.self) { currency in - Text(currency.rawValue).tag(currency) - } - } - } - - if accountType == .creditCard { - Section("Credit Card Details") { - HStack { - Text(settings.currency.symbol) - TextField("Credit Limit", text: $creditLimit) - .keyboardType(.decimalPad) - } - - TextField("Interest Rate %", text: $interestRate) - .keyboardType(.decimalPad) - - DatePicker("Due Date", selection: $dueDate, displayedComponents: .date) - - HStack { - Text(settings.currency.symbol) - TextField("Minimum Payment", text: $minimumPayment) - .keyboardType(.decimalPad) - } - } - } - - if accountType == .deposit { - Section("Deposit Details") { - DatePicker("End Date", selection: $depositEndDate, displayedComponents: .date) - - TextField("Interest Rate %", text: $depositInterestRate) - .keyboardType(.decimalPad) - - Toggle("Auto-renewable", isOn: $isAutoRenewable) - } - } - - if accountType == .debt { - Section("Debt Details") { - TextField("Creditor Name", text: $creditorName) - - TextField("Interest Rate %", text: $debtInterestRate) + Text(currency.symbol) + TextField("Credit Limit", text: $creditLimit) .keyboardType(.decimalPad) } } } - .navigationTitle("New \(accountType.rawValue)") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { - dismiss() - } + } + .navigationTitle("New Account") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + dismiss() } - - ToolbarItem(placement: .confirmationAction) { - Button("Add") { - // TODO: Add account creation - dismiss() - } - .disabled(!isValid) + } + + ToolbarItem(placement: .confirmationAction) { + Button("Add") { + guard let balanceValue = Double(balance) else { return } + let limitValue = Double(creditLimit) + + let account = AccountModel( + name: name, + type: type, + currency: currency, + balance: balanceValue, + creditLimit: limitValue + ) + + onSave(account) + dismiss() } + .disabled(name.isEmpty || balance.isEmpty) } } } @@ -122,7 +79,9 @@ struct AddAccountView: View { struct AddAccountView_Previews: PreviewProvider { static var previews: some View { - AddAccountView(accountType: .creditCard) - .environmentObject(AppSettings.shared) + NavigationView { + AddAccountView { _ in } + } + .environmentObject(AppSettings.shared) } -} \ No newline at end of file +} diff --git a/Coinly/Features/Dashboard/DashboardView.swift b/Coinly/Features/Dashboard/DashboardView.swift index 0b53d50..2ed650e 100644 --- a/Coinly/Features/Dashboard/DashboardView.swift +++ b/Coinly/Features/Dashboard/DashboardView.swift @@ -4,230 +4,140 @@ struct DashboardView: View { @ObservedObject var store: TransactionsStore @ObservedObject var accountsStore: AccountsStore @EnvironmentObject private var settings: AppSettings - @State private var showingAccountsList = false + @State private var showingAddAccount = false + @State private var selectedAccount: AccountModel? - private var totalBalance: Double { - store.transactions.reduce(0) { total, transaction in - let amount = transaction.amountInCurrentCurrency() - return total + (transaction.isExpense ? -amount : amount) + private var totalAccountsBalance: Double { + accountsStore.accounts.reduce(0) { total, account in + total + (account.type == .creditCard ? 0 : account.balance) } } - private var income: Double { - store.transactions + private var thisMonthTransactions: [TransactionModel] { + let calendar = Calendar.current + return store.transactions.filter { transaction in + calendar.isDate(transaction.date, equalTo: Date(), toGranularity: .month) + } + } + + private var monthlyIncome: Double { + thisMonthTransactions .filter { !$0.isExpense } .reduce(0) { $0 + $1.amountInCurrentCurrency() } } - private var expenses: Double { - store.transactions + private var monthlyExpenses: Double { + thisMonthTransactions .filter { $0.isExpense } .reduce(0) { $0 + $1.amountInCurrentCurrency() } } - private var categoryExpenses: [PieChartView.PieSlice] { - let expensesByCategory = Dictionary(grouping: store.transactions.filter { $0.isExpense }) { $0.category } - let totalExpenses = expenses - - return expensesByCategory.map { category, transactions in - let categoryTotal = transactions.reduce(0) { $0 + $1.amountInCurrentCurrency() } - let percentage = totalExpenses > 0 ? categoryTotal / totalExpenses : 0 - return PieChartView.PieSlice( - category: CategoryModel.category(for: category), - amount: categoryTotal, - percentage: percentage - ) - }.sorted { $0.amount > $1.amount } - } - var body: some View { - NavigationView { - ScrollView { - VStack(spacing: 24) { - // Header Stats - HStack(spacing: 20) { - // Monthly Balance - VStack(alignment: .leading, spacing: 8) { - Text("This Month") - .font(.subheadline) - .foregroundColor(Color(uiColor: .secondaryLabel)) - Text(totalBalance.formatAsCurrency()) - .font(.system(size: 28, weight: .semibold)) - - HStack(spacing: 16) { - // Income indicator - HStack(spacing: 4) { - Circle() - .fill(Color.green) - .frame(width: 8, height: 8) - Text("↑ \(income.formatAsCurrency())") - .font(.caption) - .foregroundColor(.green) - } - - // Expense indicator - HStack(spacing: 4) { - Circle() - .fill(Color.red) - .frame(width: 8, height: 8) - Text("↓ \(expenses.formatAsCurrency())") - .font(.caption) - .foregroundColor(.red) - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } - .padding() - .background(Color(uiColor: .secondarySystemBackground)) - .cornerRadius(16) + ScrollView { + VStack(spacing: 24) { + // Monthly Summary + VStack(spacing: 8) { + Text("This Month") + .font(.subheadline) + .foregroundColor(Color(uiColor: .secondaryLabel)) - // Accounts Section - VStack(alignment: .leading, spacing: 16) { + Text(totalAccountsBalance.formatAsCurrency()) + .font(.system(size: 34, weight: .bold)) + + HStack(spacing: 20) { + // Income HStack { - Text("Accounts") - .font(.title2) - .fontWeight(.bold) - Spacer() - Button("See All") { - showingAccountsList = true - } - .foregroundColor(.accentColor) + Circle() + .fill(Color(uiColor: .systemGreen)) + .frame(width: 8, height: 8) + Text("↑ \(monthlyIncome.formatAsCurrency())") + .font(.caption) + .foregroundColor(Color(uiColor: .systemGreen)) } - ForEach(Array(accountsStore.accounts.prefix(3))) { account in - Button(action: { - // Show account details - }) { - HStack(spacing: 16) { - // Account Icon - Image(systemName: account.type.icon) - .font(.title2) - .foregroundColor(.white) - .frame(width: 40, height: 40) - .background(accountColor(for: account.type)) - .cornerRadius(12) - - VStack(alignment: .leading, spacing: 4) { - Text(account.name) - .font(.headline) - .foregroundColor(Color(uiColor: .label)) - Text(account.type.rawValue) - .font(.caption) - .foregroundColor(Color(uiColor: .secondaryLabel)) - } - - Spacer() - - VStack(alignment: .trailing, spacing: 4) { - Text(account.balance.formatAsCurrency()) - .font(.headline) - .foregroundColor(Color(uiColor: .label)) - if let availableCredit = account.availableCredit { - Text("Available: \(availableCredit.formatAsCurrency())") - .font(.caption) - .foregroundColor(Color(uiColor: .secondaryLabel)) - } - } - } - .padding() - .background(Color(uiColor: .secondarySystemBackground)) - .cornerRadius(16) - } + // Expenses + HStack { + Circle() + .fill(Color(uiColor: .systemRed)) + .frame(width: 8, height: 8) + Text("↓ \(monthlyExpenses.formatAsCurrency())") + .font(.caption) + .foregroundColor(Color(uiColor: .systemRed)) + } + } + } + .padding() + .background(Color(uiColor: .secondarySystemBackground)) + .cornerRadius(16) + + // Accounts Section + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Accounts") + .font(.title2) + .fontWeight(.bold) + + Spacer() + + Button { + showingAddAccount = true + } label: { + Text("Add Account") + .foregroundColor(.accentColor) } } - .padding(.horizontal) - // Expenses Analysis + ForEach(accountsStore.accounts) { account in + AccountRowView(account: account) + .onTapGesture { + selectedAccount = account + } + } + } + .padding(.horizontal) + + // Spending Analysis Section + if !monthlyExpenses.isZero { VStack(alignment: .leading, spacing: 16) { Text("Spending Analysis") .font(.title2) .fontWeight(.bold) - .padding(.horizontal) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 16) { - ForEach(categoryExpenses) { slice in - VStack(alignment: .leading, spacing: 12) { - HStack { - Image(systemName: slice.category.icon) - .foregroundColor(.white) - .frame(width: 32, height: 32) - .background(Color(slice.category.color)) - .cornerRadius(8) - - VStack(alignment: .leading) { - Text(slice.category.name) - .font(.headline) - Text(slice.amount.formatAsCurrency()) - .font(.subheadline) - .foregroundColor(Color(uiColor: .secondaryLabel)) - } - } - - // Progress bar - GeometryReader { geometry in - ZStack(alignment: .leading) { - Rectangle() - .fill(Color(uiColor: .systemFill)) - Rectangle() - .fill(Color(slice.category.color)) - .frame(width: geometry.size.width * slice.percentage) - } - } - .frame(height: 8) - .cornerRadius(4) - - Text(String(format: "%.1f%%", slice.percentage * 100)) - .font(.caption) - .foregroundColor(Color(uiColor: .secondaryLabel)) - } - .frame(width: 160) - .padding() - .background(Color(uiColor: .secondarySystemBackground)) - .cornerRadius(16) - } - } - .padding(.horizontal) - } } + .padding(.horizontal) } - .padding(.vertical) } - .navigationTitle("Dashboard") - .background(Color(uiColor: .systemBackground)) - .sheet(isPresented: $showingAccountsList) { - AccountsListView(store: accountsStore) + .padding(.vertical) + } + .navigationTitle("Dashboard") + .sheet(isPresented: $showingAddAccount) { + NavigationView { + AddAccountView { newAccount in + accountsStore.addAccount(newAccount) + } } } - } - - private func accountColor(for type: AccountType) -> Color { - switch type { - case .wallet: return .blue - case .bankAccount: return .green - case .creditCard: return .purple - case .deposit: return .orange - case .debt: return .red + .sheet(item: $selectedAccount) { account in + NavigationView { + AccountDetailView( + account: account, + onDelete: { + accountsStore.deleteAccount(account) + selectedAccount = nil + } + ) + } } } } struct DashboardView_Previews: PreviewProvider { static var previews: some View { - Group { + NavigationView { DashboardView( store: TransactionsStore(), accountsStore: AccountsStore() ) - .preferredColorScheme(.light) - - DashboardView( - store: TransactionsStore(), - accountsStore: AccountsStore() - ) - .preferredColorScheme(.dark) + .environmentObject(AppSettings.shared) } - .environmentObject(AppSettings.shared) } } diff --git a/Coinly/Features/Models/AccountModel.swift b/Coinly/Features/Models/AccountModel.swift index dd2f538..02bd8fc 100644 --- a/Coinly/Features/Models/AccountModel.swift +++ b/Coinly/Features/Models/AccountModel.swift @@ -1,59 +1,32 @@ import Foundation import SwiftUI -enum AccountType: String, Codable, CaseIterable { - case wallet = "Wallet" - case bankAccount = "Bank Account" - case creditCard = "Credit Card" - case deposit = "Deposit" - case debt = "Debt" - - var icon: String { - switch self { - case .wallet: return "banknote" - case .bankAccount: return "building.columns.fill" - case .creditCard: return "creditcard.fill" - case .deposit: return "vault.fill" - case .debt: return "exclamationmark.circle.fill" - } - } - - var color: Color { - switch self { - case .wallet: return Color(uiColor: .systemGreen) - case .bankAccount: return Color(uiColor: .systemBlue) - case .creditCard: return Color(uiColor: .systemPurple) - case .deposit: return Color(uiColor: .systemOrange) - case .debt: return Color(uiColor: .systemRed) - } - } -} - -struct AccountModel: Identifiable, Codable { +class AccountModel: ObservableObject, Identifiable { let id: String - var name: String - var type: AccountType - var currency: AppSettings.Currency - var balance: Double - var isActive: Bool + @Published var name: String + @Published var type: AccountType + @Published var currency: AppSettings.Currency + @Published var balance: Double + @Published var isActive: Bool // Кредитная карта - var creditLimit: Double? - var interestRate: Double? - var dueDate: Date? - var minimumPayment: Double? + @Published var creditLimit: Double? + @Published var interestRate: Double? + @Published var dueDate: Date? + @Published var minimumPayment: Double? // Депозит - var depositEndDate: Date? - var depositInterestRate: Double? - var isAutoRenewable: Bool? + @Published var depositEndDate: Date? + @Published var depositInterestRate: Double? + @Published var isAutoRenewable: Bool? // Долг - var creditorName: String? - var debtInterestRate: Double? - var paymentSchedule: [DebtPayment]? + @Published var creditorName: String? + @Published var debtInterestRate: Double? + @Published var paymentSchedule: [DebtPayment]? init( + id: String = UUID().uuidString, name: String, type: AccountType, currency: AppSettings.Currency, @@ -70,7 +43,7 @@ struct AccountModel: Identifiable, Codable { debtInterestRate: Double? = nil, paymentSchedule: [DebtPayment]? = nil ) { - self.id = UUID().uuidString + self.id = id self.name = name self.type = type self.currency = currency @@ -102,38 +75,6 @@ struct AccountModel: Identifiable, Codable { var formattedBalance: String { balance.formatAsCurrency() } - - // Методы для работы с кредитной картой - mutating func addPurchase(_ amount: Double) -> Bool { - guard type == .creditCard, - let limit = creditLimit, - balance + amount <= limit else { - return false - } - balance += amount - return true - } - - mutating func makePayment(_ amount: Double) -> Bool { - guard amount <= balance else { return false } - balance -= amount - return true - } -} - -// Структура для платежей по долгу -struct DebtPayment: Codable, Identifiable { - let id: String - var dueDate: Date - var amount: Double - var isPaid: Bool - - init(dueDate: Date, amount: Double, isPaid: Bool = false) { - self.id = UUID().uuidString - self.dueDate = dueDate - self.amount = amount - self.isPaid = isPaid - } } // Sample Data diff --git a/Coinly/Features/Models/AccountType.swift b/Coinly/Features/Models/AccountType.swift new file mode 100644 index 0000000..e888cdd --- /dev/null +++ b/Coinly/Features/Models/AccountType.swift @@ -0,0 +1,38 @@ +// +// AccountType.swift +// Coinly +// +// Created by Vadym Samoilenko on 02/03/2025. +// + + +import Foundation +import SwiftUI + +enum AccountType: String, Codable, CaseIterable { + case wallet = "Wallet" + case bankAccount = "Bank Account" + case creditCard = "Credit Card" + case deposit = "Deposit" + case debt = "Debt" + + var icon: String { + switch self { + case .wallet: return "banknote" + case .bankAccount: return "building.columns.fill" + case .creditCard: return "creditcard.fill" + case .deposit: return "vault.fill" + case .debt: return "exclamationmark.circle.fill" + } + } + + var color: Color { + switch self { + case .wallet: return Color(uiColor: .systemGreen) + case .bankAccount: return Color(uiColor: .systemBlue) + case .creditCard: return Color(uiColor: .systemPurple) + case .deposit: return Color(uiColor: .systemOrange) + case .debt: return Color(uiColor: .systemRed) + } + } +} \ No newline at end of file diff --git a/Coinly/Features/Models/AccountsStore.swift b/Coinly/Features/Models/AccountsStore.swift index 26545e6..2cbfd42 100644 --- a/Coinly/Features/Models/AccountsStore.swift +++ b/Coinly/Features/Models/AccountsStore.swift @@ -1,15 +1,12 @@ -// -// AccountsStore.swift -// Coinly -// -// Created by Vadym Samoilenko on 02/03/2025. -// - - import Foundation class AccountsStore: ObservableObject { - @Published private(set) var accounts: [AccountModel] = AccountModel.sampleData + @Published private(set) var accounts: [AccountModel] = [] + + init() { + // Загружаем тестовые данные + accounts = AccountModel.sampleData + } func addAccount(_ account: AccountModel) { accounts.append(account) @@ -21,6 +18,10 @@ class AccountsStore: ObservableObject { } } + func deleteAccount(_ account: AccountModel) { + accounts.removeAll { $0.id == account.id } + } + func deleteAccount(at indexSet: IndexSet) { accounts.remove(atOffsets: indexSet) } @@ -29,17 +30,7 @@ class AccountsStore: ObservableObject { accounts.first { $0.id == id } } - // Получение счетов по типу func getAccounts(of type: AccountType) -> [AccountModel] { accounts.filter { $0.type == type } } - - // Общий баланс всех счетов в выбранной валюте - func totalBalance(in currency: AppSettings.Currency) -> Double { - accounts.reduce(0) { total, account in - let settings = AppSettings.shared - let convertedBalance = settings.convert(account.balance, from: account.currency, to: currency) - return total + convertedBalance - } - } -} \ No newline at end of file +} diff --git a/Coinly/Features/Models/DebtPayment.swift b/Coinly/Features/Models/DebtPayment.swift new file mode 100644 index 0000000..b186acd --- /dev/null +++ b/Coinly/Features/Models/DebtPayment.swift @@ -0,0 +1,23 @@ +// +// DebtPayment.swift +// Coinly +// +// Created by Vadym Samoilenko on 02/03/2025. +// + + +import Foundation + +struct DebtPayment: Codable, Identifiable { + let id: String + var dueDate: Date + var amount: Double + var isPaid: Bool + + init(id: String = UUID().uuidString, dueDate: Date, amount: Double, isPaid: Bool = false) { + self.id = id + self.dueDate = dueDate + self.amount = amount + self.isPaid = isPaid + } +} \ No newline at end of file diff --git a/Coinly/UI/Components/AccountRow.swift b/Coinly/UI/Components/AccountRow.swift new file mode 100644 index 0000000..0f50996 --- /dev/null +++ b/Coinly/UI/Components/AccountRow.swift @@ -0,0 +1,53 @@ +// +// AccountRow.swift +// Coinly +// +// Created by Vadym Samoilenko on 02/03/2025. +// + + +import SwiftUI + +struct AccountRow: View { + let account: AccountModel + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: 16) { + Image(systemName: account.type.icon) + .font(.title2) + .foregroundColor(.white) + .frame(width: 40, height: 40) + .background(account.type.color) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + VStack(alignment: .leading, spacing: 4) { + Text(account.name) + .font(AppStyle.fontHeadline) + + Text(account.type.rawValue) + .font(AppStyle.fontSubheadline) + .foregroundColor(AppStyle.labelSecondary) + } + + Spacer() + + VStack(alignment: .trailing, spacing: 4) { + Text(account.formattedBalance) + .font(AppStyle.fontCallout) + + if let available = account.availableCredit { + Text("Available: \(available.formatAsCurrency())") + .font(AppStyle.fontCaption) + .foregroundColor(AppStyle.labelSecondary) + } + } + } + .padding() + .background(AppStyle.backgroundSecondary) + .cornerRadius(AppStyle.cornerRadiusLarge) + } + .buttonStyle(PlainButtonStyle()) + } +} diff --git a/Coinly/UI/Components/AccountRowView.swift b/Coinly/UI/Components/AccountRowView.swift index 181ae6e..2f7a0d2 100644 --- a/Coinly/UI/Components/AccountRowView.swift +++ b/Coinly/UI/Components/AccountRowView.swift @@ -1,34 +1,30 @@ -// -// AccountRowView.swift -// Coinly -// -// Created by Vadym Samoilenko on 02/03/2025. -// - - import SwiftUI struct AccountRowView: View { let account: AccountModel var body: some View { - VStack(spacing: 8) { - // Header - HStack { + HStack(spacing: 16) { + Image(systemName: account.type.icon) + .font(.title2) + .foregroundColor(.white) + .frame(width: 40, height: 40) + .background(account.type.color) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + VStack(alignment: .leading, spacing: 4) { Text(account.name) .font(.headline) - Spacer() - Text(account.currency.symbol) + Text(account.type.rawValue) .font(.subheadline) .foregroundColor(.secondary) } - // Balance - HStack { + Spacer() + + VStack(alignment: .trailing, spacing: 4) { Text(account.formattedBalance) - .font(.title3) - .fontWeight(.semibold) - Spacer() + .font(.headline) if let availableCredit = account.availableCredit { Text("Available: \(availableCredit.formatAsCurrency())") @@ -36,29 +32,17 @@ struct AccountRowView: View { .foregroundColor(.secondary) } } - - // Additional Info - if account.type == .creditCard { - HStack { - if let dueDate = account.dueDate { - Label(dueDate.formatted(date: .abbreviated, time: .omitted), - systemImage: "calendar") - .font(.caption) - .foregroundColor(.secondary) - } - - if let rate = account.interestRate { - Text("•") - .foregroundColor(.secondary) - Label("\(String(format: "%.1f", rate))%", - systemImage: "percent") - .font(.caption) - .foregroundColor(.secondary) - } - } - } } - .padding(.vertical, 8) - .contentShape(Rectangle()) + .padding() + .background(Color(uiColor: .secondarySystemBackground)) + .cornerRadius(12) } -} \ No newline at end of file +} + +struct AccountRowView_Previews: PreviewProvider { + static var previews: some View { + AccountRowView(account: AccountModel.sampleData[0]) + .previewLayout(.sizeThatFits) + .padding() + } +} diff --git a/Coinly/UI/Components/AccountsSummaryView.swift b/Coinly/UI/Components/AccountsSummaryView.swift index 373b076..7367388 100644 --- a/Coinly/UI/Components/AccountsSummaryView.swift +++ b/Coinly/UI/Components/AccountsSummaryView.swift @@ -5,10 +5,6 @@ struct AccountsSummaryView: View { @EnvironmentObject private var settings: AppSettings @State private var showingAccountsList = false - private var totalBalance: Double { - accountsStore.totalBalance(in: settings.currency) - } - var body: some View { VStack(spacing: 16) { // Header with total balance @@ -17,7 +13,9 @@ struct AccountsSummaryView: View { .font(.subheadline) .foregroundColor(Color(uiColor: .secondaryLabel)) - Text(totalBalance.formatAsCurrency()) + Text(accountsStore.accounts.reduce(0) { total, account in + total + (account.type == .creditCard ? 0 : account.balance) + }.formatAsCurrency()) .font(.title2) .fontWeight(.bold) } @@ -25,41 +23,36 @@ struct AccountsSummaryView: View { // Recent accounts preview VStack(spacing: 12) { ForEach(Array(accountsStore.accounts.prefix(2))) { account in - AccountCardView(account: account) + AccountRowView(account: account) } } // Show all button - Button(action: { + Button { showingAccountsList = true - }) { + } label: { Text("Show All Accounts") .font(.headline) - .foregroundColor(.blue) + .foregroundColor(.accentColor) } } .padding() - .background(Color(uiColor: .systemBackground)) + .background(Color(uiColor: .secondarySystemBackground)) .cornerRadius(12) - .shadow(radius: 2) .sheet(isPresented: $showingAccountsList) { - AccountsListView(store: accountsStore) + NavigationView { + AccountListView(store: accountsStore) + } } } } struct AccountsSummaryView_Previews: PreviewProvider { static var previews: some View { - Group { - AccountsSummaryView(accountsStore: AccountsStore()) - .preferredColorScheme(.light) - - AccountsSummaryView(accountsStore: AccountsStore()) - .preferredColorScheme(.dark) - } - .environmentObject(AppSettings.shared) - .padding() - .background(Color(uiColor: .systemGroupedBackground)) - .previewLayout(.sizeThatFits) + AccountsSummaryView(accountsStore: AccountsStore()) + .environmentObject(AppSettings.shared) + .padding() + .background(Color(uiColor: .systemBackground)) + .previewLayout(.sizeThatFits) } } diff --git a/README.md b/README.md index b3d1c4c..df1ac4e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Coinly -![Tests](https://github.com/SamoilenkoVadym/Coinly/workflows/iOS%20Tests/badge.svg) - Personal Finance Management App built with SwiftUI +## Development Status +Currently in active development with local testing. ## Features - Transaction management with categories - Multiple account types support (wallet, bank account, credit card, deposit)