Update
This commit is contained in:
parent
739dd17b05
commit
d957385a17
5 changed files with 282 additions and 203 deletions
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
60
Coinly/Features/Transactions/TransactionsSummaryView.swift
Normal file
60
Coinly/Features/Transactions/TransactionsSummaryView.swift
Normal 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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue