|
@@ -6,113 +6,88 @@
|
|
|
//
|
|
|
|
|
|
import SwiftUI
|
|
|
+import Combine
|
|
|
|
|
|
-struct BudgetCategoryCell: View {
|
|
|
- @State var category: BudgetCategory
|
|
|
- @State var amountText: String
|
|
|
- @State var color: Color
|
|
|
+/// Shows the budget values for the selected budget, month, and year.
|
|
|
+struct BudgetView: View {
|
|
|
+ @State var budget: BudgetInfo
|
|
|
+ @Binding var currentBudgetId: UUID?
|
|
|
+ @State private var month: Int = 0
|
|
|
+ @State private var monthName: String
|
|
|
+ @State private var year: Int = Calendar(identifier: .gregorian).component(.year, from: Date())
|
|
|
+ @State private var showMonthPicker: Bool = false
|
|
|
+ @State private var showNotImplemented: Bool = false
|
|
|
+ @State private var showQuickBudget: Bool = false
|
|
|
+ @State private var showEditCategoryEntry: Bool = false
|
|
|
+ @State private var editingCategoryEntry: BudgetCategoryEntry? = nil
|
|
|
|
|
|
- var body: some View {
|
|
|
- HStack {
|
|
|
- Text(category.name)
|
|
|
- Spacer()
|
|
|
- Text(amountText)
|
|
|
- .bold()
|
|
|
- .foregroundColor(color)
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+ @AppStorage("lastLoadedBudgetId") private var lastLoadedBudgetId = ""
|
|
|
|
|
|
-struct BudgetSectionCell: View {
|
|
|
- @State var section: BudgetSection
|
|
|
- @State var amountText: String
|
|
|
- @State var color: Color
|
|
|
+ @Environment(\.managedObjectContext) var managedObjectContext
|
|
|
|
|
|
- @Environment(\.editMode) var editMode
|
|
|
+// private var budgetEntriesRequest: FetchRequest<BudgetCategoryEntry>
|
|
|
+// private var budgetEntries: FetchedResults<BudgetCategoryEntry> { budgetEntriesRequest.wrappedValue }
|
|
|
|
|
|
- var body: some View {
|
|
|
- HStack {
|
|
|
- Text(section.name)
|
|
|
- .textCase(.none)
|
|
|
- Spacer()
|
|
|
- if editMode?.wrappedValue == .inactive {
|
|
|
- Text(amountText)
|
|
|
- .foregroundColor(color)
|
|
|
- }
|
|
|
- }
|
|
|
- .padding(.vertical, 11)
|
|
|
- }
|
|
|
-}
|
|
|
+ @FetchRequest<BudgetCategoryEntry> private var budgetEntries: FetchedResults<BudgetCategoryEntry>
|
|
|
|
|
|
-struct YearAndMonthPicker: View {
|
|
|
- @Binding var month: Int
|
|
|
- @Binding var monthName: String
|
|
|
- @Binding var year: Int
|
|
|
- @State var locale: Locale
|
|
|
+ init(budget: BudgetInfo, currentBudgetId: Binding<UUID?>) {
|
|
|
+ _budget = .init(initialValue: budget)
|
|
|
+ _currentBudgetId = currentBudgetId
|
|
|
+ var cal = Calendar(identifier: .gregorian)
|
|
|
+ cal.locale = Locale(identifier: self._budget.wrappedValue.localeIdentifier)
|
|
|
+ let now = Date()
|
|
|
+ _month = .init(initialValue: cal.component(.month, from: now))
|
|
|
+ _monthName = .init(initialValue: cal.monthSymbols[self._month.wrappedValue])
|
|
|
+ _year = .init(initialValue: cal.component(.year, from: now))
|
|
|
|
|
|
- private var monthNames: [String] {
|
|
|
- var c = Calendar(identifier: .gregorian)
|
|
|
- c.locale = locale
|
|
|
- return c.monthSymbols
|
|
|
+ _budgetEntries = FetchRequest<BudgetCategoryEntry>(entity: BudgetCategoryEntry.entity(),
|
|
|
+ sortDescriptors: [],
|
|
|
+ predicate: NSPredicate(format: "(budgetId == %@) AND (month == %d) AND (year == %d)",
|
|
|
+ budget.id as CVarArg, _month.wrappedValue, _year.wrappedValue))
|
|
|
+
|
|
|
+// budgetEntriesRequest = FetchRequest<BudgetCategoryEntry>(entity: BudgetCategoryEntry.entity(),
|
|
|
+// sortDescriptors: [],
|
|
|
+// predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [
|
|
|
+// NSPredicate(format: "budgetId == %@", budget.id as CVarArg),
|
|
|
+// NSPredicate(format: "month == %i", month),
|
|
|
+// NSPredicate(format: "year == %i", year)
|
|
|
+// ])
|
|
|
+// )
|
|
|
}
|
|
|
|
|
|
- var columns: [GridItem] = [
|
|
|
- GridItem(spacing: 8),
|
|
|
- GridItem(spacing: 8),
|
|
|
- GridItem(spacing: 8)
|
|
|
- ]
|
|
|
+ func formatAmount(_ amount: NSNumber) -> String? {
|
|
|
+ let f = NumberFormatter()
|
|
|
+ f.locale = Locale(identifier: budget.localeIdentifier)
|
|
|
+ f.numberStyle = .currency
|
|
|
+ return f.string(from: amount)
|
|
|
+ }
|
|
|
|
|
|
- var body: some View {
|
|
|
- VStack {
|
|
|
- HStack {
|
|
|
- Button(action: { year -= 1 }) {
|
|
|
- Image(systemName: "chevron.backward.square")
|
|
|
- .imageScale(.large)
|
|
|
- }
|
|
|
- Spacer()
|
|
|
- Text(String(year)).bold()
|
|
|
- Spacer()
|
|
|
- Button(action: { year += 1 }) {
|
|
|
- Image(systemName: "chevron.forward.square")
|
|
|
- .imageScale(.large)
|
|
|
+ private func findBudgetCategoryEntry(withId: UUID) -> BudgetCategoryEntry? {
|
|
|
+ return budgetEntries.first(where: { $0.budgetCategoryId == withId })
|
|
|
+ }
|
|
|
+
|
|
|
+ private func zeroBudgetEntries() -> Void {
|
|
|
+ for section in budget.sections {
|
|
|
+ for category in section.categories {
|
|
|
+ if let entry = findBudgetCategoryEntry(withId: category.id) {
|
|
|
+ entry.amount = 0
|
|
|
+ } else {
|
|
|
+ let newEntry = BudgetCategoryEntry(context: managedObjectContext)
|
|
|
+ newEntry.budgetId = budget.id
|
|
|
+ newEntry.budgetCategoryId = category.id
|
|
|
+ newEntry.month = Int64(month)
|
|
|
+ newEntry.year = Int64(year)
|
|
|
+ newEntry.amount = 0
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- GeometryReader { geometry in
|
|
|
- LazyVGrid(columns: columns, spacing: 4) {
|
|
|
- ForEach(monthNames.indices) { i in
|
|
|
- Button(action: {
|
|
|
- month = i
|
|
|
- monthName = monthNames[i]
|
|
|
- }, label: {
|
|
|
- Text(monthNames[i])
|
|
|
- .frame(minWidth: geometry.size.width / 3,
|
|
|
- idealWidth: geometry.size.width / 3,
|
|
|
- maxWidth: geometry.size.width / 3,
|
|
|
- minHeight: geometry.size.height / 4,
|
|
|
- idealHeight: geometry.size.height / 4,
|
|
|
- maxHeight: geometry.size.height / 4,
|
|
|
- alignment: .center)
|
|
|
- })
|
|
|
- .background(month == i ? Color("bgActive") : Color("bgInactive"))
|
|
|
- .foregroundColor(month == i ? Color.white : Color("AccentColor"))
|
|
|
- }
|
|
|
+ do {
|
|
|
+ try managedObjectContext.save()
|
|
|
+ } catch {
|
|
|
+ print(error)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- .padding(.bottom, 22)
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
-struct BudgetViewSummary: View {
|
|
|
- @Binding var budget: BudgetInfo
|
|
|
- @Binding var month: Int
|
|
|
- @Binding var monthName: String
|
|
|
- @Binding var year: Int
|
|
|
-
|
|
|
- @State private var showMonthPicker: Bool = false
|
|
|
-
|
|
|
- @State private var showNotImplemented: Bool = false
|
|
|
|
|
|
var body: some View {
|
|
|
VStack {
|
|
@@ -122,62 +97,23 @@ struct BudgetViewSummary: View {
|
|
|
Image(systemName: showMonthPicker ? "chevron.up" : "chevron.down")
|
|
|
}
|
|
|
Spacer()
|
|
|
- Button(action: { showNotImplemented.toggle() }) { // TODO: Implement this
|
|
|
+ Button(action: { showQuickBudget.toggle() }) { // TODO: Implement this
|
|
|
Image(systemName: "bolt.fill")
|
|
|
}
|
|
|
Button(action: { showNotImplemented.toggle() }) { // TODO: Implement this
|
|
|
Image(systemName: "gearshape.fill")
|
|
|
}.padding(.leading, 11)
|
|
|
}
|
|
|
- .padding(.vertical, 11)
|
|
|
+ .padding(11)
|
|
|
|
|
|
if showMonthPicker {
|
|
|
- YearAndMonthPicker(month: $month, monthName: $monthName, year: $year, locale: Locale(identifier: budget.localeIdentifier))
|
|
|
- }
|
|
|
-
|
|
|
- HStack {
|
|
|
- Text("To budget") // Or "overbudgeted"
|
|
|
- Spacer()
|
|
|
- Text("$ 0.00")
|
|
|
- }
|
|
|
-
|
|
|
- .alert(isPresented: $showNotImplemented) {
|
|
|
- Alert(title: Text("Not Implemented!"))
|
|
|
+ YearAndMonthPicker(month: $month,
|
|
|
+ monthName: $monthName,
|
|
|
+ year: $year,
|
|
|
+ locale: Locale(identifier: budget.localeIdentifier)
|
|
|
+ )
|
|
|
}
|
|
|
- }
|
|
|
- .padding(.vertical, 11)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-struct BudgetView: View {
|
|
|
- @State var budget: BudgetInfo
|
|
|
- @Binding var currentBudgetId: UUID?
|
|
|
- @State private var month: Int = 0
|
|
|
- @State private var monthName: String
|
|
|
- @State private var year: Int = Calendar(identifier: .gregorian).component(.year, from: Date())
|
|
|
-
|
|
|
- @AppStorage("lastLoadedBudgetId") private var lastLoadedBudgetId = ""
|
|
|
-
|
|
|
- init(budget: BudgetInfo, currentBudgetId: Binding<UUID?>) {
|
|
|
- _budget = .init(initialValue: budget)
|
|
|
- _currentBudgetId = currentBudgetId
|
|
|
- var cal = Calendar(identifier: .gregorian)
|
|
|
- cal.locale = Locale(identifier: self._budget.wrappedValue.localeIdentifier)
|
|
|
- let now = Date()
|
|
|
- _month = .init(initialValue: cal.component(.month, from: now))
|
|
|
- _monthName = .init(initialValue: cal.monthSymbols[self._month.wrappedValue])
|
|
|
- _year = .init(initialValue: cal.component(.year, from: now))
|
|
|
- }
|
|
|
-
|
|
|
- func formatAmount(_ amount: NSNumber) -> String? {
|
|
|
- let f = NumberFormatter()
|
|
|
- f.locale = Locale(identifier: budget.localeIdentifier)
|
|
|
- f.numberStyle = .currency
|
|
|
- return f.string(from: amount)
|
|
|
- }
|
|
|
-
|
|
|
- var body: some View {
|
|
|
- VStack {
|
|
|
+
|
|
|
BudgetViewSummary(budget: $budget, month: $month, monthName: $monthName, year: $year)
|
|
|
.padding([.leading, .trailing], 16)
|
|
|
|
|
@@ -185,15 +121,19 @@ struct BudgetView: View {
|
|
|
ForEach(budget.sections) { section in
|
|
|
if !section.hidden {
|
|
|
Section(header: BudgetSectionCell(section: section,
|
|
|
- amountText: formatAmount(0) ?? "--",
|
|
|
+ amountText: "", //formatAmount(0) ?? "--",
|
|
|
color: 0 < 0 ? Color("negativeAmount") : Color("positiveAmount"))
|
|
|
) {
|
|
|
ForEach(section.categories) { category in
|
|
|
if !category.hidden {
|
|
|
BudgetCategoryCell(category: category,
|
|
|
- amountText: formatAmount(0) ?? "--",
|
|
|
+ amountText: formatAmount(budgetEntries.first(where: { $0.budgetCategoryId == category.id })?.amount ?? 0) ?? "--",
|
|
|
color: 0 < 0 ? Color("negativeAmount") : Color("positiveAmount")
|
|
|
)
|
|
|
+ .onLongPressGesture {
|
|
|
+ editingCategoryEntry = findBudgetCategoryEntry(withId: category.id)
|
|
|
+ showEditCategoryEntry.toggle()
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
.onMove { (indexSet, s) in
|
|
@@ -212,10 +152,55 @@ struct BudgetView: View {
|
|
|
EditButton()
|
|
|
}
|
|
|
.onAppear() {
|
|
|
- print("BudgetView Appears")
|
|
|
currentBudgetId = budget.id
|
|
|
lastLoadedBudgetId = budget.id.uuidString
|
|
|
}
|
|
|
+ .onChange(of: month) { e in
|
|
|
+
|
|
|
+ }
|
|
|
+ .onReceive(budgetEntries.publisher) { o in
|
|
|
+ print(o.month)
|
|
|
+ }
|
|
|
+ .actionSheet(isPresented: $showQuickBudget) {
|
|
|
+ ActionSheet(
|
|
|
+ title: Text("Quick budget"),
|
|
|
+ message: Text("How would you like to set up your budget for \(monthName) \(String(year))?"),
|
|
|
+ buttons: [
|
|
|
+ .cancel { print(self.showQuickBudget) },
|
|
|
+ .default(Text("All categories to zero"), action: zeroBudgetEntries),
|
|
|
+ .default(Text("Values used the previous month"), action: { print(self) })
|
|
|
+ ]
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .popover(isPresented: $showEditCategoryEntry) {
|
|
|
+// BudgetCategoryEntryEditor(amount: $editingCategoryEntry.wrappedValue?.amount)
|
|
|
+ }
|
|
|
+
|
|
|
+ .alert(isPresented: $showNotImplemented) {
|
|
|
+ Alert(title: Text("Not Implemented!"))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+struct BudgetCategoryEntryEditor: View {
|
|
|
+ @Binding var amount: Decimal?
|
|
|
+ @Binding var locale: Locale
|
|
|
+ @State private var amountText = ""
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ VStack {
|
|
|
+ TextField("Amount", text: $amountText)
|
|
|
+ .keyboardType(.numbersAndPunctuation)
|
|
|
+ .onReceive(Just(amountText)) { newValue in
|
|
|
+ let filtered = newValue.filter { "0123456789.,".contains($0) }
|
|
|
+ if filtered != newValue {
|
|
|
+ amount = Decimal(string: filtered, locale: locale)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .onAppear {
|
|
|
+ amountText = "\(amount ?? -69)"
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -255,8 +240,6 @@ struct BudgetView_Previews: PreviewProvider {
|
|
|
currentBudgetId: $currentBudgetId)
|
|
|
}
|
|
|
.preferredColorScheme(.light)
|
|
|
-
|
|
|
- //.environment(\.layoutDirection, .rightToLeft)
|
|
|
}
|
|
|
}
|
|
|
|