From dc9a21a5013501abb7bc13c93aba0de7de54049c Mon Sep 17 00:00:00 2001 From: graham-dds Date: Mon, 9 Dec 2019 09:20:31 -0500 Subject: [PATCH] Refactor mock reporting data and class methods All mock reporting data was moved to a JSON file. The concept of what JEDI CLIN a particular environment drew money from was added to the data. This change had a cascade effect to the reporting class methods, templates, and Vue components that ingested that reporting data. Many of these files were modified to adapt to these changes. This also included modifying the obligated funding bar graphs to reflect new design changes. --- atst/domain/csp/fixture_spend_data.json | 390 ++++++++++++++++ atst/domain/csp/reports.py | 436 +++++------------- atst/domain/reports.py | 29 +- atst/filters.py | 10 + atst/routes/portfolios/index.py | 9 +- js/components/tables/spend_table.js | 22 +- styles/core/_util.scss | 4 + styles/sections/_reports.scss | 73 ++- .../reports/application_and_env_spending.html | 31 +- .../reports/expired_task_orders.html | 10 +- .../portfolios/reports/obligated_funds.html | 62 ++- 11 files changed, 676 insertions(+), 400 deletions(-) create mode 100644 atst/domain/csp/fixture_spend_data.json diff --git a/atst/domain/csp/fixture_spend_data.json b/atst/domain/csp/fixture_spend_data.json new file mode 100644 index 00000000..2a59eadf --- /dev/null +++ b/atst/domain/csp/fixture_spend_data.json @@ -0,0 +1,390 @@ +{ + "A-Wing": { + "applications": [ + { + "name": "LC04", + "environments": [ + { + "name": "Integ", + "spending": { + "this_month": { + "JEDI_CLIN_1": 663, + "JEDI_CLIN_2": 397 + }, + "last_month": { + "JEDI_CLIN_1": 590, + "JEDI_CLIN_2": 829 + }, + "total": { + "JEDI_CLIN_1": 42467, + "JEDI_CLIN_2": 33873 + } + } + }, + { + "name": "PreProd", + "spending": { + "this_month": { + "JEDI_CLIN_1": 1000, + "JEDI_CLIN_2": 626 + }, + "last_month": { + "JEDI_CLIN_1": 685, + "JEDI_CLIN_2": 331 + }, + "total": { + "JEDI_CLIN_1": 21874, + "JEDI_CLIN_2": 25506 + } + } + }, + { + "name": "Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 856, + "JEDI_CLIN_2": 627 + }, + "last_month": { + "JEDI_CLIN_1": 921, + "JEDI_CLIN_2": 473 + }, + "total": { + "JEDI_CLIN_1": 35566, + "JEDI_CLIN_2": 42514 + } + } + } + ] + }, + { + "name": "SF18", + "environments": [ + { + "name": "Integ", + "spending": { + "this_month": { + "JEDI_CLIN_1": 777, + "JEDI_CLIN_2": 850 + }, + "last_month": { + "JEDI_CLIN_1": 584, + "JEDI_CLIN_2": 362 + }, + "total": { + "JEDI_CLIN_1": 44505, + "JEDI_CLIN_2": 21378 + } + } + }, + { + "name": "PreProd", + "spending": { + "this_month": { + "JEDI_CLIN_1": 487, + "JEDI_CLIN_2": 733 + }, + "last_month": { + "JEDI_CLIN_1": 542, + "JEDI_CLIN_2": 999 + }, + "total": { + "JEDI_CLIN_1": 8713, + "JEDI_CLIN_2": 10586 + } + } + }, + { + "name": "Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 420, + "JEDI_CLIN_2": 503 + }, + "last_month": { + "JEDI_CLIN_1": 756, + "JEDI_CLIN_2": 941 + }, + "total": { + "JEDI_CLIN_1": 43003, + "JEDI_CLIN_2": 20601 + } + } + } + ] + }, + { + "name": "Canton", + "environments": [ + { + "name": "Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 661, + "JEDI_CLIN_2": 599 + }, + "last_month": { + "JEDI_CLIN_1": 962, + "JEDI_CLIN_2": 383 + }, + "total": { + "JEDI_CLIN_1": 24501, + "JEDI_CLIN_2": 7551 + } + } + } + ] + }, + { + "name": "BD04", + "environments": [ + { + "name": "Integ", + "spending": { + "this_month": { + "JEDI_CLIN_1": 790, + "JEDI_CLIN_2": 513 + }, + "last_month": { + "JEDI_CLIN_1": 886, + "JEDI_CLIN_2": 991 + }, + "total": { + "JEDI_CLIN_1": 43684, + "JEDI_CLIN_2": 40196 + } + } + }, + { + "name": "PreProd", + "spending": { + "this_month": { + "JEDI_CLIN_1": 513, + "JEDI_CLIN_2": 706 + }, + "last_month": { + "JEDI_CLIN_1": 945, + "JEDI_CLIN_2": 380 + }, + "total": { + "JEDI_CLIN_1": 28189, + "JEDI_CLIN_2": 9759 + } + } + } + ] + }, + { + "name": "SCV18", + "environments": [ + { + "name": "Dev", + "spending": { + "this_month": { + "JEDI_CLIN_1": 933, + "JEDI_CLIN_2": 993 + }, + "last_month": { + "JEDI_CLIN_1": 319, + "JEDI_CLIN_2": 619 + }, + "total": { + "JEDI_CLIN_1": 40585, + "JEDI_CLIN_2": 28872 + } + } + } + ] + }, + { + "name": "Crown", + "environments": [ + { + "name": "CR Portal Dev", + "spending": { + "this_month": { + "JEDI_CLIN_1": 711, + "JEDI_CLIN_2": 413 + }, + "last_month": { + "JEDI_CLIN_1": 908, + "JEDI_CLIN_2": 632 + }, + "total": { + "JEDI_CLIN_1": 18753, + "JEDI_CLIN_2": 4004 + } + } + }, + { + "name": "CR Staging", + "spending": { + "this_month": { + "JEDI_CLIN_1": 440, + "JEDI_CLIN_2": 918 + }, + "last_month": { + "JEDI_CLIN_1": 370, + "JEDI_CLIN_2": 472 + }, + "total": { + "JEDI_CLIN_1": 40602, + "JEDI_CLIN_2": 6834 + } + } + }, + { + "name": "CR Portal Test 1", + "spending": { + "this_month": { + "JEDI_CLIN_1": 928, + "JEDI_CLIN_2": 796 + }, + "last_month": { + "JEDI_CLIN_1": 680, + "JEDI_CLIN_2": 312 + }, + "total": { + "JEDI_CLIN_1": 36058, + "JEDI_CLIN_2": 42375 + } + } + }, + { + "name": "Jewels Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 304, + "JEDI_CLIN_2": 428 + }, + "last_month": { + "JEDI_CLIN_1": 898, + "JEDI_CLIN_2": 729 + }, + "total": { + "JEDI_CLIN_1": 3162, + "JEDI_CLIN_2": 49836 + } + } + }, + { + "name": "Jewels Dev", + "spending": { + "this_month": { + "JEDI_CLIN_1": 498, + "JEDI_CLIN_2": 890 + }, + "last_month": { + "JEDI_CLIN_1": 506, + "JEDI_CLIN_2": 659 + }, + "total": { + "JEDI_CLIN_1": 6248, + "JEDI_CLIN_2": 3866 + } + } + } + ] + } + ] + }, + "B-Wing": { + "applications": [ + { + "name": "NP02", + "environments": [ + { + "name": "Integ", + "spending": { + "this_month": { + "JEDI_CLIN_1": 455, + "JEDI_CLIN_2": 746 + }, + "last_month": { + "JEDI_CLIN_1": 973, + "JEDI_CLIN_2": 504 + }, + "total": { + "JEDI_CLIN_1": 11493, + "JEDI_CLIN_2": 17751 + } + } + }, + { + "name": "PreProd", + "spending": { + "this_month": { + "JEDI_CLIN_1": 582, + "JEDI_CLIN_2": 339 + }, + "last_month": { + "JEDI_CLIN_1": 392, + "JEDI_CLIN_2": 885 + }, + "total": { + "JEDI_CLIN_1": 41856, + "JEDI_CLIN_2": 46399 + } + } + }, + { + "name": "Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 446, + "JEDI_CLIN_2": 670 + }, + "last_month": { + "JEDI_CLIN_1": 368, + "JEDI_CLIN_2": 963 + }, + "total": { + "JEDI_CLIN_1": 10030, + "JEDI_CLIN_2": 29253 + } + } + } + ] + }, + { + "name": "FM", + "environments": [ + { + "name": "Integ", + "spending": { + "this_month": { + "JEDI_CLIN_1": 994, + "JEDI_CLIN_2": 573 + }, + "last_month": { + "JEDI_CLIN_1": 699, + "JEDI_CLIN_2": 418 + }, + "total": { + "JEDI_CLIN_1": 27881, + "JEDI_CLIN_2": 37092 + } + } + }, + { + "name": "Prod", + "spending": { + "this_month": { + "JEDI_CLIN_1": 838, + "JEDI_CLIN_2": 839 + }, + "last_month": { + "JEDI_CLIN_1": 775, + "JEDI_CLIN_2": 946 + }, + "total": { + "JEDI_CLIN_1": 45007, + "JEDI_CLIN_2": 16197 + } + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/atst/domain/csp/reports.py b/atst/domain/csp/reports.py index 4b69e904..9949050e 100644 --- a/atst/domain/csp/reports.py +++ b/atst/domain/csp/reports.py @@ -1,344 +1,124 @@ -from itertools import groupby -import pendulum +from collections import defaultdict +import json +import os from decimal import Decimal -from collections import OrderedDict -class ReportingInterface: - def monthly_totals_for_environment(environment): - """Return the monthly totals for the specified environment. +def load_fixture_data(): + with open( + os.path.join(os.path.dirname(__file__), "fixture_spend_data.json"), "r" + ) as json_file: + return json.load(json_file) - Data should be in the format of a dictionary with the month as the key - and the spend in that month as the value. For example: - { "01/2018": 79.85, "02/2018": 86.54 } +class MockReportingProvider: + FIXTURE_SPEND_DATA = load_fixture_data() + @classmethod + def get_portfolio_monthly_spending(cls, portfolio): """ - raise NotImplementedError() - - -class MockEnvironment: - def __init__(self, id_, env_name): - self.id = id_ - self.name = env_name - - -class MockApplication: - def __init__(self, application_name, envs): - def make_env(name): - return MockEnvironment("{}_{}".format(application_name, name), name) - - self.name = application_name - self.environments = [make_env(env_name) for env_name in envs] - - -def generate_sample_dates(_max=8): - current = pendulum.now() - sample_dates = [] - for _i in range(_max): - sample_dates.append(current.strftime("%m/%Y")) - current = current.subtract(months=1) - - reversed(sample_dates) - return sample_dates - - -class MockReportingProvider(ReportingInterface): - MOCK_PERCENT_EXPENDED_FUNDS = 0.75 - - FIXTURE_MONTHS = generate_sample_dates() - - MONTHLY_SPEND_BY_ENVIRONMENT = { - "LC04_Integ": { - FIXTURE_MONTHS[7]: 284, - FIXTURE_MONTHS[6]: 1210, - FIXTURE_MONTHS[5]: 1430, - FIXTURE_MONTHS[4]: 1366, - FIXTURE_MONTHS[3]: 1169, - FIXTURE_MONTHS[2]: 991, - FIXTURE_MONTHS[1]: 978, - FIXTURE_MONTHS[0]: 737, - }, - "LC04_PreProd": { - FIXTURE_MONTHS[7]: 812, - FIXTURE_MONTHS[6]: 1389, - FIXTURE_MONTHS[5]: 1425, - FIXTURE_MONTHS[4]: 1306, - FIXTURE_MONTHS[3]: 1112, - FIXTURE_MONTHS[2]: 936, - FIXTURE_MONTHS[1]: 921, - FIXTURE_MONTHS[0]: 694, - }, - "LC04_Prod": { - FIXTURE_MONTHS[7]: 1742, - FIXTURE_MONTHS[6]: 1716, - FIXTURE_MONTHS[5]: 1866, - FIXTURE_MONTHS[4]: 1809, - FIXTURE_MONTHS[3]: 1839, - FIXTURE_MONTHS[2]: 1633, - FIXTURE_MONTHS[1]: 1654, - FIXTURE_MONTHS[0]: 1103, - }, - "SF18_Integ": { - FIXTURE_MONTHS[5]: 1498, - FIXTURE_MONTHS[4]: 1400, - FIXTURE_MONTHS[3]: 1394, - FIXTURE_MONTHS[2]: 1171, - FIXTURE_MONTHS[1]: 1200, - FIXTURE_MONTHS[0]: 963, - }, - "SF18_PreProd": { - FIXTURE_MONTHS[5]: 1780, - FIXTURE_MONTHS[4]: 1667, - FIXTURE_MONTHS[3]: 1703, - FIXTURE_MONTHS[2]: 1474, - FIXTURE_MONTHS[1]: 1441, - FIXTURE_MONTHS[0]: 933, - }, - "SF18_Prod": { - FIXTURE_MONTHS[5]: 1686, - FIXTURE_MONTHS[4]: 1779, - FIXTURE_MONTHS[3]: 1792, - FIXTURE_MONTHS[2]: 1570, - FIXTURE_MONTHS[1]: 1539, - FIXTURE_MONTHS[0]: 986, - }, - "Canton_Prod": { - FIXTURE_MONTHS[4]: 28699, - FIXTURE_MONTHS[3]: 26766, - FIXTURE_MONTHS[2]: 22619, - FIXTURE_MONTHS[1]: 24090, - FIXTURE_MONTHS[0]: 16719, - }, - "BD04_Integ": {}, - "BD04_PreProd": { - FIXTURE_MONTHS[7]: 7019, - FIXTURE_MONTHS[6]: 3004, - FIXTURE_MONTHS[5]: 2691, - FIXTURE_MONTHS[4]: 2901, - FIXTURE_MONTHS[3]: 3463, - FIXTURE_MONTHS[2]: 3314, - FIXTURE_MONTHS[1]: 3432, - FIXTURE_MONTHS[0]: 723, - }, - "SCV18_Dev": {FIXTURE_MONTHS[1]: 9797}, - "Crown_CR Portal Dev": { - FIXTURE_MONTHS[6]: 208, - FIXTURE_MONTHS[5]: 457, - FIXTURE_MONTHS[4]: 671, - FIXTURE_MONTHS[3]: 136, - FIXTURE_MONTHS[2]: 1524, - FIXTURE_MONTHS[1]: 2077, - FIXTURE_MONTHS[0]: 1858, - }, - "Crown_CR Staging": { - FIXTURE_MONTHS[6]: 208, - FIXTURE_MONTHS[5]: 457, - FIXTURE_MONTHS[4]: 671, - FIXTURE_MONTHS[3]: 136, - FIXTURE_MONTHS[2]: 1524, - FIXTURE_MONTHS[1]: 2077, - FIXTURE_MONTHS[0]: 1858, - }, - "Crown_CR Portal Test 1": { - FIXTURE_MONTHS[2]: 806, - FIXTURE_MONTHS[1]: 1966, - FIXTURE_MONTHS[0]: 2597, - }, - "Crown_Jewels Prod": { - FIXTURE_MONTHS[2]: 806, - FIXTURE_MONTHS[1]: 1966, - FIXTURE_MONTHS[0]: 2597, - }, - "Crown_Jewels Dev": { - FIXTURE_MONTHS[6]: 145, - FIXTURE_MONTHS[5]: 719, - FIXTURE_MONTHS[4]: 1243, - FIXTURE_MONTHS[3]: 2214, - FIXTURE_MONTHS[2]: 2959, - FIXTURE_MONTHS[1]: 4151, - FIXTURE_MONTHS[0]: 4260, - }, - "NP02_Integ": {FIXTURE_MONTHS[1]: 284, FIXTURE_MONTHS[0]: 1210}, - "NP02_PreProd": {FIXTURE_MONTHS[1]: 812, FIXTURE_MONTHS[0]: 1389}, - "NP02_Prod": {FIXTURE_MONTHS[1]: 3742, FIXTURE_MONTHS[0]: 4716}, - "FM_Integ": {FIXTURE_MONTHS[1]: 1498}, - "FM_Prod": {FIXTURE_MONTHS[0]: 5686}, - } - - REPORT_FIXTURE_MAP = { - "A-Wing": { - "applications": [ - MockApplication("LC04", ["Integ", "PreProd", "Prod"]), - MockApplication("SF18", ["Integ", "PreProd", "Prod"]), - MockApplication("Canton", ["Prod"]), - MockApplication("BD04", ["Integ", "PreProd"]), - MockApplication("SCV18", ["Dev"]), - MockApplication( - "Crown", - [ - "CR Portal Dev", - "CR Staging", - "CR Portal Test 1", - "Jewels Prod", - "Jewels Dev", - ], - ), - ], - "budget": 500_000, - }, - "B-Wing": { - "applications": [ - MockApplication("NP02", ["Integ", "PreProd", "Prod"]), - MockApplication("FM", ["Integ", "Prod"]), - ], - "budget": 70000, - }, - } - - def _rollup_application_totals(self, data): - application_totals = {} - for application, environments in data.items(): - application_spend = [ - (month, spend) - for env in environments.values() - if env - for month, spend in env.items() - ] - application_totals[application] = { - month: sum([spend[1] for spend in spends]) - for month, spends in groupby(sorted(application_spend), lambda x: x[0]) - } - - return application_totals - - def _rollup_portfolio_totals(self, application_totals): - monthly_spend = [ - (month, spend) - for application in application_totals.values() - for month, spend in application.items() - ] - portfolio_totals = {} - for month, spends in groupby(sorted(monthly_spend), lambda m: m[0]): - portfolio_totals[month] = sum([spend[1] for spend in spends]) - - return portfolio_totals - - def monthly_totals_for_environment(self, environment_id): - """Return the monthly totals for the specified environment. - - Data should be in the format of a dictionary with the month as the key - and the spend in that month as the value. For example: - - { "01/2018": 79.85, "02/2018": 86.54 } - - """ - environment_monthly_totals = self.MONTHLY_SPEND_BY_ENVIRONMENT.get( - environment_id, {} - ).copy() - - environment_monthly_totals["total_spend_to_date"] = sum( - monthly_total for monthly_total in environment_monthly_totals.values() - ) - return environment_monthly_totals - - def monthly_totals(self, portfolio): - """Return month totals rolled up by environment, application, and portfolio. - - Data should returned with three top level keys, "portfolio", "applications", - and "environments". - The "applications" key will have budget data per month for each application, - The "environments" key will have budget data for each environment. - The "portfolio" key will be total monthly spending for the portfolio. - For example: - + returns an array of application and environment spending for the + portfolio. Applications and their nested environments are sorted in + alphabetical order by name. + [ { - "environments": { "X-Wing": { "Prod": { "01/2018": 75.42 } } }, - "applications": { "X-Wing": { "01/2018": 75.42 } }, - "portfolio": { "01/2018": 75.42 }, + name + this_month + last_month + total + environments [ + { + name + this_month + last_month + total + } + ] } - + ] """ - applications = portfolio.applications - if portfolio.name in self.REPORT_FIXTURE_MAP: - applications = self.REPORT_FIXTURE_MAP[portfolio.name]["applications"] - environments = { - application.name: { - env.name: self.monthly_totals_for_environment(env.id) - for env in application.environments - } - for application in applications - } - - application_totals = self._rollup_application_totals(environments) - portfolio_totals = self._rollup_portfolio_totals(application_totals) - - return { - "environments": environments, - "applications": application_totals, - "portfolio": portfolio_totals, - } - - def get_obligated_funds_by_JEDI_clin(self, portfolio): - """ - Returns a dictionary of obligated funds and spending per JEDI CLIN - { - JEDI_CLIN: { - obligated_funds, - expended_funds - } - } - """ - if portfolio.name in self.REPORT_FIXTURE_MAP: - return_dict = {} - for jedi_clin, clins in groupby( - portfolio.active_clins, lambda clin: clin.jedi_clin_type - ): - obligated_funds = sum(clin.obligated_amount for clin in clins) - return_dict[jedi_clin.value] = { - "obligated_funds": obligated_funds, - "expended_funds": ( - obligated_funds * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS) - ), - } - return OrderedDict( - # 0 index for dict item, -1 for last digit of 4 digit CLIN, e.g. 0001 - sorted(return_dict.items(), key=lambda clin: clin[0][-1]) + if portfolio.name in cls.FIXTURE_SPEND_DATA: + applications = cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"] + return sorted( + [ + cls._get_application_monthly_totals(application) + for application in applications + ], + key=lambda app: app["name"], ) - return {} + return [] - def get_expired_task_orders(self, portfolio): - def sorted_task_orders(to_list): - return sorted(to_list, key=lambda to: to["number"]) + @classmethod + def _get_environment_monthly_totals(cls, environment): + """ + returns a dictionary that represents spending totals for an environment e.g. + { + name + this_month + last_month + total + } + """ + return { + "name": environment["name"], + "this_month": sum(environment["spending"]["this_month"].values()), + "last_month": sum(environment["spending"]["last_month"].values()), + "total": sum(environment["spending"]["total"].values()), + } - def sorted_clins(clin_list): - return sorted(clin_list, key=lambda clin: clin["number"]) - - def serialize_clin(clin): - return { - "number": clin.number, - "jedi_clin_type": clin.jedi_clin_type, - "period_of_performance": { - "start_date": clin.start_date, - "end_date": clin.end_date, - }, - "total_value": clin.total_amount, - "total_obligated_funds": clin.obligated_amount, - "expended_funds": ( - clin.obligated_amount * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS) - ), + @classmethod + def _get_application_monthly_totals(cls, application): + """ + returns a dictionary that represents spending totals for an application + and its environments e.g. + { + name + this_month + last_month + total + environments: [ + { + name + this_month + last_month + total + } + ] } - - return sorted_task_orders( + """ + environments = sorted( [ - { - "id": task_order.id, - "number": task_order.number, - "clins": sorted_clins( - [serialize_clin(clin) for clin in task_order.clins] - ), - } - for task_order in portfolio.task_orders - if task_order.is_expired - ] + cls._get_environment_monthly_totals(env) + for env in application["environments"] + ], + key=lambda env: env["name"], ) + return { + "name": application["name"], + "this_month": sum(env["this_month"] for env in environments), + "last_month": sum(env["last_month"] for env in environments), + "total": sum(env["total"] for env in environments), + "environments": environments, + } + + @classmethod + def get_spending_by_JEDI_clin(cls, portfolio): + """ + returns an dictionary of spending per JEDI CLIN for a portfolio + { + jedi_clin: { + invoiced + estimated + }, + } + """ + if portfolio.name in cls.FIXTURE_SPEND_DATA: + CLIN_spend_dict = defaultdict(lambda: defaultdict(Decimal)) + for application in cls.FIXTURE_SPEND_DATA[portfolio.name]["applications"]: + for environment in application["environments"]: + for clin, spend in environment["spending"]["this_month"].items(): + CLIN_spend_dict[clin]["estimated"] += Decimal(spend) + for clin, spend in environment["spending"]["total"].items(): + CLIN_spend_dict[clin]["invoiced"] += Decimal(spend) + return CLIN_spend_dict + return {} diff --git a/atst/domain/reports.py b/atst/domain/reports.py index 94f6c54e..22760ec9 100644 --- a/atst/domain/reports.py +++ b/atst/domain/reports.py @@ -1,15 +1,36 @@ from flask import current_app +from itertools import groupby class Reports: @classmethod - def monthly_totals(cls, portfolio): - return current_app.csp.reports.monthly_totals(portfolio) + def monthly_spending(cls, portfolio): + return current_app.csp.reports.get_portfolio_monthly_spending(portfolio) @classmethod def expired_task_orders(cls, portfolio): - return current_app.csp.reports.get_expired_task_orders(portfolio) + return [ + task_order for task_order in portfolio.task_orders if task_order.is_expired + ] @classmethod def obligated_funds_by_JEDI_clin(cls, portfolio): - return current_app.csp.reports.get_obligated_funds_by_JEDI_clin(portfolio) + clin_spending = current_app.csp.reports.get_spending_by_JEDI_clin(portfolio) + active_clins = portfolio.active_clins + for jedi_clin, clins in groupby( + active_clins, key=lambda clin: clin.jedi_clin_type + ): + if not clin_spending.get(jedi_clin.name): + clin_spending[jedi_clin.name] = {} + clin_spending[jedi_clin.name]["obligated"] = sum( + clin.obligated_amount for clin in clins + ) + return [ + { + "name": clin, + "invoiced": clin_spending[clin].get("invoiced", 0), + "estimated": clin_spending[clin].get("estimated", 0), + "obligated": clin_spending[clin].get("obligated", 0), + } + for clin in sorted(clin_spending.keys()) + ] diff --git a/atst/filters.py b/atst/filters.py index 48b8166c..3508f1e9 100644 --- a/atst/filters.py +++ b/atst/filters.py @@ -5,6 +5,7 @@ from flask import render_template from jinja2 import contextfilter from jinja2.exceptions import TemplateNotFound from urllib.parse import urlparse, urlunparse, parse_qs, urlencode +from decimal import DivisionByZero as DivisionByZeroException def iconSvg(name): @@ -38,6 +39,14 @@ def usPhone(number): return "+1 ({}) {} - {}".format(phone[0:3], phone[3:6], phone[6:]) +def obligatedFundingGraphWidth(values): + numerator, denominator = values + try: + return (numerator / denominator) * 100 + except DivisionByZeroException: + return 0 + + def formattedDate(value, formatter="%m/%d/%Y"): if value: return value.strftime(formatter) @@ -76,6 +85,7 @@ def register_filters(app): app.jinja_env.filters["pageWindow"] = pageWindow app.jinja_env.filters["renderAuditEvent"] = renderAuditEvent app.jinja_env.filters["withExtraParams"] = with_extra_params + app.jinja_env.filters["obligatedFundingGraphWidth"] = obligatedFundingGraphWidth @contextfilter def translateWithoutCache(context, *kwargs): diff --git a/atst/routes/portfolios/index.py b/atst/routes/portfolios/index.py index a34d9057..8787c4f6 100644 --- a/atst/routes/portfolios/index.py +++ b/atst/routes/portfolios/index.py @@ -1,4 +1,4 @@ -from datetime import date, datetime, timedelta +from datetime import datetime from flask import redirect, render_template, url_for, request as http_request, g @@ -35,9 +35,6 @@ def create_portfolio(): @user_can(Permissions.VIEW_PORTFOLIO_REPORTS, message="view portfolio reports") def reports(portfolio_id): portfolio = Portfolios.get(g.current_user, portfolio_id) - today = date.today() - current_month = date(int(today.year), int(today.month), 15) - prev_month = current_month - timedelta(days=28) # wrapped in str() because the sum of obligated funds returns a Decimal object total_portfolio_value = str( sum( @@ -51,10 +48,10 @@ def reports(portfolio_id): total_portfolio_value=total_portfolio_value, current_obligated_funds=Reports.obligated_funds_by_JEDI_clin(portfolio), expired_task_orders=Reports.expired_task_orders(portfolio), - monthly_totals=Reports.monthly_totals(portfolio), + monthly_spending=Reports.monthly_spending(portfolio), current_month=current_month, prev_month=prev_month, - now=datetime.now(), # mocked datetime of reporting data retrival + retrieved=datetime.now(), # mocked datetime of reporting data retrival ) diff --git a/js/components/tables/spend_table.js b/js/components/tables/spend_table.js index c3a1c90f..bd5f5a01 100644 --- a/js/components/tables/spend_table.js +++ b/js/components/tables/spend_table.js @@ -1,14 +1,12 @@ import { set } from 'vue/dist/vue' import { formatDollars } from '../../lib/dollars' +import { set as _set } from 'lodash' export default { name: 'spend-table', props: { - applications: Object, - environments: Object, - currentMonthIndex: String, - prevMonthIndex: String, + applications: Array, }, data: function() { @@ -18,20 +16,16 @@ export default { }, created: function() { - Object.keys(this.applications).forEach(application => { - set(this.applicationsState[application], 'isVisible', false) + this.applicationsState.forEach(application => { + application.isVisible = false }) }, methods: { - toggle: function(e, applicationName) { - this.applicationsState = Object.assign(this.applicationsState, { - [applicationName]: Object.assign( - this.applicationsState[applicationName], - { - isVisible: !this.applicationsState[applicationName].isVisible, - } - ), + toggle: function(e, applicationIndex) { + set(this.applicationsState, applicationIndex, { + ...this.applicationsState[applicationIndex], + isVisible: !this.applicationsState[applicationIndex].isVisible, }) }, diff --git a/styles/core/_util.scss b/styles/core/_util.scss index b719d855..ff6e8e3f 100644 --- a/styles/core/_util.scss +++ b/styles/core/_util.scss @@ -73,6 +73,10 @@ color: $color-green; } +.text-danger { + color: $color-secondary; +} + .user-permission { font-weight: $font-normal; } diff --git a/styles/sections/_reports.scss b/styles/sections/_reports.scss index 0cd737c8..ee43ffa2 100644 --- a/styles/sections/_reports.scss +++ b/styles/sections/_reports.scss @@ -16,6 +16,16 @@ } .jedi-clin-funding { + $insufficient-gradient: repeating-linear-gradient( + 45deg, + $color-secondary-dark, + $color-secondary-dark 10px, + $color-secondary-darkest 11px, + $color-secondary-darkest 14px + ); + + $graph-bar-height: 2rem; + padding-top: $gap * 3; padding-bottom: $gap * 3; @@ -37,14 +47,36 @@ margin: 0; } - &__meter { - margin: 10px 0; - -moz-transform: scale(-1, 1); - -webkit-transform: scale(-1, 1); - -o-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - transform: scale(-1, 1); + &__graph { width: 100%; + height: $graph-bar-height; + margin-top: $gap * 2; + margin-bottom: $gap * 2; + display: flex; + + &-bar { + height: 100%; + display: block; + float: left; + margin-right: $gap / 2; + + &:last-child { + margin-right: 0; + } + + &--invoiced { + background: $color-green; + } + &--estimated { + background: $color-green-lighter; + } + &--remaining { + background: $color-primary-darkest; + } + &--insufficient { + background: $insufficient-gradient; + } + } &-values { display: flex; @@ -52,13 +84,32 @@ } &__meta { - &--remaining { - margin-left: auto; - text-align: right; - } + margin-right: $gap * 5; + &-header { @include small-copy; margin-bottom: 0; + display: flex; + align-items: center; + } + + &-key { + height: $graph-bar-height; + width: $graph-bar-height; + margin-right: $gap / 2; + + &--invoiced { + background: $color-green; + } + &--estimated { + background: $color-green-lighter; + } + &--remaining { + background: $color-primary-darkest; + } + &--insufficient { + background: $insufficient-gradient; + } } &-value { margin-bottom: 0; diff --git a/templates/portfolios/reports/application_and_env_spending.html b/templates/portfolios/reports/application_and_env_spending.html index 0af72112..c7d94f55 100644 --- a/templates/portfolios/reports/application_and_env_spending.html +++ b/templates/portfolios/reports/application_and_env_spending.html @@ -24,12 +24,7 @@ ) }} {% else %} - +
@@ -41,41 +36,41 @@ -