From df59aab47103b66b7cdb29a674052f750211070b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSamoilenkoVadym=E2=80=9D?= <“samoylenko.vadym@gmail.com”> Date: Sun, 2 Mar 2025 18:54:18 +0000 Subject: [PATCH] Add transaction filtering by period, type and categories --- .../Features/Models/TransactionFilter.swift | 57 +++++++++++++++ .../Transactions/TransactionFilterView.swift | 71 +++++++++++++++++++ .../Transactions/TransactionsView.swift | 44 ++++++++++-- 3 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 Coinly/Features/Models/TransactionFilter.swift create mode 100644 Coinly/Features/Transactions/TransactionFilterView.swift diff --git a/Coinly/Features/Models/TransactionFilter.swift b/Coinly/Features/Models/TransactionFilter.swift new file mode 100644 index 0000000..11ea04f --- /dev/null +++ b/Coinly/Features/Models/TransactionFilter.swift @@ -0,0 +1,57 @@ +// +// TransactionFilter.swift +// Coinly +// +// Created by Vadym Samoilenko on 02/03/2025. +// + + +import Foundation + +struct TransactionFilter { + enum Period { + case all + case today + case week + case month + + func filter(_ date: Date) -> Bool { + let calendar = Calendar.current + let now = Date() + + switch self { + case .all: + return true + case .today: + return calendar.isDate(date, inSameDayAs: now) + case .week: + let weekAgo = calendar.date(byAdding: .day, value: -7, to: now)! + return date >= weekAgo + case .month: + let monthAgo = calendar.date(byAdding: .month, value: -1, to: now)! + return date >= monthAgo + } + } + } + + var period: Period = .all + var selectedCategories: Set = [] + var transactionType: TransactionType? = nil + + func applies(to transaction: TransactionModel) -> Bool { + // Check period + guard period.filter(transaction.date) else { return false } + + // Check categories + if !selectedCategories.isEmpty && !selectedCategories.contains(transaction.category) { + return false + } + + // Check type + if let type = transactionType, transaction.type != type { + return false + } + + return true + } +} \ No newline at end of file diff --git a/Coinly/Features/Transactions/TransactionFilterView.swift b/Coinly/Features/Transactions/TransactionFilterView.swift new file mode 100644 index 0000000..e10e26e --- /dev/null +++ b/Coinly/Features/Transactions/TransactionFilterView.swift @@ -0,0 +1,71 @@ +import SwiftUI + +struct TransactionFilterView: View { + @Environment(\.dismiss) private var dismiss + @Binding var filter: TransactionFilter + + var body: some View { + NavigationView { + Form { + // Period Filter + Section("Time Period") { + Picker("Period", selection: $filter.period) { + Text("All Time").tag(TransactionFilter.Period.all) + Text("Today").tag(TransactionFilter.Period.today) + Text("This Week").tag(TransactionFilter.Period.week) + Text("This Month").tag(TransactionFilter.Period.month) + } + } + + // Type Filter + Section("Transaction Type") { + Picker("Type", selection: Binding( + get: { filter.transactionType ?? .expense }, + set: { filter.transactionType = $0 } + )) { + Text("All").tag(TransactionType.expense) + Text("Income").tag(TransactionType.income) + Text("Expense").tag(TransactionType.expense) + } + } + + // Categories Filter + Section("Categories") { + ForEach(CategoryModel.categories) { category in + Toggle(isOn: Binding( + get: { filter.selectedCategories.contains(category.name) }, + set: { isSelected in + if isSelected { + filter.selectedCategories.insert(category.name) + } else { + filter.selectedCategories.remove(category.name) + } + } + )) { + HStack { + Image(systemName: category.icon) + .foregroundColor(Color(category.color)) + Text(category.name) + } + } + } + } + } + .navigationTitle("Filters") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Reset") { + filter = TransactionFilter() + } + } + + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + dismiss() + } + } + } + } + } +} diff --git a/Coinly/Features/Transactions/TransactionsView.swift b/Coinly/Features/Transactions/TransactionsView.swift index 173e91d..99bef21 100644 --- a/Coinly/Features/Transactions/TransactionsView.swift +++ b/Coinly/Features/Transactions/TransactionsView.swift @@ -3,28 +3,60 @@ import SwiftUI struct TransactionsView: View { @ObservedObject var store: TransactionsStore @State private var showingAddTransaction = false + @State private var showingFilters = false + @State private var filter = TransactionFilter() + + var filteredTransactions: [TransactionModel] { + store.transactions.filter { filter.applies(to: $0) } + } var body: some View { NavigationView { List { - ForEach(store.transactions) { transaction in + ForEach(filteredTransactions) { transaction in TransactionRowView(transaction: transaction) } .onDelete { indexSet in - store.deleteTransaction(at: indexSet) + // Преобразуем индексы отфильтрованного списка в индексы полного списка + let transactionsToDelete = indexSet.map { filteredTransactions[$0] } + for transaction in transactionsToDelete { + if let index = store.transactions.firstIndex(where: { $0.id == transaction.id }) { + store.deleteTransaction(at: IndexSet([index])) + } + } } } .navigationTitle("Transactions") .toolbar { - Button { - showingAddTransaction = true - } label: { - Image(systemName: "plus") + ToolbarItem(placement: .navigationBarLeading) { + Button { + showingFilters = true + } label: { + Image(systemName: "line.3.horizontal.decrease.circle") + .foregroundColor(hasActiveFilters ? .blue : .gray) + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button { + showingAddTransaction = true + } label: { + Image(systemName: "plus") + } } } .sheet(isPresented: $showingAddTransaction) { AddTransactionView(addTransaction: store.addTransaction) } + .sheet(isPresented: $showingFilters) { + TransactionFilterView(filter: $filter) + } } } + + private var hasActiveFilters: Bool { + filter.period != .all || + !filter.selectedCategories.isEmpty || + filter.transactionType != nil + } }