BudgetView.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. //
  2. // BudgetView.swift
  3. // ydnab
  4. //
  5. // Created by Andrea Franceschini on 23/09/2020.
  6. //
  7. import SwiftUI
  8. import Combine
  9. import CoreData
  10. /// Shows the budget values for the selected budget, month, and year.
  11. struct BudgetView: View {
  12. @State var budget: BudgetInfo
  13. @Binding var currentBudgetId: UUID?
  14. @State private var month: Int = 0
  15. @State private var monthName: String
  16. @State private var year: Int = Calendar(identifier: .gregorian).component(.year, from: Date())
  17. @State private var showMonthPicker: Bool = false
  18. @State private var showNotImplemented: Bool = false
  19. @State private var showQuickBudget: Bool = false
  20. @State private var showEditCategoryEntry: Bool = false
  21. @State private var editingCategoryEntry: BudgetCategoryEntry? = nil
  22. @AppStorage("lastLoadedBudgetId") private var lastLoadedBudgetId = ""
  23. @Environment(\.managedObjectContext) var managedObjectContext
  24. init(budget: BudgetInfo, currentBudgetId: Binding<UUID?>) {
  25. _budget = .init(initialValue: budget)
  26. _currentBudgetId = currentBudgetId
  27. var cal = Calendar(identifier: .gregorian)
  28. cal.locale = Locale(identifier: self._budget.wrappedValue.localeIdentifier)
  29. let now = Date()
  30. _month = .init(initialValue: cal.component(.month, from: now))
  31. _monthName = .init(initialValue: cal.monthSymbols[self._month.wrappedValue])
  32. _year = .init(initialValue: cal.component(.year, from: now))
  33. }
  34. func formatAmount(_ amount: NSNumber) -> String? {
  35. let f = NumberFormatter()
  36. f.locale = Locale(identifier: budget.localeIdentifier)
  37. f.numberStyle = .currency
  38. return f.string(from: amount)
  39. }
  40. // private func findBudgetCategoryEntry(withId: UUID) -> BudgetCategoryEntry? {
  41. // return budgetEntries.first(where: { $0.budgetCategoryId == withId })
  42. // }
  43. private func zeroBudgetEntries() -> Void {
  44. // for section in budget.sections {
  45. // for category in section.categories {
  46. // if let entry = findBudgetCategoryEntry(withId: category.id) {
  47. // entry.amount = 0
  48. // } else {
  49. // let newEntry = BudgetCategoryEntry(context: managedObjectContext)
  50. // newEntry.budgetId = budget.id
  51. // newEntry.budgetCategoryId = category.id
  52. // newEntry.month = Int64(month)
  53. // newEntry.year = Int64(year)
  54. // newEntry.amount = 0
  55. // }
  56. //
  57. // do {
  58. // try managedObjectContext.save()
  59. // } catch {
  60. // print(error)
  61. // }
  62. // }
  63. // }
  64. }
  65. @State private var refreshView: Bool = false
  66. var body: some View {
  67. DynamicFetchView(predicate: NSPredicate(format: "budgetId == %@ AND month == %ld AND year == %ld", budget.id as CVarArg, month, year),
  68. sortDescriptors: []) { (budgetEntries: FetchedResults<BudgetCategoryEntry>) in
  69. VStack {
  70. HStack {
  71. Button(action: { withAnimation() { showMonthPicker.toggle() } } ) {
  72. Text("\(monthName) \(String(year))")
  73. Image(systemName: showMonthPicker ? "chevron.up" : "chevron.down")
  74. }
  75. Spacer()
  76. Button(action: { showQuickBudget.toggle() }) { // TODO: Implement this
  77. Image(systemName: "bolt.fill")
  78. }
  79. Button(action: { showNotImplemented.toggle() }) { // TODO: Implement this
  80. Image(systemName: "gearshape.fill")
  81. }.padding(.leading, 11)
  82. }
  83. .padding(11)
  84. if showMonthPicker {
  85. YearAndMonthPicker(month: $month,
  86. monthName: $monthName,
  87. year: $year,
  88. locale: Locale(identifier: budget.localeIdentifier)
  89. )
  90. }
  91. BudgetViewSummary(budget: $budget, month: $month, monthName: $monthName, year: $year)
  92. .padding([.leading, .trailing], 16)
  93. List {
  94. ForEach(budget.sections) { section in
  95. if !section.hidden {
  96. Section(header: BudgetSectionCell(section: section,
  97. amountText: "", //formatAmount(0) ?? "--",
  98. color: 0 < 0 ? Color("negativeAmount") : Color("positiveAmount"))
  99. ) {
  100. ForEach(section.categories) { category in
  101. if !category.hidden {
  102. BudgetCategoryCell(category: category,
  103. amountText: String(format: "= %@ =", budgetEntries.first(where: { $0.budgetCategoryId == category.id })?.amount ?? -69 ),
  104. color: 0 < 0 ? Color("negativeAmount") : Color("positiveAmount")
  105. )
  106. }
  107. }
  108. }
  109. }
  110. }
  111. }
  112. .listStyle(PlainListStyle())
  113. // .onChange(of: month) { m in
  114. // let entries = budgetEntries
  115. // print(entries.map { $0.amount })
  116. // }
  117. .navigationBarTitle(budget.name, displayMode: .inline)
  118. .toolbar {
  119. EditButton()
  120. }
  121. .onAppear() {
  122. currentBudgetId = budget.id
  123. lastLoadedBudgetId = budget.id.uuidString
  124. }
  125. .actionSheet(isPresented: $showQuickBudget) {
  126. ActionSheet(
  127. title: Text("Quick budget"),
  128. message: Text("How would you like to set up your budget for \(monthName) \(String(year))?"),
  129. buttons: [
  130. .cancel { print(self.showQuickBudget) },
  131. .default(Text("All categories to zero"), action: zeroBudgetEntries),
  132. .default(Text("Values used the previous month"), action: { print(self) })
  133. ]
  134. )
  135. }
  136. .popover(isPresented: $showEditCategoryEntry) {
  137. // BudgetCategoryEntryEditor(amount: $editingCategoryEntry.wrappedValue?.amount)
  138. }
  139. .alert(isPresented: $showNotImplemented) {
  140. Alert(title: Text("Not Implemented!"))
  141. }
  142. }
  143. }
  144. }
  145. }
  146. struct BudgetCategoryEntryEditor: View {
  147. @Binding var amount: Decimal?
  148. @Binding var locale: Locale
  149. @State private var amountText = ""
  150. var body: some View {
  151. VStack {
  152. TextField("Amount", text: $amountText)
  153. .keyboardType(.numbersAndPunctuation)
  154. .onReceive(Just(amountText)) { newValue in
  155. let filtered = newValue.filter { "0123456789.,".contains($0) }
  156. if filtered != newValue {
  157. amount = Decimal(string: filtered, locale: locale)
  158. }
  159. }
  160. }
  161. .onAppear {
  162. amountText = "\(amount ?? -69)"
  163. }
  164. }
  165. }
  166. struct BudgetView_Previews: PreviewProvider {
  167. static let budget: [BudgetSection] = [
  168. BudgetSection(name: "Everyday Expenses", categories: [
  169. BudgetCategory(name: "Groceries"),
  170. BudgetCategory(name: "Eating out"),
  171. BudgetCategory(name: "Medical"),
  172. BudgetCategory(name: "Clothing"),
  173. BudgetCategory(name: "Household goods")
  174. ]),
  175. BudgetSection(name: "Travel", categories: [
  176. BudgetCategory(name: "Transport"),
  177. BudgetCategory(name: "Fuel"),
  178. BudgetCategory(name: "Accommodation")
  179. ]),
  180. BudgetSection(name: "Rainy Days", categories: [
  181. BudgetCategory(name: "Emergencies"),
  182. BudgetCategory(name: "Car insurance")
  183. ]),
  184. BudgetSection(name: "Monthly Expenses", categories: [
  185. BudgetCategory(name: "Rent"),
  186. BudgetCategory(name: "Mobile")
  187. ]),
  188. BudgetSection(name: "Savings Goals", categories: [
  189. BudgetCategory(name: "Savings"),
  190. BudgetCategory(name: "Holidays")
  191. ])
  192. ]
  193. @State static var currentBudgetId: UUID?
  194. static var previews: some View {
  195. NavigationView {
  196. BudgetView(budget: BudgetInfo(name: "Default Budget", sections: budget),
  197. currentBudgetId: $currentBudgetId)
  198. }
  199. .preferredColorScheme(.light)
  200. }
  201. }