Add category and budget models

This commit is contained in:
“SamoilenkoVadym” 2025-03-02 23:04:42 +00:00
parent d7d8de38f1
commit 607493fdd0
7 changed files with 435 additions and 0 deletions

View file

View file

@ -0,0 +1,125 @@
import SwiftUI
struct BudgetSettingsView: View {
@ObservedObject var categoryStore: CategoryStore
@State private var selectedCategory: CategoryModel?
var body: some View {
List {
Section {
NavigationLink {
MonthlyBudgetView(categoryStore: categoryStore)
} label: {
HStack {
Text("Monthly Budget")
Spacer()
Text("2000")
.foregroundColor(.secondary)
}
}
}
Section("Category Budgets") {
ForEach(categoryStore.getCategories(of: .expense)) { category in
HStack {
CategoryRowView(category: category)
Button {
selectedCategory = category
} label: {
if let budget = categoryStore.getBudget(for: category.id) {
Text(budget.amount.formatAsCurrency())
.foregroundColor(.secondary)
} else {
Text("Set Budget")
.foregroundColor(.accentColor)
}
}
}
}
}
}
.navigationTitle("Budget Settings")
.sheet(item: $selectedCategory) { category in
NavigationView {
CategoryBudgetView(
category: category,
budget: categoryStore.getBudget(for: category.id)
) { budget in
if categoryStore.getBudget(for: category.id) != nil {
categoryStore.updateBudget(budget)
} else {
categoryStore.addBudget(budget)
}
}
}
}
}
}
struct CategoryBudgetView: View {
@Environment(\.dismiss) private var dismiss
let category: CategoryModel
let budget: BudgetModel?
let onSave: (BudgetModel) -> Void
@State private var amount: String = ""
@State private var period: BudgetModel.BudgetPeriod = .monthly
@State private var isRecurring = true
var body: some View {
Form {
Section {
CategoryRowView(category: category)
.listRowBackground(Color.clear)
}
Section("Budget Details") {
HStack {
Text(AppSettings.shared.selectedCurrency.symbol)
TextField("Amount", text: $amount)
.keyboardType(.decimalPad)
}
Picker("Period", selection: $period) {
ForEach(BudgetModel.BudgetPeriod.allCases, id: \.self) { period in
Text(period.rawValue).tag(period)
}
}
Toggle("Recurring", isOn: $isRecurring)
}
}
.navigationTitle("Category Budget")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
guard let amountValue = Double(amount) else { return }
let budget = BudgetModel(
categoryId: category.id,
amount: amountValue,
period: period,
isRecurring: isRecurring
)
onSave(budget)
dismiss()
}
.disabled(amount.isEmpty)
}
}
.onAppear {
if let existingBudget = budget {
amount = existingBudget.amount.description
period = existingBudget.period
isRecurring = existingBudget.isRecurring
}
}
}
}

View file

@ -0,0 +1,90 @@
import SwiftUI
struct AddCategoryView: View {
@Environment(\.dismiss) private var dismiss
let type: TransactionType
let onSave: (CategoryModel) -> Void
@State private var name = ""
@State private var selectedIcon = "cart.fill"
@State private var selectedColor = "blue"
private let icons = [
"cart.fill", "car.fill", "house.fill", "creditcard.fill",
"gift.fill", "heart.fill", "tv.fill", "gamecontroller.fill",
"airplane", "cross.fill", "doc.text.fill", "bag.fill",
"dollarsign.circle.fill", "chart.line.uptrend.xyaxis",
"briefcase.fill", "gift.fill", "leaf.fill", "graduationcap.fill"
]
private let colors = [
"red", "blue", "green", "yellow",
"purple", "orange", "pink", "indigo"
]
var body: some View {
Form {
Section("Basic Information") {
TextField("Category Name", text: $name)
}
Section("Icon") {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: 16) {
ForEach(icons, id: \.self) { icon in
Image(systemName: icon)
.font(.title2)
.frame(width: 44, height: 44)
.background(selectedIcon == icon ? Color.accentColor : Color.clear)
.foregroundColor(selectedIcon == icon ? .white : .primary)
.cornerRadius(8)
.onTapGesture {
selectedIcon = icon
}
}
}
}
Section("Color") {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 8), spacing: 16) {
ForEach(colors, id: \.self) { color in
Circle()
.fill(CategoryModel(name: "", icon: "", color: color, type: .expense).iconColor())
.frame(width: 32, height: 32)
.overlay {
if selectedColor == color {
Image(systemName: "checkmark")
.foregroundColor(.white)
}
}
.onTapGesture {
selectedColor = color
}
}
}
}
}
.navigationTitle("New Category")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
let category = CategoryModel(
name: name,
icon: selectedIcon,
color: selectedColor,
type: type
)
onSave(category)
dismiss()
}
.disabled(name.isEmpty)
}
}
}
}

