Update project structure and implement basic functionality
This commit is contained in:
parent
fcaf3c5105
commit
5861b34737
14 changed files with 567 additions and 568 deletions
|
|
@ -2,41 +2,60 @@ import SwiftUI
|
|||
|
||||
struct AccountCardView: View {
|
||||
let account: AccountModel
|
||||
let isSelected: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Image(systemName: account.icon)
|
||||
.font(.title2)
|
||||
Text(account.name)
|
||||
.font(.headline)
|
||||
if account.isDefault {
|
||||
Image(systemName: "star.fill")
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
.foregroundColor(account.type.color)
|
||||
|
||||
Spacer()
|
||||
if !account.isActive {
|
||||
Text("Inactive")
|
||||
.font(.caption)
|
||||
|
||||
if account.isDefault {
|
||||
Text("Default")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color(uiColor: .systemGray6))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
Text(AppCurrencyFormatter.format(account.balance, currency: account.currency))
|
||||
.font(.title)
|
||||
.bold()
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(account.name)
|
||||
.font(.headline)
|
||||
|
||||
Text(account.type.rawValue)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text(account.type.rawValue)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(account.balance.formatted(.currency(code: account.currency.rawValue)))
|
||||
.font(.title2.bold())
|
||||
}
|
||||
.padding()
|
||||
.background(account.type.color.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
.frame(width: 200)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color(uiColor: .systemBackground))
|
||||
.shadow(
|
||||
color: isSelected ? Color.accentColor.opacity(0.3) : Color.black.opacity(0.1),
|
||||
radius: isSelected ? 8 : 4
|
||||
)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(isSelected ? Color.accentColor : Color.clear, lineWidth: 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AccountCardView(account: AccountModel.sampleData)
|
||||
.padding()
|
||||
AccountCardView(
|
||||
account: AccountModel.previewData,
|
||||
isSelected: true
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,83 +3,86 @@ import SwiftUI
|
|||
struct AccountDetailView: View {
|
||||
@EnvironmentObject private var accountsStore: AccountsStore
|
||||
@EnvironmentObject private var transactionsStore: TransactionsStore
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
let account: AccountModel
|
||||
@State private var showingEditAccount = false
|
||||
|
||||
@State private var showingEditSheet = false
|
||||
@State private var showingAddTransaction = false
|
||||
|
||||
private var transactions: [TransactionModel] {
|
||||
transactionsStore.filterTransactions(
|
||||
filter: TransactionFilter(accountId: account.id)
|
||||
)
|
||||
}
|
||||
|
||||
private func handleAddTransaction(_ transaction: TransactionModel) {
|
||||
transactionsStore.addTransaction(transaction)
|
||||
if let targetAccount = accountsStore.getAccount(withId: transaction.accountId ?? "") {
|
||||
let amount = transaction.type == .income ? transaction.amount : -transaction.amount
|
||||
let newBalance = targetAccount.balance + amount
|
||||
accountsStore.updateAccount(targetAccount.with(balance: newBalance))
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
AccountCardView(account: account)
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
AccountCardView(
|
||||
account: account,
|
||||
isSelected: false
|
||||
)
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
Section("Recent Transactions") {
|
||||
let transactions = transactionsStore.filterTransactions(
|
||||
filter: TransactionFilter(accountId: account.id)
|
||||
if transactions.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"No Transactions",
|
||||
systemImage: "tray.fill",
|
||||
description: Text("Add your first transaction to start tracking your finances")
|
||||
)
|
||||
|
||||
if transactions.isEmpty {
|
||||
Text("No transactions yet")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
ForEach(transactions.prefix(5)) { transaction in
|
||||
TransactionRowView(transaction: transaction)
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
TransactionsView(
|
||||
title: "\(account.name) Transactions",
|
||||
filter: TransactionFilter(accountId: account.id)
|
||||
)
|
||||
} label: {
|
||||
Text("See All Transactions")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !account.isDefault {
|
||||
Section {
|
||||
Button(role: .destructive) {
|
||||
accountsStore.deleteAccount(account)
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("Delete Account", systemImage: "trash")
|
||||
}
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listRowBackground(Color.clear)
|
||||
} else {
|
||||
ForEach(transactions) { transaction in
|
||||
TransactionRowView(transaction: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(account.name)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Edit") {
|
||||
showingEditAccount = true
|
||||
Button {
|
||||
showingEditSheet = true
|
||||
} label: {
|
||||
Text("Edit")
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showingAddTransaction = true
|
||||
} label: {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingEditAccount) {
|
||||
.sheet(isPresented: $showingEditSheet) {
|
||||
NavigationView {
|
||||
EditAccountView(account: account)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddTransaction) {
|
||||
NavigationView {
|
||||
AddTransactionView { transaction in
|
||||
handleAddTransaction(transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
AccountDetailView(account: AccountModel(
|
||||
name: "Sample Account",
|
||||
balance: 1000,
|
||||
type: .cash,
|
||||
icon: "creditcard",
|
||||
isDefault: true,
|
||||
currency: .usd
|
||||
))
|
||||
.environmentObject(AccountsStore.shared)
|
||||
.environmentObject(TransactionsStore.shared)
|
||||
.environmentObject(CategoryStore.shared)
|
||||
.environmentObject(AppSettings.shared)
|
||||
AccountDetailView(account: AccountModel.previewData)
|
||||
.environmentObject(AccountsStore.shared)
|
||||
.environmentObject(TransactionsStore.shared)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,10 @@ struct AccountRowView: View {
|
|||
let account: AccountModel
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
HStack {
|
||||
Image(systemName: account.icon)
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 40, height: 40)
|
||||
.background(account.type.color)
|
||||
.cornerRadius(8)
|
||||
.foregroundColor(account.type.color)
|
||||
.frame(width: 32)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(account.name)
|
||||
|
|
@ -24,7 +21,7 @@ struct AccountRowView: View {
|
|||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text(AppCurrencyFormatter.format(account.balance, currency: account.currency))
|
||||
Text(account.balance.formatted(.currency(code: account.currency.rawValue)))
|
||||
.font(.headline)
|
||||
|
||||
if account.isDefault {
|
||||
|
|
@ -34,11 +31,11 @@ struct AccountRowView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.opacity(account.isActive ? 1 : 0.5)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AccountRowView(account: AccountModel.sampleData)
|
||||
AccountRowView(account: AccountModel.previewData)
|
||||
.padding()
|
||||
}
|
||||
|
|
|
|||
58
Coinly/Features/Accounts/AccountsSummaryView.swift
Normal file
58
Coinly/Features/Accounts/AccountsSummaryView.swift
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Path: Features/Accounts/AccountsSummaryView.swift
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AccountsSummaryView: View {
|
||||
@EnvironmentObject private var accountsStore: AccountsStore
|
||||
@EnvironmentObject private var settings: AppSettings
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Text("Total Balance")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(accountsStore.totalBalance.formatted(.currency(code: settings.selectedCurrency.rawValue)))
|
||||
.font(.largeTitle.bold())
|
||||
|
||||
if accountsStore.accounts.isEmpty {
|
||||
Text("No accounts")
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 16) {
|
||||
ForEach(accountsStore.accounts) { account in
|
||||
VStack(spacing: 8) {
|
||||
Text(account.name)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(account.balance.formatted(.currency(code: account.currency.rawValue)))
|
||||
.font(.callout.bold())
|
||||
}
|
||||
.frame(minWidth: 100)
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 12)
|
||||
.background(Color(uiColor: .secondarySystemBackground))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AccountsSummaryView()
|
||||
.environmentObject(AccountsStore.shared)
|
||||
.environmentObject(AppSettings.shared)
|
||||
.padding()
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import SwiftUI
|
|||
struct EditAccountView: View {
|
||||
@EnvironmentObject private var accountsStore: AccountsStore
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
let account: AccountModel
|
||||
|
||||
@State private var name: String
|
||||
|
|
@ -12,19 +11,15 @@ struct EditAccountView: View {
|
|||
@State private var currency: AppSettings.Currency
|
||||
@State private var icon: String
|
||||
@State private var isDefault: Bool
|
||||
@State private var isActive: Bool
|
||||
@State private var isArchived: Bool
|
||||
|
||||
init(account: AccountModel) {
|
||||
self.account = account
|
||||
self._name = State(initialValue: account.name)
|
||||
self._type = State(initialValue: account.type)
|
||||
self._balance = State(initialValue: account.balance)
|
||||
self._currency = State(initialValue: account.currency)
|
||||
self._icon = State(initialValue: account.icon)
|
||||
self._isDefault = State(initialValue: account.isDefault)
|
||||
self._isActive = State(initialValue: account.isActive)
|
||||
self._isArchived = State(initialValue: account.isArchived)
|
||||
_name = State(initialValue: account.name)
|
||||
_type = State(initialValue: account.type)
|
||||
_balance = State(initialValue: account.balance)
|
||||
_currency = State(initialValue: account.currency)
|
||||
_icon = State(initialValue: account.icon)
|
||||
_isDefault = State(initialValue: account.isDefault)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -48,20 +43,19 @@ struct EditAccountView: View {
|
|||
}
|
||||
|
||||
Section {
|
||||
Toggle("Active", isOn: $isActive)
|
||||
Toggle("Set as Default", isOn: $isDefault)
|
||||
Toggle("Archive", isOn: $isArchived)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Delete Account", role: .destructive) {
|
||||
accountsStore.deleteAccount(account)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Edit Account")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
let updatedAccount = AccountModel(
|
||||
|
|
@ -69,11 +63,9 @@ struct EditAccountView: View {
|
|||
name: name,
|
||||
balance: balance,
|
||||
type: type,
|
||||
icon: icon,
|
||||
icon: type.icon,
|
||||
isDefault: isDefault,
|
||||
currency: currency,
|
||||
isActive: isActive,
|
||||
isArchived: isArchived
|
||||
currency: currency
|
||||
)
|
||||
accountsStore.updateAccount(updatedAccount)
|
||||
dismiss()
|
||||
|
|
@ -86,8 +78,7 @@ struct EditAccountView: View {
|
|||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
EditAccountView(account: AccountModel.sampleData)
|
||||
EditAccountView(account: AccountModel.previewData)
|
||||
.environmentObject(AccountsStore.shared)
|
||||
.environmentObject(AppSettings.shared)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,53 +1,128 @@
|
|||
import SwiftUI
|
||||
|
||||
struct DashboardView: View {
|
||||
// MARK: - Environment
|
||||
@EnvironmentObject private var store: TransactionsStore
|
||||
@EnvironmentObject private var accountsStore: AccountsStore
|
||||
@EnvironmentObject private var categoryStore: CategoryStore
|
||||
@EnvironmentObject private var transactionsStore: TransactionsStore
|
||||
@EnvironmentObject private var settings: AppSettings
|
||||
|
||||
// MARK: - State
|
||||
@State private var selectedAccountId: AccountModel.ID? = nil
|
||||
@State private var showingAddTransaction = false
|
||||
@State private var selectedPeriod: TransactionFilter.Period = .month
|
||||
@State private var showingFilter = false
|
||||
@State private var currentFilter = TransactionFilter()
|
||||
|
||||
private var selectedAccount: AccountModel? {
|
||||
selectedAccountId.flatMap { id in
|
||||
accountsStore.getAccount(withId: id)
|
||||
} ?? accountsStore.accounts.first
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties
|
||||
private var filteredTransactions: [TransactionModel] {
|
||||
store.filterTransactions(filter: TransactionFilter(period: selectedPeriod))
|
||||
var filter = currentFilter
|
||||
filter.accountId = selectedAccountId
|
||||
return transactionsStore.filterTransactions(filter: filter)
|
||||
}
|
||||
|
||||
private var totalIncome: Double {
|
||||
filteredTransactions
|
||||
.filter { $0.type == .income }
|
||||
.reduce(0) { $0 + $1.amount }
|
||||
private func getFilteredTransactions(type: TransactionType?) -> [TransactionModel] {
|
||||
let filter = TransactionFilter(
|
||||
type: type,
|
||||
accountId: selectedAccountId,
|
||||
period: .month
|
||||
)
|
||||
return transactionsStore.filterTransactions(filter: filter)
|
||||
}
|
||||
|
||||
private var totalExpenses: Double {
|
||||
filteredTransactions
|
||||
.filter { $0.type == .expense }
|
||||
.reduce(0) { $0 + $1.amount }
|
||||
private var monthlyIncome: Double {
|
||||
let transactions = getFilteredTransactions(type: .income)
|
||||
return transactions.reduce(0) { $0 + $1.amount }
|
||||
}
|
||||
|
||||
private var expenseCategories: [(CategoryModel, Double)] {
|
||||
categoryStore.getCategories(of: .expense).compactMap { category in
|
||||
let amount = filteredTransactions
|
||||
.filter { $0.type == .expense && $0.categoryId == category.id }
|
||||
.reduce(0) { $0 + $1.amount }
|
||||
return amount > 0 ? (category, amount) : nil
|
||||
private var monthlyExpenses: Double {
|
||||
let transactions = getFilteredTransactions(type: .expense)
|
||||
return transactions.reduce(0) { $0 + $1.amount }
|
||||
}
|
||||
|
||||
private func handleAddTransaction(_ transaction: TransactionModel) {
|
||||
transactionsStore.addTransaction(transaction)
|
||||
if let accountId = transaction.accountId,
|
||||
let account = accountsStore.getAccount(withId: accountId) {
|
||||
let amount = transaction.type == .income ? transaction.amount : -transaction.amount
|
||||
let newBalance = account.balance + amount
|
||||
accountsStore.updateAccount(account.with(balance: newBalance))
|
||||
}
|
||||
.sorted { $0.1 > $1.1 }
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
balanceCard
|
||||
periodSelector
|
||||
if !expenseCategories.isEmpty { expenseCategoriesSection }
|
||||
if !filteredTransactions.isEmpty { recentTransactionsSection }
|
||||
VStack(spacing: 16) {
|
||||
// Account Cards Carousel
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 16) {
|
||||
ForEach(accountsStore.accounts) { account in
|
||||
AccountCardView(
|
||||
account: account,
|
||||
isSelected: account.id == selectedAccountId
|
||||
)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
selectedAccountId = account.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(height: 200)
|
||||
|
||||
// Stats
|
||||
HStack(spacing: 16) {
|
||||
StatCard(
|
||||
title: "Income",
|
||||
amount: monthlyIncome,
|
||||
color: .green,
|
||||
currency: settings.selectedCurrency
|
||||
)
|
||||
|
||||
StatCard(
|
||||
title: "Expenses",
|
||||
amount: monthlyExpenses,
|
||||
color: .red,
|
||||
currency: settings.selectedCurrency
|
||||
)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
// Transactions List
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Text("Transactions")
|
||||
.font(.title2.bold())
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
showingFilter = true
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
if filteredTransactions.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"No Transactions",
|
||||
systemImage: "tray.fill",
|
||||
description: Text("Add your first transaction to start tracking your finances")
|
||||
)
|
||||
} else {
|
||||
LazyVStack(spacing: 8) {
|
||||
ForEach(filteredTransactions) { transaction in
|
||||
TransactionRowView(transaction: transaction)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Dashboard")
|
||||
.toolbar {
|
||||
|
|
@ -55,144 +130,60 @@ struct DashboardView: View {
|
|||
Button {
|
||||
showingAddTransaction = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.disabled(accountsStore.accounts.isEmpty)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddTransaction) {
|
||||
NavigationView {
|
||||
AddTransactionView { transaction in
|
||||
store.addTransaction(transaction)
|
||||
handleAddTransaction(transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
private var balanceCard: some View {
|
||||
VStack(spacing: 8) {
|
||||
Text("Total Balance")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(AppCurrencyFormatter.format(accountsStore.totalBalance()))
|
||||
.font(.system(size: 34, weight: .bold))
|
||||
|
||||
HStack(spacing: 20) {
|
||||
VStack(spacing: 4) {
|
||||
Text("Income")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Text(AppCurrencyFormatter.format(totalIncome))
|
||||
.font(.headline)
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text("Expenses")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Text(AppCurrencyFormatter.format(totalExpenses))
|
||||
.font(.headline)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
private var periodSelector: some View {
|
||||
Picker("Period", selection: $selectedPeriod) {
|
||||
ForEach(TransactionFilter.Period.allCases, id: \.self) { period in
|
||||
Text(period.rawValue).tag(period)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
private var expenseCategoriesSection: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text("Top Expenses")
|
||||
.font(.headline)
|
||||
|
||||
ForEach(expenseCategories, id: \.0.id) { category, amount in
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
CategoryRowView(category: category)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text(AppCurrencyFormatter.format(amount))
|
||||
.font(.headline)
|
||||
|
||||
let percentage = totalExpenses > 0 ? (amount / totalExpenses) * 100 : 0
|
||||
Text(String(format: "%.1f%%", percentage))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if let budget = categoryStore.getBudget(for: category.id) {
|
||||
BudgetProgressView(
|
||||
spent: amount,
|
||||
total: budget.amount,
|
||||
color: category.iconColor()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if category.id != expenseCategories.last?.0.id {
|
||||
Divider()
|
||||
.sheet(isPresented: $showingFilter) {
|
||||
NavigationView {
|
||||
TransactionFilterView(filter: currentFilter) { filter in
|
||||
currentFilter = filter
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
private var recentTransactionsSection: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Text("Recent Transactions")
|
||||
.font(.headline)
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink("See All") {
|
||||
TransactionsView()
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
ForEach(Array(filteredTransactions.prefix(5))) { transaction in
|
||||
VStack {
|
||||
TransactionRowView(transaction: transaction)
|
||||
|
||||
if transaction.id != filteredTransactions.prefix(5).last?.id {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(uiColor: .secondarySystemGroupedBackground))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview
|
||||
// MARK: - Supporting Views
|
||||
|
||||
private struct StatCard: View {
|
||||
let title: String
|
||||
let amount: Double
|
||||
let color: Color
|
||||
let currency: AppSettings.Currency
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(amount.formatted(.currency(code: currency.rawValue)))
|
||||
.font(.headline)
|
||||
.foregroundColor(color)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
.background(Color(uiColor: .systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationView {
|
||||
DashboardView()
|
||||
.environmentObject(TransactionsStore.shared)
|
||||
.environmentObject(AccountsStore.shared)
|
||||
.environmentObject(CategoryStore.shared)
|
||||
.environmentObject(TransactionsStore.shared)
|
||||
.environmentObject(AppSettings.shared)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +1,22 @@
|
|||
import Foundation
|
||||
|
||||
class AccountModel: ObservableObject, Identifiable, Codable {
|
||||
struct AccountModel: Identifiable, Hashable {
|
||||
let id: String
|
||||
@Published var name: String
|
||||
@Published var balance: Double
|
||||
@Published var type: AccountType
|
||||
@Published var icon: String
|
||||
@Published var isDefault: Bool
|
||||
@Published var currency: AppSettings.Currency
|
||||
@Published var isActive: Bool
|
||||
@Published var isArchived: Bool
|
||||
|
||||
var balanceInDefaultCurrency: Double {
|
||||
// TODO: Implement currency conversion
|
||||
// Пока просто возвращаем баланс без конвертации
|
||||
balance
|
||||
}
|
||||
|
||||
static let sampleData = AccountModel(
|
||||
name: "Sample Account",
|
||||
balance: 1000,
|
||||
type: .cash,
|
||||
icon: "creditcard",
|
||||
isDefault: true,
|
||||
currency: .usd,
|
||||
isActive: true,
|
||||
isArchived: false
|
||||
)
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
case balance
|
||||
case type
|
||||
case icon
|
||||
case isDefault
|
||||
case currency
|
||||
case isActive
|
||||
case isArchived
|
||||
}
|
||||
let name: String
|
||||
var balance: Double
|
||||
let type: AccountType
|
||||
let icon: String
|
||||
let isDefault: Bool
|
||||
let currency: AppSettings.Currency
|
||||
|
||||
init(
|
||||
id: String = UUID().uuidString,
|
||||
name: String,
|
||||
balance: Double = 0,
|
||||
type: AccountType = .cash,
|
||||
icon: String = "creditcard",
|
||||
balance: Double,
|
||||
type: AccountType,
|
||||
icon: String,
|
||||
isDefault: Bool = false,
|
||||
currency: AppSettings.Currency = .usd,
|
||||
isActive: Bool = true,
|
||||
isArchived: Bool = false
|
||||
currency: AppSettings.Currency
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
|
|
@ -58,33 +25,58 @@ class AccountModel: ObservableObject, Identifiable, Codable {
|
|||
self.icon = icon
|
||||
self.isDefault = isDefault
|
||||
self.currency = currency
|
||||
self.isActive = isActive
|
||||
self.isArchived = isArchived
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
balance = try container.decode(Double.self, forKey: .balance)
|
||||
type = try container.decode(AccountType.self, forKey: .type)
|
||||
icon = try container.decode(String.self, forKey: .icon)
|
||||
isDefault = try container.decode(Bool.self, forKey: .isDefault)
|
||||
currency = try container.decode(AppSettings.Currency.self, forKey: .currency)
|
||||
isActive = try container.decode(Bool.self, forKey: .isActive)
|
||||
isArchived = try container.decode(Bool.self, forKey: .isArchived)
|
||||
func with(balance: Double) -> AccountModel {
|
||||
AccountModel(
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
balance: balance,
|
||||
type: self.type,
|
||||
icon: self.icon,
|
||||
isDefault: self.isDefault,
|
||||
currency: self.currency
|
||||
)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(balance, forKey: .balance)
|
||||
try container.encode(type, forKey: .type)
|
||||
try container.encode(icon, forKey: .icon)
|
||||
try container.encode(isDefault, forKey: .isDefault)
|
||||
try container.encode(currency, forKey: .currency)
|
||||
try container.encode(isActive, forKey: .isActive)
|
||||
try container.encode(isArchived, forKey: .isArchived)
|
||||
static func == (lhs: AccountModel, rhs: AccountModel) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview Data
|
||||
extension AccountModel {
|
||||
static let previewData = AccountModel(
|
||||
id: "preview",
|
||||
name: "Cash",
|
||||
balance: 1000,
|
||||
type: .cash,
|
||||
icon: "banknote",
|
||||
isDefault: true,
|
||||
currency: .usd
|
||||
)
|
||||
|
||||
static let previewItems: [AccountModel] = [
|
||||
previewData,
|
||||
AccountModel(
|
||||
id: "bank",
|
||||
name: "Bank Account",
|
||||
balance: 5000,
|
||||
type: .bankAccount,
|
||||
icon: "building.columns.fill",
|
||||
currency: .usd
|
||||
),
|
||||
AccountModel(
|
||||
id: "credit",
|
||||
name: "Credit Card",
|
||||
balance: -500,
|
||||
type: .creditCard,
|
||||
icon: "creditcard.fill",
|
||||
currency: .usd
|
||||
)
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,58 +5,57 @@ class AccountsStore: ObservableObject {
|
|||
|
||||
@Published private(set) var accounts: [AccountModel] = []
|
||||
|
||||
internal init() {
|
||||
loadAccounts()
|
||||
var totalBalance: Double {
|
||||
accounts.reduce(0) { $0 + $1.balance }
|
||||
}
|
||||
|
||||
// MARK: - CRUD Operations
|
||||
private init() {
|
||||
#if DEBUG
|
||||
self.accounts = AccountModel.previewItems
|
||||
#endif
|
||||
}
|
||||
|
||||
func addAccount(_ account: AccountModel) {
|
||||
if account.isDefault {
|
||||
accounts = accounts.map { acc in
|
||||
AccountModel(
|
||||
id: acc.id,
|
||||
name: acc.name,
|
||||
balance: acc.balance,
|
||||
type: acc.type,
|
||||
icon: acc.icon,
|
||||
isDefault: false,
|
||||
currency: acc.currency
|
||||
)
|
||||
}
|
||||
}
|
||||
accounts.append(account)
|
||||
saveAccounts()
|
||||
}
|
||||
|
||||
func deleteAccount(_ account: AccountModel) {
|
||||
accounts.removeAll { $0.id == account.id }
|
||||
saveAccounts()
|
||||
}
|
||||
|
||||
func updateAccount(_ account: AccountModel) {
|
||||
if let index = accounts.firstIndex(where: { $0.id == account.id }) {
|
||||
if account.isDefault {
|
||||
accounts = accounts.map { acc in
|
||||
AccountModel(
|
||||
id: acc.id,
|
||||
name: acc.name,
|
||||
balance: acc.balance,
|
||||
type: acc.type,
|
||||
icon: acc.icon,
|
||||
isDefault: false,
|
||||
currency: acc.currency
|
||||
)
|
||||
}
|
||||
}
|
||||
accounts[index] = account
|
||||
saveAccounts()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
func totalBalance() -> Double {
|
||||
accounts
|
||||
.filter { !$0.isArchived }
|
||||
.reduce(into: 0) { result, account in
|
||||
result += account.balanceInDefaultCurrency
|
||||
}
|
||||
func deleteAccount(_ account: AccountModel) {
|
||||
accounts.removeAll { $0.id == account.id }
|
||||
}
|
||||
|
||||
func getAccount(withId id: String) -> AccountModel? {
|
||||
accounts.first { $0.id == id }
|
||||
}
|
||||
|
||||
// MARK: - Storage
|
||||
|
||||
private func loadAccounts() {
|
||||
// TODO: Implement actual persistence
|
||||
let defaultAccount = AccountModel(
|
||||
name: "Cash",
|
||||
balance: 0,
|
||||
type: .cash,
|
||||
icon: "banknote",
|
||||
isDefault: true
|
||||
)
|
||||
accounts = [defaultAccount]
|
||||
}
|
||||
|
||||
private func saveAccounts() {
|
||||
// TODO: Implement actual persistence
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// Features/Models/TransactionFilter.swift
|
||||
|
||||
import Foundation
|
||||
|
||||
struct TransactionFilter {
|
||||
|
|
@ -5,10 +7,9 @@ struct TransactionFilter {
|
|||
var endDate: Date?
|
||||
var type: TransactionType?
|
||||
var categoryId: String?
|
||||
var accountId: String? // Добавляем accountId
|
||||
var accountId: String?
|
||||
var period: Period?
|
||||
|
||||
// Добавляем инициализатор
|
||||
init(
|
||||
startDate: Date? = nil,
|
||||
endDate: Date? = nil,
|
||||
|
|
@ -23,35 +24,46 @@ struct TransactionFilter {
|
|||
self.categoryId = categoryId
|
||||
self.accountId = accountId
|
||||
self.period = period
|
||||
|
||||
if let period = period {
|
||||
self.startDate = period.dateInterval.start
|
||||
self.endDate = period.dateInterval.end
|
||||
}
|
||||
}
|
||||
|
||||
enum Period: String, CaseIterable, Hashable {
|
||||
case day = "Day"
|
||||
case week = "Week"
|
||||
case month = "Month"
|
||||
case year = "Year"
|
||||
enum Period: String, CaseIterable {
|
||||
case day = "Today"
|
||||
case week = "This Week"
|
||||
case month = "This Month"
|
||||
case year = "This Year"
|
||||
|
||||
var dateInterval: (start: Date, end: Date) {
|
||||
var dateInterval: DateInterval {
|
||||
let calendar = Calendar.current
|
||||
let now = Date()
|
||||
|
||||
switch self {
|
||||
case .day:
|
||||
let start = calendar.startOfDay(for: now)
|
||||
let end = calendar.date(byAdding: .day, value: 1, to: start)!
|
||||
return (start, end)
|
||||
let end = calendar.date(byAdding: .day, value: 1, to: start) ?? now
|
||||
return DateInterval(start: start, end: end)
|
||||
|
||||
case .week:
|
||||
let start = calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: now))!
|
||||
let end = calendar.date(byAdding: .weekOfYear, value: 1, to: start)!
|
||||
return (start, end)
|
||||
if let interval = calendar.dateInterval(of: .weekOfMonth, for: now) {
|
||||
return interval
|
||||
}
|
||||
return DateInterval(start: now, end: now)
|
||||
|
||||
case .month:
|
||||
let start = calendar.date(from: calendar.dateComponents([.year, .month], from: now))!
|
||||
let end = calendar.date(byAdding: .month, value: 1, to: start)!
|
||||
return (start, end)
|
||||
if let interval = calendar.dateInterval(of: .month, for: now) {
|
||||
return interval
|
||||
}
|
||||
return DateInterval(start: now, end: now)
|
||||
|
||||
case .year:
|
||||
let start = calendar.date(from: calendar.dateComponents([.year], from: now))!
|
||||
let end = calendar.date(byAdding: .year, value: 1, to: start)!
|
||||
return (start, end)
|
||||
if let interval = calendar.dateInterval(of: .year, for: now) {
|
||||
return interval
|
||||
}
|
||||
return DateInterval(start: now, end: now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,26 @@
|
|||
import Foundation
|
||||
|
||||
struct TransactionModel: Identifiable, Codable {
|
||||
struct TransactionModel: Identifiable {
|
||||
let id: String
|
||||
var amount: Double
|
||||
var type: TransactionType
|
||||
var category: String
|
||||
var categoryId: String
|
||||
var note: String?
|
||||
var date: Date
|
||||
var accountId: String
|
||||
var originalCurrency: AppSettings.Currency
|
||||
|
||||
var isExpense: Bool { type == .expense }
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case amount
|
||||
case type
|
||||
case category
|
||||
case categoryId
|
||||
case note
|
||||
case date
|
||||
case accountId
|
||||
case originalCurrency
|
||||
}
|
||||
let amount: Double
|
||||
let type: TransactionType
|
||||
let category: String
|
||||
let categoryId: String?
|
||||
let note: String?
|
||||
let date: Date
|
||||
let accountId: String?
|
||||
let originalCurrency: AppSettings.Currency
|
||||
|
||||
init(
|
||||
id: String = UUID().uuidString,
|
||||
amount: Double,
|
||||
type: TransactionType,
|
||||
category: String,
|
||||
categoryId: String,
|
||||
categoryId: String?,
|
||||
note: String? = nil,
|
||||
date: Date = Date(),
|
||||
accountId: String,
|
||||
originalCurrency: AppSettings.Currency = AppSettings.shared.selectedCurrency
|
||||
accountId: String?,
|
||||
originalCurrency: AppSettings.Currency
|
||||
) {
|
||||
self.id = id
|
||||
self.amount = amount
|
||||
|
|
@ -46,30 +32,38 @@ struct TransactionModel: Identifiable, Codable {
|
|||
self.accountId = accountId
|
||||
self.originalCurrency = originalCurrency
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decode(String.self, forKey: .id)
|
||||
amount = try container.decode(Double.self, forKey: .amount)
|
||||
type = try container.decode(TransactionType.self, forKey: .type)
|
||||
category = try container.decode(String.self, forKey: .category)
|
||||
categoryId = try container.decode(String.self, forKey: .categoryId)
|
||||
note = try container.decodeIfPresent(String.self, forKey: .note)
|
||||
date = try container.decode(Date.self, forKey: .date)
|
||||
accountId = try container.decode(String.self, forKey: .accountId)
|
||||
originalCurrency = try container.decode(AppSettings.Currency.self, forKey: .originalCurrency)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(amount, forKey: .amount)
|
||||
try container.encode(type, forKey: .type)
|
||||
try container.encode(category, forKey: .category)
|
||||
try container.encode(categoryId, forKey: .categoryId)
|
||||
try container.encodeIfPresent(note, forKey: .note)
|
||||
try container.encode(date, forKey: .date)
|
||||
try container.encode(accountId, forKey: .accountId)
|
||||
try container.encode(originalCurrency, forKey: .originalCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preview Data
|
||||
extension TransactionModel {
|
||||
static let previewData = TransactionModel(
|
||||
amount: 42.50,
|
||||
type: .expense,
|
||||
category: "Food",
|
||||
categoryId: "food",
|
||||
note: "Lunch",
|
||||
accountId: AccountModel.previewData.id,
|
||||
originalCurrency: .usd
|
||||
)
|
||||
|
||||
static let previewItems: [TransactionModel] = [
|
||||
previewData,
|
||||
TransactionModel(
|
||||
amount: 1000,
|
||||
type: .income,
|
||||
category: "Salary",
|
||||
categoryId: "salary",
|
||||
note: "Monthly salary",
|
||||
accountId: AccountModel.previewData.id,
|
||||
originalCurrency: .usd
|
||||
),
|
||||
TransactionModel(
|
||||
amount: 15.99,
|
||||
type: .expense,
|
||||
category: "Entertainment",
|
||||
categoryId: "entertainment",
|
||||
accountId: AccountModel.previewData.id,
|
||||
originalCurrency: .usd
|
||||
)
|
||||
]
|
||||
}
|
||||
|
|
|
|||
45
Coinly/Features/Transactions/TransactionRowView.swift
Normal file
45
Coinly/Features/Transactions/TransactionRowView.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// Features/Transactions/TransactionRowView.swift
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TransactionRowView: View {
|
||||
@EnvironmentObject private var categoryStore: CategoryStore
|
||||
let transaction: TransactionModel
|
||||
|
||||
private var category: CategoryModel? {
|
||||
guard let categoryId = transaction.categoryId else { return nil }
|
||||
return categoryStore.getCategory(withId: categoryId)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if let category = category {
|
||||
CategoryRowView(category: category)
|
||||
} else {
|
||||
Image(systemName: "questionmark.circle.fill")
|
||||
.foregroundColor(.secondary)
|
||||
Text(transaction.category)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text(transaction.amount.formatted(.currency(code: transaction.originalCurrency.rawValue)))
|
||||
.font(.headline)
|
||||
.foregroundColor(transaction.type == .income ? .green : .primary)
|
||||
|
||||
Text(transaction.date.formatted(date: .abbreviated, time: .shortened))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TransactionRowView(transaction: TransactionModel.previewData)
|
||||
.environmentObject(CategoryStore.shared)
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -6,71 +6,55 @@ class TransactionsStore: ObservableObject {
|
|||
@Published private(set) var transactions: [TransactionModel] = []
|
||||
|
||||
private init() {
|
||||
loadTransactions()
|
||||
#if DEBUG
|
||||
self.transactions = TransactionModel.previewItems
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - CRUD Operations
|
||||
|
||||
func addTransaction(_ transaction: TransactionModel) {
|
||||
transactions.append(transaction)
|
||||
saveTransactions()
|
||||
}
|
||||
|
||||
func filterTransactions(filter: TransactionFilter) -> [TransactionModel] {
|
||||
transactions.filter { transaction in
|
||||
var matches = true
|
||||
|
||||
if let startDate = filter.startDate {
|
||||
matches = matches && transaction.date >= startDate
|
||||
}
|
||||
|
||||
if let endDate = filter.endDate {
|
||||
matches = matches && transaction.date < endDate
|
||||
}
|
||||
|
||||
if let type = filter.type {
|
||||
matches = matches && transaction.type == type
|
||||
}
|
||||
|
||||
if let categoryId = filter.categoryId {
|
||||
matches = matches && transaction.categoryId == categoryId
|
||||
}
|
||||
|
||||
if let accountId = filter.accountId {
|
||||
matches = matches && transaction.accountId == accountId
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
.sorted { $0.date > $1.date }
|
||||
}
|
||||
|
||||
func deleteTransaction(_ transaction: TransactionModel) {
|
||||
if let index = transactions.firstIndex(where: { $0.id == transaction.id }) {
|
||||
transactions.remove(at: index)
|
||||
saveTransactions()
|
||||
}
|
||||
transactions.removeAll { $0.id == transaction.id }
|
||||
}
|
||||
|
||||
func updateTransaction(_ transaction: TransactionModel) {
|
||||
if let index = transactions.firstIndex(where: { $0.id == transaction.id }) {
|
||||
transactions[index] = transaction
|
||||
saveTransactions()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Filtering
|
||||
|
||||
func filterTransactions(filter: TransactionFilter) -> [TransactionModel] {
|
||||
var filtered = transactions
|
||||
|
||||
if let period = filter.period {
|
||||
let interval = period.dateInterval
|
||||
filtered = filtered.filter { $0.date >= interval.start && $0.date < interval.end }
|
||||
}
|
||||
|
||||
if let startDate = filter.startDate {
|
||||
filtered = filtered.filter { $0.date >= startDate }
|
||||
}
|
||||
|
||||
if let endDate = filter.endDate {
|
||||
filtered = filtered.filter { $0.date <= endDate }
|
||||
}
|
||||
|
||||
if let type = filter.type {
|
||||
filtered = filtered.filter { $0.type == type }
|
||||
}
|
||||
|
||||
if let categoryId = filter.categoryId {
|
||||
filtered = filtered.filter { $0.categoryId == categoryId }
|
||||
}
|
||||
|
||||
if let accountId = filter.accountId {
|
||||
filtered = filtered.filter { $0.accountId == accountId }
|
||||
}
|
||||
|
||||
return filtered.sorted { $0.date > $1.date }
|
||||
}
|
||||
|
||||
// MARK: - Storage
|
||||
|
||||
private func loadTransactions() {
|
||||
// TODO: Implement actual persistence
|
||||
transactions = []
|
||||
}
|
||||
|
||||
private func saveTransactions() {
|
||||
// TODO: Implement actual persistence
|
||||
func getTransaction(withId id: String) -> TransactionModel? {
|
||||
transactions.first { $0.id == id }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import SwiftUI
|
||||
|
||||
struct AccountsSummaryView: View {
|
||||
@EnvironmentObject private var accountsStore: AccountsStore
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Text("Total Balance")
|
||||
.font(.headline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text(AppCurrencyFormatter.format(accountsStore.totalBalance()))
|
||||
.font(.system(size: 34, weight: .bold))
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 16) {
|
||||
ForEach(accountsStore.accounts) { account in
|
||||
AccountCardView(account: account)
|
||||
.frame(width: 300)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AccountsSummaryView()
|
||||
.environmentObject(AccountsStore.shared)
|
||||
.environmentObject(AppSettings.shared)
|
||||
.padding()
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import SwiftUI
|
||||
|
||||
struct TransactionRowView: View {
|
||||
@EnvironmentObject private var categoryStore: CategoryStore
|
||||
let transaction: TransactionModel
|
||||
|
||||
private var category: CategoryModel? {
|
||||
categoryStore.getCategory(withId: transaction.categoryId)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
if let category = category {
|
||||
CategoryIconView(category: category)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(category?.name ?? "Unknown Category")
|
||||
.font(.headline)
|
||||
|
||||
if let note = transaction.note {
|
||||
Text(note)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text(transaction.amount.formatAsCurrency(currency: transaction.originalCurrency))
|
||||
.font(.headline)
|
||||
.foregroundColor(transaction.type == .expense ? .red : .green)
|
||||
|
||||
Text(transaction.date.formatted(date: .abbreviated, time: .shortened))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TransactionRowView(transaction: TransactionModel(
|
||||
amount: 100,
|
||||
type: .expense,
|
||||
category: "Food",
|
||||
categoryId: CategoryModel.sampleData.id,
|
||||
accountId: "1"
|
||||
))
|
||||
.environmentObject(CategoryStore.shared)
|
||||
.padding()
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue