Use Swift Charts for budget overview actual vs expected
This commit is contained in:
parent
eb50266745
commit
6af0fda86f
7 changed files with 100 additions and 96 deletions
|
@ -492,7 +492,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1320;
|
||||
LastUpgradeCheck = 1340;
|
||||
LastUpgradeCheck = 1420;
|
||||
ORGANIZATIONNAME = "William Brawner";
|
||||
TargetAttributes = {
|
||||
28AC94E9233C373900BFB70A = {
|
||||
|
@ -757,7 +757,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
|
@ -814,7 +814,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "";
|
||||
|
@ -834,22 +834,27 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Twigs/Twigs.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Twigs/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = Twigs/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 15.0;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Twigs;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 16.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.wbrawner.projects.budget.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
};
|
||||
|
@ -864,21 +869,26 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Twigs/Twigs.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 19;
|
||||
CURRENT_PROJECT_VERSION = 22;
|
||||
DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Twigs/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = Twigs/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 15.0;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Twigs;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 16.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.wbrawner.projects.budget.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
};
|
||||
|
@ -892,7 +902,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
|
||||
INFOPLIST_FILE = TwigsTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -902,7 +912,7 @@
|
|||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Twigs.app/Twigs";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Twigs.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Twigs";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -914,7 +924,7 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
|
||||
INFOPLIST_FILE = TwigsTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -924,7 +934,7 @@
|
|||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Twigs.app/Twigs";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Twigs.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Twigs";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
@ -942,9 +952,10 @@
|
|||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.wbrawner.BudgetUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = Budget;
|
||||
TEST_TARGET_NAME = Twigs;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -962,9 +973,10 @@
|
|||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.wbrawner.BudgetUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = Budget;
|
||||
TEST_TARGET_NAME = Twigs;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
@ -974,7 +986,8 @@
|
|||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = VJ33S6H7W7;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.1;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -989,7 +1002,8 @@
|
|||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = VJ33S6H7W7;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 9Z6DE6KNJ9;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.1;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1340"
|
||||
LastUpgradeVersion = "1420"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1340"
|
||||
LastUpgradeVersion = "1420"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
// Copyright © 2019 William Brawner. All rights reserved.
|
||||
//
|
||||
|
||||
import Charts
|
||||
import SwiftUI
|
||||
import TwigsCore
|
||||
|
||||
|
@ -21,14 +22,50 @@ struct BudgetDetailsView: View {
|
|||
errorTextLocalizedStringKey: "budgets_load_failure"
|
||||
) { overview in
|
||||
List {
|
||||
Section(overview.budget.name) {
|
||||
DescriptionOverview(overview: overview)
|
||||
if let description = overview.budget.description {
|
||||
Section(overview.budget.name) {
|
||||
Text(description)
|
||||
}
|
||||
}
|
||||
Section("income") {
|
||||
IncomeOverview(overview: overview)
|
||||
|
||||
Section("stats") {
|
||||
HStack {
|
||||
VStack(alignment: .center) {
|
||||
Text("cash_flow")
|
||||
.font(.caption)
|
||||
Text(verbatim: overview.balance.toCurrencyString())
|
||||
.foregroundColor(overview.balance < 0 ? .red : .green)
|
||||
.font(.title2)
|
||||
}
|
||||
Spacer()
|
||||
VStack(alignment: .center) {
|
||||
Text("transactions")
|
||||
.font(.caption)
|
||||
Text(verbatim: String(overview.transactionCount))
|
||||
.font(.title2)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Section("expenses") {
|
||||
ExpensesOverview(overview: overview)
|
||||
Section("expected_vs_actual") {
|
||||
Chart {
|
||||
BarMark(
|
||||
x: .value("Amount", overview.expectedIncome),
|
||||
y: .value("Label", Bundle.main.localizedString(forKey: "expected_income", value: nil, table: nil))
|
||||
).foregroundStyle(Color.gray)
|
||||
BarMark(
|
||||
x: .value("Amount", overview.actualIncome),
|
||||
y: .value("Label", Bundle.main.localizedString(forKey: "actual_income", value: nil, table: nil))
|
||||
).foregroundStyle(Color.green)
|
||||
BarMark(
|
||||
x: .value("Amount", overview.expectedExpenses),
|
||||
y: .value("Label", Bundle.main.localizedString(forKey: "expected_expenses", value: nil, table: nil))
|
||||
).foregroundStyle(Color.gray)
|
||||
BarMark(
|
||||
x: .value("Amount", overview.actualExpenses),
|
||||
y: .value("Label", Bundle.main.localizedString(forKey: "actual_expenses", value: nil, table: nil))
|
||||
).foregroundStyle(Color.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
|
@ -61,63 +98,6 @@ struct BudgetDetailsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct DescriptionOverview: View {
|
||||
let overview: BudgetOverview
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if let description = overview.budget.description {
|
||||
Text(description)
|
||||
}
|
||||
HStack {
|
||||
Text("current_balance")
|
||||
Text(verbatim: overview.balance.toCurrencyString())
|
||||
.foregroundColor(overview.balance < 0 ? .red : .green)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IncomeOverview: View {
|
||||
let overview: BudgetOverview
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("expected")
|
||||
Text(verbatim: overview.expectedIncome.toCurrencyString())
|
||||
}
|
||||
ProgressView(value: Float(overview.expectedIncome), maxValue: overview.maxValue, progressTintColor: .gray, progressBarHeight: 10.0, progressBarCornerRadius: 4.0)
|
||||
HStack {
|
||||
Text("actual")
|
||||
Text(verbatim: overview.actualIncome.toCurrencyString())
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
ProgressView(value: Float(overview.actualIncome), maxValue: overview.maxValue, progressTintColor: .green, progressBarHeight: 10.0, progressBarCornerRadius: 4.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ExpensesOverview: View {
|
||||
let overview: BudgetOverview
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("expected")
|
||||
Text(verbatim: overview.expectedExpenses.toCurrencyString())
|
||||
}
|
||||
ProgressView(value: Float(overview.expectedExpenses), maxValue: overview.maxValue, progressTintColor: .gray, progressBarHeight: 10.0, progressBarCornerRadius: 4.0)
|
||||
HStack {
|
||||
Text("actual")
|
||||
Text(verbatim: overview.actualExpenses.toCurrencyString())
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
ProgressView(value: Float(overview.actualExpenses), maxValue: overview.maxValue, progressTintColor: .red, progressBarHeight: 10.0, progressBarCornerRadius: 4.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
struct BudgetDetailsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
|
|
|
@ -178,7 +178,12 @@ class DataStore : ObservableObject {
|
|||
do {
|
||||
let budgetBalance = try await self.apiService.sumTransactions(budgetId: budget.id, categoryId: nil, from: nil, to: nil)
|
||||
let categories = try await self.apiService.getCategories(budgetId: budget.id, expense: nil, archived: false, count: nil, page: nil)
|
||||
var budgetOverview = BudgetOverview(budget: budget, balance: budgetBalance.balance)
|
||||
await self.getTransactions(showLoader: false)
|
||||
var transactionCount = 0
|
||||
if case let .success(transactions) = self.transactions {
|
||||
transactionCount = transactions.count
|
||||
}
|
||||
var budgetOverview = BudgetOverview(budget: budget, balance: budgetBalance.balance, categories: categories, transactionCount: transactionCount)
|
||||
try await withThrowingTaskGroup(of: (TwigsCore.Category, BalanceResponse).self) { group in
|
||||
for category in categories {
|
||||
group.addTask {
|
||||
|
@ -188,15 +193,15 @@ class DataStore : ObservableObject {
|
|||
|
||||
for try await (category, response) in group {
|
||||
if category.expense {
|
||||
budgetOverview.expectedExpenses += category.amount
|
||||
budgetOverview.expectedExpenses += Float(category.amount) / 100.0
|
||||
} else {
|
||||
budgetOverview.expectedIncome += category.amount
|
||||
budgetOverview.expectedIncome += Float(category.amount) / 100.0
|
||||
}
|
||||
|
||||
if category.expense {
|
||||
budgetOverview.actualExpenses += abs(response.balance)
|
||||
budgetOverview.actualExpenses += Float(abs(response.balance)) / 100.0
|
||||
} else {
|
||||
budgetOverview.actualIncome += response.balance
|
||||
budgetOverview.actualIncome += Float(response.balance) / 100.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,10 +66,13 @@
|
|||
"budgets" = "Budgets";
|
||||
"new_budget" = "New Budget";
|
||||
"current_balance" = "Current Balance:";
|
||||
"expected" = "Expected:";
|
||||
"actual" = "Actual:";
|
||||
"income" = "Income:";
|
||||
"expenses" = "Expenses:";
|
||||
"stats" = "Stats";
|
||||
"cash_flow" = "Cash Flow";
|
||||
"expected_vs_actual" = "Expected vs Actual";
|
||||
"expected_income" = "Expected Income";
|
||||
"expected_expenses" = "Expected Expenses";
|
||||
"actual_income" = "Actual Income";
|
||||
"actual_expenses" = "Actual Expenses";
|
||||
|
||||
// MARK: Profile
|
||||
"profile" = "Profile";
|
||||
|
|
|
@ -65,11 +65,13 @@
|
|||
"budget" = "Presupuesto";
|
||||
"budgets" = "Presupuestos";
|
||||
"new_budget" = "Nuevo Presupuesto";
|
||||
"current_balance" = "Saldo Actual:";
|
||||
"income" = "Ingresos:";
|
||||
"expenses" = "Gastos:";
|
||||
"expected" = "Planeados:";
|
||||
"actual" = "Actuales:";
|
||||
"stats" = "Estadísticas";
|
||||
"cash_flow" = "Flujo de dinero";
|
||||
"expected_vs_actual" = "Planeado vs Actual";
|
||||
"expected_income" = "Ingresos Planeados";
|
||||
"expected_expenses" = "Gastos Planeados";
|
||||
"actual_income" = "Ingresos Actuales";
|
||||
"actual_expenses" = "Gastos Actuales";
|
||||
|
||||
|
||||
// MARK: Profile
|
||||
|
|
Loading…
Reference in a new issue