View file

@ -0,0 +1,91 @@
import SwiftUI
struct CategoryListView: View {
@ObservedObject var categoryStore: CategoryStore
@State private var showingAddCategory = false
@State private var selectedType: TransactionType = .expense
var body: some View {
List {
Picker("Type", selection: $selectedType) {
Text("Expenses").tag(TransactionType.expense)
Text("Income").tag(TransactionType.income)
}
.pickerStyle(.segmented)
.listRowBackground(Color.clear)
.padding(.vertical, 8)
ForEach(categoryStore.getCategories(of: selectedType)) { category in
CategoryRowView(category: category)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if !category.isDefault {
Button(role: .destructive) {
categoryStore.deleteCategory(category)
} label: {
Label("Delete", systemImage: "trash")
}
}
Button {
// Edit category
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.blue)
}
}
}
.navigationTitle("Categories")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showingAddCategory = true
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showingAddCategory) {
NavigationView {
AddCategoryView(type: selectedType) { newCategory in
categoryStore.addCategory(newCategory)
}
}
}
}
}
struct CategoryRowView: View {
let category: CategoryModel
var body: some View {
HStack(spacing: 16) {
Image(systemName: category.icon)
.font(.title2)
.foregroundColor(.white)
.frame(width: 36, height: 36)
.background(category.iconColor())
.cornerRadius(8)
VStack(alignment: .leading, spacing: 4) {
Text(category.name)
.font(.headline)
if category.isDefault {
Text("Default")
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
if let budget = CategoryStore.shared.getBudget(for: category.id) {
Text(budget.amount.formatAsCurrency())
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
}

View file

@ -0,0 +1,40 @@
import Foundation
struct BudgetModel: Identifiable, Codable {
let id: String
var categoryId: String
var amount: Double
var period: BudgetPeriod
var startDate: Date
var isRecurring: Bool
enum BudgetPeriod: String, Codable, CaseIterable {
case weekly = "Weekly"
case monthly = "Monthly"
case yearly = "Yearly"
var durationInDays: Int {
switch self {
case .weekly: return 7
case .monthly: return 30
case .yearly: return 365
}
}
}
init(
id: String = UUID().uuidString,
categoryId: String,
amount: Double,
period: BudgetPeriod = .monthly,
startDate: Date = Date(),
isRecurring: Bool = true
) {
self.id = id
self.categoryId = categoryId
self.amount = amount
self.period = period
self.startDate = startDate
self.isRecurring = isRecurring
}
}

View file

@ -0,0 +1,60 @@
import Foundation
import Combine
class CategoryStore: ObservableObject {
@Published private(set) var categories: [CategoryModel] = []
@Published private(set) var budgets: [BudgetModel] = []
init() {
categories = CategoryModel.defaultCategories
}
// MARK: - Categories
func addCategory(_ category: CategoryModel) {
categories.append(category)
}
func updateCategory(_ category: CategoryModel) {
guard let index = categories.firstIndex(where: { $0.id == category.id }) else { return }
categories[index] = category
}
func deleteCategory(_ category: CategoryModel) {
guard !category.isDefault else { return }
categories.removeAll { $0.id == category.id }
}
func getCategories(of type: TransactionType) -> [CategoryModel] {
categories.filter { $0.type == type }
}
// MARK: - Budgets
func addBudget(_ budget: BudgetModel) {
budgets.append(budget)
}
func updateBudget(_ budget: BudgetModel) {
guard let index = budgets.firstIndex(where: { $0.id == budget.id }) else { return }
budgets[index] = budget
}
func deleteBudget(_ budget: BudgetModel) {
budgets.removeAll { $0.id == budget.id }
}
func getBudget(for categoryId: String) -> BudgetModel? {
budgets.first { $0.categoryId == categoryId }
}
func getSpentPercentage(for categoryId: String, transactions: [TransactionModel]) -> Double {
guard let budget = getBudget(for: categoryId) else { return 0 }
let spent = transactions
.filter { $0.categoryId == categoryId }
.reduce(0) { $0 + $1.amount }
return spent / budget.amount
}
}

View file

@ -0,0 +1,29 @@
import SwiftUI
struct CategoryRowView: View {
let category: CategoryModel
var body: some View {
HStack(spacing: 16) {
Image(systemName: category.icon)
.font(.title2)
.foregroundColor(.white)
.frame(width: 36, height: 36)
.background(category.iconColor())
.cornerRadius(8)
VStack(alignment: .leading, spacing: 4) {
Text(category.name)
.font(.headline)
if category.isDefault {
Text("Default")
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
}
}
}