From 18dffd81e59fd065193d33fed7f9712a5a702b83 Mon Sep 17 00:00:00 2001 From: Eddy Harrington Date: Fri, 15 May 2020 13:25:02 -0700 Subject: [PATCH] added payers report --- app.py | 21 ++++++++-- static/js/dashboard.js | 78 +++++++++++++++++++++++++++++++++++++ static/js/reports.js | 78 +++++++++++++++++++++++++++++++++++++ templates/index.html | 36 +++++++++++++++++ templates/payersreport.html | 59 ++++++++++++++++++++++++++++ templates/reports.html | 2 +- tendie_account.py | 3 +- tendie_reports.py | 30 +++++++++++++- 8 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 templates/payersreport.html diff --git a/app.py b/app.py index e1f9b5c..44de076 100644 --- a/app.py +++ b/app.py @@ -152,6 +152,8 @@ def index(): # User reached route via GET if request.method == "GET": + # TODO reduce or completely remove the redundant use of javascript code in dashboard.js and reports.js + # Initialize metrics to None to render the appropriate UX if data does not exist yet for the user expenses_year = None expenses_month = None @@ -198,15 +200,15 @@ def index(): spending_month = tendie_dashboard.getMonthlySpending( session["user_id"]) - # TODO consider passing additional vars to the template that has strings formatted for the charts. E.g. javascript needs months/data in an array, - # but due to jinja looping it makes the HTML doc render really messy with a lot of spaces. Might be better to just pass a single string for those charts. - # Get spending trends for the user spending_trends = tendie_dashboard.getSpendingTrends( session["user_id"]) + # Get payer spending for the user + payersChart = tendie_reports.generatePayersReport(session["user_id"]) + return render_template("index.html", categories=categories, payers=payers, date=date, income=income, expenses_year=expenses_year, expenses_month=expenses_month, expenses_week=expenses_week, expenses_last5=expenses_last5, - budgets=budgets, spending_week=spending_week, spending_month=spending_month, spending_trends=spending_trends) + budgets=budgets, spending_week=spending_week, spending_month=spending_month, spending_trends=spending_trends, payersChart=payersChart) # User reached route via POST else: @@ -792,6 +794,17 @@ def updateaccount(): return render_template("account.html", username=user["name"], income=user["income"], payers=user["payers"], stats=user["stats"], newIncome=None, addPayer=None, renamedPayer=None, deletedPayer=None, updatedPassword=None) +@app.route("/payersreport", methods=["GET"]) +@login_required +def payersreport(): + """View payers spending report""" + + # Generate a data structure that combines the users payers and expense data for chart and table + payersReport = tendie_reports.generatePayersReport(session["user_id"]) + + return render_template("payersreport.html", payers=payersReport) + + # Handle errors by rendering apology def errorhandler(e): """Handle error""" diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 9bcf4d5..fcb78d2 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -24,6 +24,12 @@ function loadTrendsData(trendsData) { loadSpendingTrendsChart(spendingTrends); } +// Loads *payers* data from Flask/Jinja that is passed from the request +function loadPayersData(payersData) { + payersSpending = JSON.parse(payersData); + loadPayersSpendingChart(payersSpending); +} + // After the modal is fully rendered, focus input into the new 'description' field $('#quickExpenseModal').on('shown.bs.modal', function () { $('#description').trigger('focus') @@ -245,4 +251,76 @@ function loadSpendingTrendsChart(spendingTrends) { } }); } +} + +function loadPayersSpendingChart(payersSpending) { + if (payersSpending == null) { + return; + } + else { + // Loop through the payers object and build the labels + payerNames = [] + for (i = 0; i < payersSpending.length; i++) { + // If the payer represents less than 1% of overall expenses do not include + if (payersSpending[i].percentAmount < 1) { + continue; + } + else { + payerNames.push(payersSpending[i].name) + } + } + + // Loop through the payers object and build the amounts dataset + payerAmounts = [] + for (i = 0; i < payersSpending.length; i++) { + // If the payer represents less than 1% of overall expenses do not include + if (payersSpending[i].percentAmount < 1) { + continue; + } + else { + payerAmounts.push(payersSpending[i].amount) + } + } + + // Build the chart + chartElement = document.getElementById('payersChart').getContext('2d'); + budgetCharts = new Chart(chartElement, { + type: 'pie', + data: { + labels: payerNames, + datasets: [{ + data: payerAmounts, + // Note: the color scheme for this is hard-coded with an assumption of 6 total payers including the default 'Self' payer. + // If max payer count changes in the future, the background/border colors will need to as well + backgroundColor: [ + 'rgba(240, 173, 78, 1)', + 'rgba(2, 184, 117, 1)', + 'rgba(69, 130, 236)', + 'rgba(23, 162, 184)', + 'rgba(202, 207, 212)', + 'rgba(217, 83, 79)' + ], + borderColor: [ + 'rgba(192, 138, 62, 1)', + 'rgba(1, 147, 93, 1)', + 'rgba(64, 121, 220)', + 'rgba(21, 151, 171)', + 'rgba(173, 181, 189)', + 'rgba(195, 74, 71)' + ], + borderWidth: 2 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + legend: { + labels: { + fontColor: 'black' + } + } + } + }); + + } } \ No newline at end of file diff --git a/static/js/reports.js b/static/js/reports.js index e122231..5f48218 100644 --- a/static/js/reports.js +++ b/static/js/reports.js @@ -22,6 +22,12 @@ function loadTrendsData(trendsData_chart, trendsData_table) { loadSpendingTrendsTable(spendingTrends_table); } +// Loads *payers* data from Flask/Jinja that is passed from the request +function loadPayersData(payersData) { + payersSpending = JSON.parse(payersData); + loadPayersSpendingChart(payersSpending); +} + function loadBudgetCharts(budgets) { if (budgets == null) { return; @@ -255,4 +261,76 @@ function loadSpendingTrendsTable(spendingTrends_table) { buttons: ['copy', 'csv', 'excel', 'colvis'] }); } +} + +function loadPayersSpendingChart(payersSpending) { + if (payersSpending == null) { + return; + } + else { + // Loop through the payers object and build the labels + payerNames = [] + for (i = 0; i < payersSpending.length; i++) { + // If the payer represents less than 1% of overall expenses do not include + if (payersSpending[i].percentAmount < 1) { + continue; + } + else { + payerNames.push(payersSpending[i].name) + } + } + + // Loop through the payers object and build the amounts dataset + payerAmounts = [] + for (i = 0; i < payersSpending.length; i++) { + // If the payer represents less than 1% of overall expenses do not include + if (payersSpending[i].percentAmount < 1) { + continue; + } + else { + payerAmounts.push(payersSpending[i].amount) + } + } + + // Build the chart + chartElement = document.getElementById('payersChart').getContext('2d'); + budgetCharts = new Chart(chartElement, { + type: 'pie', + data: { + labels: payerNames, + datasets: [{ + data: payerAmounts, + // Note: the color scheme for this is hard-coded with an assumption of 6 total payers including the default 'Self' payer. + // If max payer count changes in the future, the background/border colors will need to as well + backgroundColor: [ + 'rgba(240, 173, 78, 1)', + 'rgba(2, 184, 117, 1)', + 'rgba(69, 130, 236)', + 'rgba(23, 162, 184)', + 'rgba(202, 207, 212)', + 'rgba(217, 83, 79)' + ], + borderColor: [ + 'rgba(192, 138, 62, 1)', + 'rgba(1, 147, 93, 1)', + 'rgba(64, 121, 220)', + 'rgba(21, 151, 171)', + 'rgba(173, 181, 189)', + 'rgba(195, 74, 71)' + ], + borderWidth: 2 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + legend: { + labels: { + fontColor: 'black' + } + } + } + }); + + } } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index d3a37dd..c0512d9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -143,6 +143,7 @@

