Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monthly recurring expenses and income #30

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
Binary file modified .DS_Store
Binary file not shown.
26 changes: 19 additions & 7 deletions Expenso.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -53,14 +54,15 @@

/* 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 = "<group>"; };
24D9168C285735920025227B /* MonthlyTransactionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthlyTransactionSettingsView.swift; sourceTree = "<group>"; };
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 = "<group>"; };
736C720925CFD89900720DEA /* empty-face.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "empty-face.json"; sourceTree = "<group>"; };
736C721A25CFE8E200720DEA /* LottieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieView.swift; sourceTree = "<group>"; };
738B1C1425C65DFE0067407B /* Expenso.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Expenso.app; sourceTree = BUILT_PRODUCTS_DIR; };
738B1C1725C65DFE0067407B /* ExpensoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpensoApp.swift; sourceTree = "<group>"; };
738B1C1B25C65E060067407B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
738B1C1E25C65E060067407B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
738B1C2325C65E060067407B /* Expenso.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Expenso.xcdatamodel; sourceTree = "<group>"; };
738B1C2525C65E060067407B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
738B1C2E25C65EF70067407B /* Configs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configs.swift; sourceTree = "<group>"; };
738B1C3225C660140067407B /* ExpenseCD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpenseCD.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -112,9 +114,18 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
24D9168B285735730025227B /* MonthlyTransactionSettings */ = {
isa = PBXGroup;
children = (
24D9168C285735920025227B /* MonthlyTransactionSettingsView.swift */,
);
path = MonthlyTransactionSettings;
sourceTree = "<group>";
};
733763A827120DB600AA983A /* Screens */ = {
isa = PBXGroup;
children = (
24D9168B285735730025227B /* MonthlyTransactionSettings */,
739DFF7925DC1E23005BD5C8 /* Authenticate */,
738B1C8E25C680C50067407B /* About */,
738B1C8925C67D790067407B /* ExpenseFilter */,
Expand Down Expand Up @@ -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 */,
);
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down Expand Up @@ -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 = "<group>";
versionGroupType = wrapper.xcdatamodel;
Expand Down
5 changes: 3 additions & 2 deletions Expenso/Expenso.xcdatamodeld/Expenso.xcdatamodel/contents
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20B28" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21D62" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="ExpenseCD" representedClassName=".ExpenseCD" syncable="YES">
<attribute name="amount" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="frequencyValue" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="imageAttached" optional="YES" attributeType="Binary"/>
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="occuredOn" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
Expand All @@ -12,6 +13,6 @@
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<elements>
<element name="ExpenseCD" positionX="-63" positionY="-9" width="128" height="164"/>
<element name="ExpenseCD" positionX="-63" positionY="-9" width="128" height="179"/>
</elements>
</model>
22 changes: 22 additions & 0 deletions Expenso/Library/CoreData/ExpenseCD.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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 {
Expand All @@ -50,4 +63,13 @@ extension ExpenseCD {
request.sortDescriptors = [sortDescriptor]
return request
}

static func sortExpenseDataByFrequency(sortBy: ExpenseCDSort = .occuredOn, frequency: Frequency, ascending: Bool = true) -> NSFetchRequest<ExpenseCD> {
let request: NSFetchRequest<ExpenseCD> = ExpenseCD.fetchRequest() as! NSFetchRequest<ExpenseCD>
let sortDescriptor = NSSortDescriptor(key: sortBy.rawValue, ascending: ascending)
let predicate = NSPredicate(format: "frequencyValue == %i", frequency.rawValue)
request.predicate = predicate
request.sortDescriptors = [sortDescriptor]
return request
}
}
14 changes: 13 additions & 1 deletion Expenso/Screens/AddExpense/AddExpenseView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -134,6 +146,7 @@ struct AddExpenseView: View {
])
}


if let image = viewModel.imageAttached {
Button(action: { showAttachSheet = true }, label: {
Image(uiImage: image)
Expand All @@ -146,7 +159,6 @@ struct AddExpenseView: View {
}

Spacer().frame(height: 150)
Spacer()
}
.frame(maxWidth: .infinity).padding(.horizontal, 8)
.alert(isPresented: $viewModel.showAlert,
Expand Down
47 changes: 43 additions & 4 deletions Expenso/Screens/AddExpense/AddExpenseViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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() }
Expand Down Expand Up @@ -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
Expand All @@ -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) }
}
}
11 changes: 10 additions & 1 deletion Expenso/Screens/Expense/ExpenseView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ struct ExpenseView: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
// 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<ExpenseCD>

@State private var filter: ExpenseCDFilterTime = .all
Expand 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 {
Expand All @@ -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 }
Expand All @@ -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()
])
}
Expand All @@ -66,6 +72,9 @@ struct ExpenseView: View {
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.onAppear() {
AddExpenseViewModel().repeatTransaction(managedObjectContext: managedObjectContext)
}
}
}

Expand Down Expand Up @@ -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) })
}
}

Expand Down
Loading