This commit is contained in:
“SamoilenkoVadym” 2025-03-03 10:36:57 +00:00
parent 739dd17b05
commit d957385a17
5 changed files with 282 additions and 203 deletions

View file

@ -15,6 +15,13 @@ struct ContentView: View {
Label("Dashboard", systemImage: "chart.pie.fill")
}
NavigationView {
TransactionsView()
}
.tabItem {
Label("Transactions", systemImage: "arrow.left.arrow.right")
}
NavigationView {
AccountsListView()
}

View file

@ -4,123 +4,59 @@ struct DashboardView: View {
@EnvironmentObject private var accountsStore: AccountsStore
@EnvironmentObject private var transactionsStore: TransactionsStore
@EnvironmentObject private var settings: AppSettings
@State private var selectedAccountId: AccountModel.ID? = nil
@State private var showingAddTransaction = false
@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
}
private var filteredTransactions: [TransactionModel] {
var filter = currentFilter
filter.accountId = selectedAccountId
return transactionsStore.filterTransactions(filter: filter)
}
private func getFilteredTransactions(type: TransactionType?) -> [TransactionModel] {
let filter = TransactionFilter(
type: type,
accountId: selectedAccountId,
period: .month
)
return transactionsStore.filterTransactions(filter: filter)
}
private var monthlyIncome: Double {
let transactions = getFilteredTransactions(type: .income)
return transactions.reduce(0) { $0 + $1.amount }
}
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))
}
}
@State private var selectedAccountId: String?
var body: some View {
ScrollView {
VStack(spacing: 16) {
// Account Cards Carousel
List {
Section {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 16) {
LazyHStack(spacing: 12) {
ForEach(accountsStore.accounts) { account in
AccountCardView(
account: account,
isSelected: account.id == selectedAccountId
isSelected: selectedAccountId == account.id
)
.onTapGesture {
withAnimation {
selectedAccountId = account.id
}
selectedAccountId = account.id
}
}
}
.padding(.horizontal)
}
.frame(height: 200)
.frame(height: 160)
}
Section {
StatCard(
title: "Income",
amount: totalIncome,
color: .green,
currency: settings.selectedCurrency
)
// Stats
HStack(spacing: 16) {
StatCard(
title: "Income",
amount: monthlyIncome,
color: .green,
currency: settings.selectedCurrency
)
StatCard(
title: "Expenses",
amount: monthlyExpenses,
color: .red,
currency: settings.selectedCurrency
)
StatCard(
title: "Expenses",
amount: totalExpenses,
color: .red,
currency: settings.selectedCurrency
)
}
Section {
ForEach(filteredTransactions) { transaction in
TransactionRowView(transaction: transaction)
}
.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)
}
}
} header: {
HStack {
Text("Recent Transactions")
Spacer()
Button("See All") {
// Navigate to transactions
}
.font(.caption)
}
}
}
@ -145,16 +81,36 @@ struct DashboardView: View {
}
.sheet(isPresented: $showingFilter) {
NavigationView {
TransactionFilterView(filter: currentFilter) { filter in
currentFilter = filter
}
TransactionFilterView(filter: $currentFilter)
}
}
}
private var filteredTransactions: [TransactionModel] {
transactionsStore.filterTransactions(filter: currentFilter)
}
private var totalIncome: Double {
filteredTransactions
.filter { $0.type == .income }
.reduce(0) { $0 + $1.amount }
}
private var totalExpenses: Double {
filteredTransactions
.filter { $0.type == .expense }
.reduce(0) { $0 + $1.amount }
}
private func handleAddTransaction(_ transaction: TransactionModel) {
transactionsStore.addTransaction(transaction)
if let account = accountsStore.getAccount(withId: transaction.accountId ?? "") {
let newBalance = account.balance + (transaction.type == .income ? transaction.amount : -transaction.amount)
accountsStore.updateAccount(account.with(balance: newBalance))
}
}
}
// MARK: - Supporting Views
private struct StatCard: View {
let title: String
let amount: Double
@ -162,20 +118,15 @@ private struct StatCard: View {
let currency: AppSettings.Currency
var body: some View {
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.subheadline)
.font(.caption)
.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)
.padding(.vertical, 8)
}
}

View file

