Fix account management, update UI components and fix navigation issues

This commit is contained in:
“SamoilenkoVadym” 2025-03-02 22:42:59 +00:00
parent d624434f6a
commit 148f357aa2
13 changed files with 476 additions and 505 deletions

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}

View file

@ -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())
}
}

View file

@ -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)
}
}
}
struct AccountRowView_Previews: PreviewProvider {
static var previews: some View {
AccountRowView(account: AccountModel.sampleData[0])
.previewLayout(.sizeThatFits)
.padding()
}
}

View file

@ -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)
}
}

View file

@ -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)