{{ expenses_week | usd }}

Last 5 Expenses

+

(view all expense history)

{% if expenses_last5 != None %}
@@ -180,6 +181,7 @@

Last 5 Expenses

Your Budgets

+

(view all budgets)

@@ -237,6 +239,7 @@

Weekly Spending

Monthly Spending

+

(view full monthly report)

@@ -260,6 +263,7 @@

Monthly Spending

Spending Habits

+

(view full spending report)

@@ -279,6 +283,31 @@

Spending Habits

{% endif %}
+ + +
+
+
+

Payer Spending

+

(view full payers report)

+
+
+
+ {% if payersChart %} +
+
+ +
+

Chart note: does not include payers that represent less than 1% of overall spending

+
+ {% else %} +
+

Chart will not display until you record at least 1 + expense.

+
+ {% endif %} +
+
@@ -312,4 +341,11 @@

Spending Habits

{% endif %} +{% if payersChart %} + +{% endif %} + {% endblock %} \ No newline at end of file diff --git a/templates/payersreport.html b/templates/payersreport.html new file mode 100644 index 0000000..ff0b542 --- /dev/null +++ b/templates/payersreport.html @@ -0,0 +1,59 @@ +{% extends "layout.html" %} + +{% block scripts %} + + +{% endblock %} + +{% block title %} + Reports | Payers Spending +{% endblock %} + +{% block main %} +