@ -2,99 +2,92 @@ import SwiftUI
struct TransactionFilterView: View {
@Environment(\.dismiss) private var dismiss
let filter: TransactionFilter
let onApply: (TransactionFilter) -> Void
@Binding var filter: TransactionFilter
@State private var temporaryFilter: TransactionFilter
@EnvironmentObject private var accountsStore: AccountsStore
@EnvironmentObject private var categoryStore: CategoryStore
@State private var type: TransactionType?
@State private var period: TransactionFilter.Period?
@State private var startDate: Date?
@State private var endDate: Date?
init(filter: TransactionFilter, onApply: @escaping (TransactionFilter) -> Void) {
self.filter = filter
self.onApply = onApply
_type = State(initialValue: filter.type)
_period = State(initialValue: filter.period)
_startDate = State(initialValue: filter.startDate)
_endDate = State(initialValue: filter.endDate)
init(filter: Binding<TransactionFilter>) {
self._filter = filter
self._temporaryFilter = State(initialValue: filter.wrappedValue)
}
var body: some View {
List {
Section {
Picker("Period", selection: $period) {
Text("All Time").tag(TransactionFilter.Period?.none)
Form {
Section("Period") {
Picker("Period", selection: $temporaryFilter.period) {
Text("All Time").tag(Optional<TransactionFilter.Period>.none)
ForEach(TransactionFilter.Period.allCases, id: \.self) { period in
Text(period.rawValue).tag(Optional(period))
}
}
}
Section {
Picker("Type", selection: $type) {
Text("All Types").tag(TransactionType?.none)
Section("Type") {
Picker("Type", selection: $temporaryFilter.type) {
Text("All").tag(Optional<TransactionType>.none)
ForEach(TransactionType.allCases, id: \.self) { type in
Text(type.rawValue).tag(Optional(type))
}
}
}
Section {
DatePicker(
"Start Date",
selection: Binding(
get: { startDate ?? Date() },
set: { startDate = $0 }
),
displayedComponents: .date
)
.onChange(of: startDate) { oldValue, newValue in
period = nil
}
DatePicker(
"End Date",
selection: Binding(
get: { endDate ?? Date() },
set: { endDate = $0 }
),
displayedComponents: .date
)
.onChange(of: endDate) { oldValue, newValue in
period = nil
Section("Account") {
NavigationLink {
AccountPickerView { account in
temporaryFilter.accountId = account.id
}
} label: {
HStack {
Text("Account")
Spacer()
if let accountId = temporaryFilter.accountId,
let account = accountsStore.getAccount(withId: accountId) {
Text(account.name)
.foregroundColor(.secondary)
} else {
Text("All")
.foregroundColor(.secondary)
}
}
}
}
Section {
Button("Clear Filter") {
type = nil
period = nil
startDate = nil
endDate = nil
Section("Category") {
NavigationLink {
CategoryPickerView(
transactionType: temporaryFilter.type ?? .expense
) { category in
temporaryFilter.categoryId = category.id
}
} label: {
HStack {
Text("Category")
Spacer()
if let categoryId = temporaryFilter.categoryId,
let category = categoryStore.getCategory(withId: categoryId) {
Text(category.name)
.foregroundColor(.secondary)
} else {
Text("All")
.foregroundColor(.secondary)
}
}
}
.foregroundColor(.red)
}
}
.navigationTitle("Filter")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
Button("Reset") {
temporaryFilter = TransactionFilter()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Apply") {
let newFilter = TransactionFilter(
startDate: startDate,
endDate: endDate,
type: type,
categoryId: filter.categoryId,
accountId: filter.accountId,
period: period
)
onApply(newFilter)
filter = temporaryFilter
dismiss()
}
}
@ -104,8 +97,8 @@ struct TransactionFilterView: View {
#Preview {
NavigationView {
TransactionFilterView(
filter: TransactionFilter()
) { _ in }
TransactionFilterView(filter: .constant(TransactionFilter()))
.environmentObject(AccountsStore.shared)
.environmentObject(CategoryStore.shared)
}
}

View file

@ -0,0 +1,60 @@
//
// TransactionsSummaryView.swift
// Coinly
//
// Created by Vadym Samoilenko on 03/03/2025.
//
import SwiftUI
struct TransactionsSummaryView: View {
let transactions: [TransactionModel]
@EnvironmentObject private var settings: AppSettings
private var income: Double {
transactions
.filter { $0.type == .income }
.reduce(0) { $0 + $1.amount }
}
private var expenses: Double {
transactions
.filter { $0.type == .expense }
.reduce(0) { $0 + $1.amount }
}
var body: some View {
VStack(spacing: 8) {
HStack {
VStack(alignment: .leading) {
Text("Income")
.font(.caption)
.foregroundColor(.secondary)
Text(income.formatted(.currency(code: settings.selectedCurrency.rawValue)))
.font(.headline)
.foregroundColor(.green)
}
Spacer()
VStack(alignment: .trailing) {
Text("Expenses")
.font(.caption)
.foregroundColor(.secondary)
Text(expenses.formatted(.currency(code: settings.selectedCurrency.rawValue)))
.font(.headline)
.foregroundColor(.primary)
}
}
}
.padding(.vertical, 8)
.textCase(nil)
}
}
#Preview {
TransactionsSummaryView(transactions: TransactionModel.previewItems)
.environmentObject(AppSettings.shared)
.padding()
}

View file

@ -1,44 +1,76 @@
import SwiftUI
struct TransactionsView: View {
@EnvironmentObject private var store: TransactionsStore
@State private var searchText = ""
@State private var showingFilter = false
@EnvironmentObject private var transactionsStore: TransactionsStore
@EnvironmentObject private var accountsStore: AccountsStore
@EnvironmentObject private var categoryStore: CategoryStore
@State private var showingAddTransaction = false
var title: String
var filter: TransactionFilter
init(title: String = "Transactions", filter: TransactionFilter = TransactionFilter()) {
self.title = title
self.filter = filter
}
@State private var showingFilter = false
@State private var currentFilter = TransactionFilter()
private var filteredTransactions: [TransactionModel] {
store.filterTransactions(filter: filter)
transactionsStore.filterTransactions(filter: currentFilter)
}
var body: some View {
List {
ForEach(filteredTransactions) { transaction in
TransactionRowView(transaction: transaction)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
store.deleteTransaction(transaction)
} label: {
Label("Delete", systemImage: "trash")
if !filteredTransactions.isEmpty {
Section {
ForEach(filteredTransactions) { transaction in
TransactionRowView(transaction: transaction)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deleteTransaction(transaction)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
} header: {
VStack(spacing: 8) {
HStack {
VStack(alignment: .leading) {
Text("Income")
.font(.caption)
.foregroundColor(.secondary)
Text(totalIncome.formatted(.currency(code: "USD")))
.font(.headline)
.foregroundColor(.green)
}
Spacer()
VStack(alignment: .trailing) {
Text("Expenses")
.font(.caption)
.foregroundColor(.secondary)
Text(totalExpenses.formatted(.currency(code: "USD")))
.font(.headline)
.foregroundColor(.red)
}
}
HStack {
Text("Balance")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text(balance.formatted(.currency(code: "USD")))
.font(.headline)
}
}
.textCase(nil)
.padding(.vertical, 8)
}
}
}
.navigationTitle(title)
.searchable(text: $searchText, prompt: "Search transactions")
.navigationTitle("Transactions")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showingAddTransaction = true
} label: {
Image(systemName: "plus")
Image(systemName: "plus.circle.fill")
}
}
@ -53,17 +85,53 @@ struct TransactionsView: View {
.sheet(isPresented: $showingAddTransaction) {
NavigationView {
AddTransactionView { transaction in
store.addTransaction(transaction)
transactionsStore.addTransaction(transaction)
if let account = accountsStore.getAccount(withId: transaction.accountId ?? "") {
let newBalance = account.balance + (transaction.type == .income ? transaction.amount : -transaction.amount)
accountsStore.updateAccount(account.with(balance: newBalance))
}
}
}
}
.sheet(isPresented: $showingFilter) {
NavigationView {
TransactionFilterView(filter: filter) { newFilter in
// Handle filter update if needed
}
TransactionFilterView(filter: $currentFilter)
}
}
.overlay {
if filteredTransactions.isEmpty {
ContentUnavailableView(
"No Transactions",
systemImage: "arrow.left.arrow.right",
description: Text("Add your first transaction to start tracking your finances")
)
}
}
}
private var totalIncome: Double {
filteredTransactions
.filter { $0.type == .income }
.reduce(0) { $0 + $1.amount }
}
private var totalExpenses: Double {
filteredTransactions
.filter { $0.type == .expense }
.reduce(0) { $0 + $1.amount }
}
private var balance: Double {
totalIncome - totalExpenses
}
private func deleteTransaction(_ transaction: TransactionModel) {
if let account = accountsStore.getAccount(withId: transaction.accountId ?? "") {
let amount = transaction.type == .income ? -transaction.amount : transaction.amount
let newBalance = account.balance + amount
accountsStore.updateAccount(account.with(balance: newBalance))
}
transactionsStore.deleteTransaction(transaction)
}
}
@ -71,7 +139,7 @@ struct TransactionsView: View {
NavigationView {
TransactionsView()
.environmentObject(TransactionsStore.shared)
.environmentObject(AccountsStore.shared)
.environmentObject(CategoryStore.shared)
.environmentObject(AppSettings.shared)
}
}