Fix account management, update UI components and fix navigation issues
This commit is contained in:
parent
d624434f6a
commit
148f357aa2
13 changed files with 476 additions and 505 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
101
Coinly/Features/Accounts/AccountDetailView.swift
Normal file
101
Coinly/Features/Accounts/AccountDetailView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
38
Coinly/Features/Models/AccountType.swift
Normal file
38
Coinly/Features/Models/AccountType.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
Coinly/Features/Models/DebtPayment.swift
Normal file
23
Coinly/Features/Models/DebtPayment.swift
Normal 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
|
||||
}
|
||||
}
|
||||
53
Coinly/UI/Components/AccountRow.swift
Normal file
53
Coinly/UI/Components/AccountRow.swift
Normal 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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# Coinly
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue