Update project structure and implement basic functionality

This commit is contained in:
“SamoilenkoVadym” 2025-03-03 02:10:58 +00:00
parent fcaf3c5105
commit 5861b34737
14 changed files with 567 additions and 568 deletions

View file

@ -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
)
}

View file

@ -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)
}
}

View file

@ -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()
}

View 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))
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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
)
]
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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
)
]
}

View 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()
}

View file

@ -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 }
}
}

View file

@ -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()
}

View file

@ -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()
}