Payers Spending Report

+
+ + +
+ +
+

Chart note: does not include payers that represent less than 1% of overall spending

+
+ +

Total Paid Per Payer

+
+
+ + + + + + + + + {% for payer in payers %} + + + + + + {% endfor %} + + + + + + + + +
PayerAmount% of total
{{ payer["name"] }}{{ payer["amount"] | usd }}{{ payer["percentAmount"] }}%
Total{{ payers | sum(attribute="amount") | usd }}{{ payers | sum(attribute="percentAmount")}}%
+
+ + + + +{% endblock %} + diff --git a/templates/reports.html b/templates/reports.html index ada41a7..6fbbc27 100644 --- a/templates/reports.html +++ b/templates/reports.html @@ -43,7 +43,7 @@
Spending Categories
Payer

View how spending is managed between payers.

- View Report + View Report
diff --git a/tendie_account.py b/tendie_account.py index bb24824..230d86c 100644 --- a/tendie_account.py +++ b/tendie_account.py @@ -47,7 +47,8 @@ def getPayers(userID): # Add a payer to the users account def addPayer(name, userID): - # Make sure the user has no more than 5 payers (note: this max amount is arbitrary, 5 sounded good ¯\_(ツ)_/¯) + # Make sure the user has no more than 5 payers (6 w/ default 'Self') (note: this max amount is arbitrary, 5 sounded good ¯\_(ツ)_/¯. + # Also note that the payers report charts have 5 hardcoded color pallettes that will need to be updated if the max number of payers is changed in the future) if getTotalPayers(userID) >= 5: return {"apology": "You have the maximum number of payers. Try deleting one you aren't using or contact the admin."} diff --git a/tendie_reports.py b/tendie_reports.py index b1ae22a..1452b92 100644 --- a/tendie_reports.py +++ b/tendie_reports.py @@ -33,7 +33,7 @@ def generateBudgetsReport(userID): return budgetsReport -# Generates data needed for the monthly spending report by gathering monthly data need +# Generates data needed for the monthly spending report def generateMonthlyReport(userID): # Create data structure to hold users monthly spending data for the chart (monthly summed data) @@ -128,3 +128,31 @@ def generateSpendingTrendsReport(userID): "table": spending_trends_table, "categories": categories} return spendingTrendsReport + + +# Generates data needed for the payers spending report +def generatePayersReport(userID): + + # First get all of the payers from expenses table (this may include payers that don't exist anymore for the user (i.e. deleted the payer and didn't update expense records)) + payers = db.execute( + "SELECT payer AS 'name', SUM(amount) AS 'amount' FROM expenses WHERE user_id = :usersID GROUP BY payer ORDER BY amount DESC", usersID=userID) + + # Now get any payers the user has in their account but haven't expensed anything + nonExpensePayers = db.execute( + "SELECT name FROM payers WHERE user_id = :usersID AND name NOT IN (SELECT payer FROM expenses WHERE expenses.user_id = :usersID)", usersID=userID) + + # Add the non-expense payers to the payers data structure and set their amounts to 0 + for payer in nonExpensePayers: + newPayer = {"name": payer["name"], "amount": 0} + payers.append(newPayer) + + # Calculate the total paid for all payers combined + totalPaid = 0 + for payer in payers: + totalPaid = totalPaid + payer["amount"] + + # Calculate the % paid per payer and add to the data structure + for payer in payers: + payer["percentAmount"] = round((payer["amount"] / totalPaid) * 100) + + return payers