diff --git a/.DS_Store b/.DS_Store index 72ad433..25956a8 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Expenso.xcodeproj/project.pbxproj b/Expenso.xcodeproj/project.pbxproj index e1672fd..066a38e 100644 --- a/Expenso.xcodeproj/project.pbxproj +++ b/Expenso.xcodeproj/project.pbxproj @@ -7,12 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 24A10DD02856809200C0BA52 /* Expenso.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 24A10DCE2856809200C0BA52 /* Expenso.xcdatamodeld */; }; + 24D9168D285735920025227B /* MonthlyTransactionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D9168C285735920025227B /* MonthlyTransactionSettingsView.swift */; }; 736C720A25CFD89900720DEA /* empty-face.json in Resources */ = {isa = PBXBuildFile; fileRef = 736C720925CFD89900720DEA /* empty-face.json */; }; 736C721B25CFE8E200720DEA /* LottieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 736C721A25CFE8E200720DEA /* LottieView.swift */; }; 738B1C1825C65DFE0067407B /* ExpensoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B1C1725C65DFE0067407B /* ExpensoApp.swift */; }; 738B1C1C25C65E060067407B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 738B1C1B25C65E060067407B /* Assets.xcassets */; }; 738B1C1F25C65E060067407B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 738B1C1E25C65E060067407B /* Preview Assets.xcassets */; }; - 738B1C2425C65E060067407B /* Expenso.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 738B1C2225C65E060067407B /* Expenso.xcdatamodeld */; }; 738B1C2F25C65EF70067407B /* Configs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B1C2E25C65EF70067407B /* Configs.swift */; }; 738B1C3325C660140067407B /* ExpenseCD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B1C3225C660140067407B /* ExpenseCD.swift */; }; 738B1C3825C661750067407B /* ExpenseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B1C3725C661750067407B /* ExpenseView.swift */; }; @@ -53,6 +54,8 @@ /* Begin PBXFileReference section */ 1B0E373FB89DD0864398FEE7 /* Pods_Expenso.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Expenso.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 24A10DCF2856809200C0BA52 /* Expenso.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Expenso.xcdatamodel; sourceTree = ""; }; + 24D9168C285735920025227B /* MonthlyTransactionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthlyTransactionSettingsView.swift; sourceTree = ""; }; 2C2D1600DAC62FA04EDCF170 /* Pods-Expenso.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Expenso.debug.xcconfig"; path = "Target Support Files/Pods-Expenso/Pods-Expenso.debug.xcconfig"; sourceTree = ""; }; 736C720925CFD89900720DEA /* empty-face.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "empty-face.json"; sourceTree = ""; }; 736C721A25CFE8E200720DEA /* LottieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieView.swift; sourceTree = ""; }; @@ -60,7 +63,6 @@ 738B1C1725C65DFE0067407B /* ExpensoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpensoApp.swift; sourceTree = ""; }; 738B1C1B25C65E060067407B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 738B1C1E25C65E060067407B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 738B1C2325C65E060067407B /* Expenso.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Expenso.xcdatamodel; sourceTree = ""; }; 738B1C2525C65E060067407B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 738B1C2E25C65EF70067407B /* Configs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configs.swift; sourceTree = ""; }; 738B1C3225C660140067407B /* ExpenseCD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpenseCD.swift; sourceTree = ""; }; @@ -112,9 +114,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 24D9168B285735730025227B /* MonthlyTransactionSettings */ = { + isa = PBXGroup; + children = ( + 24D9168C285735920025227B /* MonthlyTransactionSettingsView.swift */, + ); + path = MonthlyTransactionSettings; + sourceTree = ""; + }; 733763A827120DB600AA983A /* Screens */ = { isa = PBXGroup; children = ( + 24D9168B285735730025227B /* MonthlyTransactionSettings */, 739DFF7925DC1E23005BD5C8 /* Authenticate */, 738B1C8E25C680C50067407B /* About */, 738B1C8925C67D790067407B /* ExpenseFilter */, @@ -180,7 +191,7 @@ 738B1C1725C65DFE0067407B /* ExpensoApp.swift */, 738B1C1B25C65E060067407B /* Assets.xcassets */, 738B1C2525C65E060067407B /* Info.plist */, - 738B1C2225C65E060067407B /* Expenso.xcdatamodeld */, + 24A10DCE2856809200C0BA52 /* Expenso.xcdatamodeld */, 738B1C1D25C65E060067407B /* Preview Content */, 738B1C2E25C65EF70067407B /* Configs.swift */, ); @@ -454,7 +465,7 @@ 738B1C9025C680D20067407B /* AboutView.swift in Sources */, 738B1C4F25C663180067407B /* DateExtension.swift in Sources */, 738B1C5225C6632F0067407B /* HelperMethods.swift in Sources */, - 738B1C2425C65E060067407B /* Expenso.xcdatamodeld in Sources */, + 24A10DD02856809200C0BA52 /* Expenso.xcdatamodeld in Sources */, 738B1C8B25C67D880067407B /* ExpenseFilterView.swift in Sources */, 739DFF7B25DC1E3C005BD5C8 /* AuthenticateView.swift in Sources */, 738B1C3325C660140067407B /* ExpenseCD.swift in Sources */, @@ -463,6 +474,7 @@ 738B1C3C25C662580067407B /* Models.swift in Sources */, 753CBDD325F36864005762B8 /* BiometricAuthUtility.swift in Sources */, 738B1C7325C674320067407B /* ExpenseSettingsView.swift in Sources */, + 24D9168D285735920025227B /* MonthlyTransactionSettingsView.swift in Sources */, 75C6B48025F37FD20079BCFC /* AuthenticationViewModel.swift in Sources */, 738B1C4C25C663060067407B /* DismissKeyboard.swift in Sources */, 738B1C4625C662D90067407B /* TextView.swift in Sources */, @@ -670,12 +682,12 @@ /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ - 738B1C2225C65E060067407B /* Expenso.xcdatamodeld */ = { + 24A10DCE2856809200C0BA52 /* Expenso.xcdatamodeld */ = { isa = XCVersionGroup; children = ( - 738B1C2325C65E060067407B /* Expenso.xcdatamodel */, + 24A10DCF2856809200C0BA52 /* Expenso.xcdatamodel */, ); - currentVersion = 738B1C2325C65E060067407B /* Expenso.xcdatamodel */; + currentVersion = 24A10DCF2856809200C0BA52 /* Expenso.xcdatamodel */; path = Expenso.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Expenso/Expenso.xcdatamodeld/Expenso.xcdatamodel/contents b/Expenso/Expenso.xcdatamodeld/Expenso.xcdatamodel/contents index 11b08a8..9c162fc 100644 --- a/Expenso/Expenso.xcdatamodeld/Expenso.xcdatamodel/contents +++ b/Expenso/Expenso.xcdatamodeld/Expenso.xcdatamodel/contents @@ -1,8 +1,9 @@ - + + @@ -12,6 +13,6 @@ - + \ No newline at end of file diff --git a/Expenso/Library/CoreData/ExpenseCD.swift b/Expenso/Library/CoreData/ExpenseCD.swift index ac81dee..43fccfb 100644 --- a/Expenso/Library/CoreData/ExpenseCD.swift +++ b/Expenso/Library/CoreData/ExpenseCD.swift @@ -20,6 +20,12 @@ enum ExpenseCDFilterTime: String { case month } +@objc +public enum Frequency: Int16 { + case onetime + case monthly +} + public class ExpenseCD: NSManagedObject, Identifiable { @NSManaged public var createdAt: Date? @NSManaged public var updatedAt: Date? @@ -30,6 +36,13 @@ public class ExpenseCD: NSManagedObject, Identifiable { @NSManaged public var note: String? @NSManaged public var amount: Double @NSManaged public var imageAttached: Data? + + @NSManaged public var frequencyValue: Int16 + + var frequency: Frequency { + get { return Frequency.init(rawValue: frequencyValue) ?? .onetime} + set { frequencyValue = newValue.rawValue} + } } extension ExpenseCD { @@ -50,4 +63,13 @@ extension ExpenseCD { request.sortDescriptors = [sortDescriptor] return request } + + static func sortExpenseDataByFrequency(sortBy: ExpenseCDSort = .occuredOn, frequency: Frequency, ascending: Bool = true) -> NSFetchRequest { + let request: NSFetchRequest = ExpenseCD.fetchRequest() as! NSFetchRequest + let sortDescriptor = NSSortDescriptor(key: sortBy.rawValue, ascending: ascending) + let predicate = NSPredicate(format: "frequencyValue == %i", frequency.rawValue) + request.predicate = predicate + request.sortDescriptors = [sortDescriptor] + return request + } } diff --git a/Expenso/Screens/AddExpense/AddExpenseView.swift b/Expenso/Screens/AddExpense/AddExpenseView.swift index feb69fb..1ab89e2 100644 --- a/Expenso/Screens/AddExpense/AddExpenseView.swift +++ b/Expenso/Screens/AddExpense/AddExpenseView.swift @@ -17,6 +17,8 @@ struct AddExpenseView: View { @StateObject var viewModel: AddExpenseViewModel + var hasToggle: Bool = true + let typeOptions = [ DropdownOption(key: TRANS_TYPE_INCOME, val: "Income"), DropdownOption(key: TRANS_TYPE_EXPENSE, val: "Expense") @@ -114,6 +116,16 @@ struct AddExpenseView: View { .background(Color.secondary_color) .cornerRadius(4) + if hasToggle { + //MARK: User can define Transaction as monthly when it is created or change it to onetime, when in the Monthly Transaction View. The User is not allowed to change the state from onetime to monthly in the recent transaction list to prevent that the transaction is done more than one time monthly. If it should be monthly the user has to created a new Expense + Toggle("monthly", isOn: $viewModel.monthlyFrequency) + .padding(5) + .accentColor(Color.text_primary_color) + .frame(height: 50).padding(.leading, 16) + .background(Color.secondary_color) + .cornerRadius(4) + } + Button(action: { viewModel.attachImage() }, label: { HStack { Image(systemName: "paperclip") @@ -134,6 +146,7 @@ struct AddExpenseView: View { ]) } + if let image = viewModel.imageAttached { Button(action: { showAttachSheet = true }, label: { Image(uiImage: image) @@ -146,7 +159,6 @@ struct AddExpenseView: View { } Spacer().frame(height: 150) - Spacer() } .frame(maxWidth: .infinity).padding(.horizontal, 8) .alert(isPresented: $viewModel.showAlert, diff --git a/Expenso/Screens/AddExpense/AddExpenseViewModel.swift b/Expenso/Screens/AddExpense/AddExpenseViewModel.swift index 085c918..31385fb 100644 --- a/Expenso/Screens/AddExpense/AddExpenseViewModel.swift +++ b/Expenso/Screens/AddExpense/AddExpenseViewModel.swift @@ -27,12 +27,13 @@ class AddExpenseViewModel: ObservableObject { @Published var imageUpdated = false // When transaction edit, check if attachment is updated? @Published var imageAttached: UIImage? = nil + @Published var monthlyFrequency = false + @Published var alertMsg = String() @Published var showAlert = false @Published var closePresenter = false init(expenseObj: ExpenseCD? = nil) { - self.expenseObj = expenseObj self.title = expenseObj?.title ?? "" if let expenseObj = expenseObj { @@ -47,6 +48,9 @@ class AddExpenseViewModel: ObservableObject { self.tagTitle = getTransTagTitle(transTag: expenseObj?.tag ?? TRANS_TAG_TRANSPORT) self.selectedType = expenseObj?.type ?? TRANS_TYPE_INCOME self.selectedTag = expenseObj?.tag ?? TRANS_TAG_TRANSPORT + if expenseObj?.frequency == .monthly { + monthlyFrequency = true + } if let data = expenseObj?.imageAttached { self.imageAttached = UIImage(data: data) } @@ -55,12 +59,13 @@ class AddExpenseViewModel: ObservableObject { self?.imageUpdated = true self?.imageAttached = image } + } func getButtText() -> String { - if selectedType == TRANS_TYPE_INCOME { return "\(expenseObj == nil ? "ADD" : "EDIT") INCOME" } - else if selectedType == TRANS_TYPE_EXPENSE { return "\(expenseObj == nil ? "ADD" : "EDIT") EXPENSE" } - else { return "\(expenseObj == nil ? "ADD" : "EDIT") TRANSACTION" } + if selectedType == TRANS_TYPE_INCOME { return "\(expenseObj != nil ? "EDIT" : "ADD") INCOME" } + else if selectedType == TRANS_TYPE_EXPENSE { return "\(expenseObj != nil ? "EDIT" : "ADD") EXPENSE" } + else { return "\(expenseObj != nil ? "EDIT" : "ADD") TRANSACTION" } } func attachImage() { AttachmentHandler.shared.showAttachmentActionSheet() } @@ -126,6 +131,11 @@ class AddExpenseViewModel: ObservableObject { expense.occuredOn = occuredOn expense.note = note expense.amount = amount + if monthlyFrequency { + expense.frequency = .monthly + } else { + expense.frequency = .onetime + } do { try managedObjectContext.save() closePresenter = true @@ -139,4 +149,33 @@ class AddExpenseViewModel: ObservableObject { try managedObjectContext.save(); closePresenter = true } catch { alertMsg = "\(error)"; showAlert = true } } + + func repeatTransaction(managedObjectContext: NSManagedObjectContext) { + do { + //TODO: Make sure that only the transactions from one month ago are re done. + let request = ExpenseCD.sortExpenseDataByFrequency(frequency: Frequency.monthly) + let monthlyExpenses = try managedObjectContext.fetch(request) + for expense in monthlyExpenses { + if let compareDate = Calendar.current.date(byAdding: .month, value: 1, to: expense.occuredOn ?? Date()) { + if Calendar.current.isDateInToday(compareDate) || compareDate < Date() { + selectedType = expense.type ?? TRANS_TYPE_INCOME + selectedTag = expense.tag ?? TRANS_TAG_OTHERS + title = expense.title ?? "" + occuredOn = compareDate + if let image = imageAttached { + expense.imageAttached = image.jpegData(compressionQuality: 1.0) + } + note = expense.note ?? "" + amount = String(expense.amount) + monthlyFrequency = true + + //change frequency of old object to onetime + expense.frequency = .onetime + self.saveTransaction(managedObjectContext: managedObjectContext) + } + } + } + try managedObjectContext.save(); closePresenter = true + } catch { alertMsg = "\(error)"; showAlert = true; print(error) } + } } diff --git a/Expenso/Screens/Expense/ExpenseView.swift b/Expenso/Screens/Expense/ExpenseView.swift index d233cc6..7c75d13 100644 --- a/Expenso/Screens/Expense/ExpenseView.swift +++ b/Expenso/Screens/Expense/ExpenseView.swift @@ -12,6 +12,8 @@ struct ExpenseView: View { @Environment(\.presentationMode) var presentationMode: Binding // CoreData @Environment(\.managedObjectContext) var managedObjectContext + + //MARK: Do you still need this Fetch Request I can't see any use case @FetchRequest(fetchRequest: ExpenseCD.getAllExpenseData(sortBy: ExpenseCDSort.occuredOn, ascending: false)) var expense: FetchedResults @State private var filter: ExpenseCDFilterTime = .all @@ -20,6 +22,8 @@ struct ExpenseView: View { @State private var showOptionsSheet = false @State private var displayAbout = false @State private var displaySettings = false + @State private var displayMonthlyTransaction = false + var body: some View { NavigationView { @@ -29,6 +33,7 @@ struct ExpenseView: View { VStack { NavigationLink(destination: NavigationLazyView(ExpenseSettingsView()), isActive: $displaySettings, label: {}) NavigationLink(destination: NavigationLazyView(AboutView()), isActive: $displayAbout, label: {}) + NavigationLink(destination: NavigationLazyView(MonthlyTransactionSettingsView()), isActive: $displayMonthlyTransaction, label: {}) ToolbarModelView(title: "Dashboard", hasBackButt: false, button1Icon: IMAGE_OPTION_ICON, button2Icon: IMAGE_FILTER_ICON) { self.presentationMode.wrappedValue.dismiss() } button1Method: { self.showOptionsSheet = true } button2Method: { self.showFilterSheet = true } @@ -45,6 +50,7 @@ struct ExpenseView: View { ActionSheet(title: Text("Select an option"), buttons: [ .default(Text("About")) { self.displayAbout = true }, .default(Text("Settings")) { self.displaySettings = true }, + .default(Text("Monthly Transactions")) { self.displayMonthlyTransaction = true }, .cancel() ]) } @@ -66,6 +72,9 @@ struct ExpenseView: View { .navigationViewStyle(StackNavigationViewStyle()) .navigationBarHidden(true) .navigationBarBackButtonHidden(true) + .onAppear() { + AddExpenseViewModel().repeatTransaction(managedObjectContext: managedObjectContext) + } } } @@ -132,7 +141,7 @@ struct ExpenseMainView: View { }.padding(4) ForEach(self.fetchRequest.wrappedValue) { expenseObj in - NavigationLink(destination: ExpenseDetailedView(expenseObj: expenseObj), label: { ExpenseTransView(expenseObj: expenseObj) }) + NavigationLink(destination: ExpenseDetailedView(expenseObj: expenseObj, editViewHasToggle: false), label: { ExpenseTransView(expenseObj: expenseObj) }) } } diff --git a/Expenso/Screens/ExpenseDetailed/ExpenseDetailedView.swift b/Expenso/Screens/ExpenseDetailed/ExpenseDetailedView.swift index 8131cfb..4e69cfa 100644 --- a/Expenso/Screens/ExpenseDetailed/ExpenseDetailedView.swift +++ b/Expenso/Screens/ExpenseDetailed/ExpenseDetailedView.swift @@ -18,8 +18,14 @@ struct ExpenseDetailedView: View { @State private var confirmDelete = false - init(expenseObj: ExpenseCD) { + var editViewHasToggle: Bool + + let isMonthly: Bool + + init(expenseObj: ExpenseCD, editViewHasToggle: Bool) { viewModel = ExpenseDetailedViewModel(expenseObj: expenseObj) + self.editViewHasToggle = editViewHasToggle + self.isMonthly = editViewHasToggle } var body: some View { @@ -41,6 +47,12 @@ struct ExpenseDetailedView: View { ExpenseDetailedListView(title: "Transaction type", description: viewModel.expenseObj.type == TRANS_TYPE_INCOME ? "Income" : "Expense" ) ExpenseDetailedListView(title: "Tag", description: getTransTagTitle(transTag: viewModel.expenseObj.tag ?? "")) ExpenseDetailedListView(title: "When", description: getDateFormatter(date: viewModel.expenseObj.occuredOn, format: "EEEE, dd MMM hh:mm a")) + if self.isMonthly { + Text("This transaction was last performed on this date and will be repeated one month in advance unless you change the status to not monthly") + .foregroundColor(.gray) + .fontWeight(.light) + .font(.subheadline) + } if let note = viewModel.expenseObj.note, note != "" { ExpenseDetailedListView(title: "Note", description: note) } @@ -74,7 +86,7 @@ struct ExpenseDetailedView: View { Spacer() HStack { Spacer() - NavigationLink(destination: AddExpenseView(viewModel: AddExpenseViewModel(expenseObj: viewModel.expenseObj)), label: { + NavigationLink(destination: AddExpenseView(viewModel: AddExpenseViewModel(expenseObj: viewModel.expenseObj), hasToggle: editViewHasToggle), label: { Image("pencil_icon").resizable().frame(width: 28.0, height: 28.0) Text("Edit").modifier(InterFont(.semiBold, size: 18)).foregroundColor(.white) }) diff --git a/Expenso/Screens/ExpenseFilter/ExpenseFilterView.swift b/Expenso/Screens/ExpenseFilter/ExpenseFilterView.swift index e51e267..34186b6 100644 --- a/Expenso/Screens/ExpenseFilter/ExpenseFilterView.swift +++ b/Expenso/Screens/ExpenseFilter/ExpenseFilterView.swift @@ -168,7 +168,7 @@ struct ExpenseFilterTransList: View { var body: some View { ForEach(self.fetchRequest.wrappedValue) { expenseObj in - NavigationLink(destination: ExpenseDetailedView(expenseObj: expenseObj), label: { ExpenseTransView(expenseObj: expenseObj) }) + NavigationLink(destination: ExpenseDetailedView(expenseObj: expenseObj, editViewHasToggle: false), label: { ExpenseTransView(expenseObj: expenseObj) }) } } } diff --git a/Expenso/Screens/MonthlyTransactionSettings/MonthlyTransactionSettingsView.swift b/Expenso/Screens/MonthlyTransactionSettings/MonthlyTransactionSettingsView.swift new file mode 100644 index 0000000..d3eff89 --- /dev/null +++ b/Expenso/Screens/MonthlyTransactionSettings/MonthlyTransactionSettingsView.swift @@ -0,0 +1,44 @@ +// +// MonthlyTransactionSettings.swift +// Expenso +// +// Created by Hendrik Steen on 13.06.22. +// + +import SwiftUI + +struct MonthlyTransactionSettingsView: View { + @Environment(\.presentationMode) var presentationMode: Binding + + @FetchRequest(fetchRequest: ExpenseCD.sortExpenseDataByFrequency(frequency: Frequency.monthly)) var monthlyExpenses: FetchedResults + var body: some View { + NavigationView { + ZStack { + Color.primary_color.edgesIgnoringSafeArea(.all) + + VStack { + ToolbarModelView(title: "Monthly Transaction") { self.presentationMode.wrappedValue.dismiss() } + ScrollView { + ForEach(monthlyExpenses) { expenseObj in + NavigationLink(destination: ExpenseDetailedView(expenseObj: expenseObj, editViewHasToggle: true), label: { ExpenseTransView(expenseObj: expenseObj) }) + } + } + .padding(.horizontal, 8) + }.edgesIgnoringSafeArea(.all) + } + .navigationBarHidden(true) + + } + .navigationViewStyle(StackNavigationViewStyle()) + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + + } + + } + +struct MonthlyTransactionSettingsView_Previews: PreviewProvider { + static var previews: some View { + MonthlyTransactionSettingsView() + } +}