Merge pull request #1216 from dod-ccpo/reports-styling-summary-and-obligated-funds

Reports styling summary and obligated funds
This commit is contained in:
graham-dds 2019-11-27 10:39:31 -05:00 committed by GitHub
commit 582ddc94b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 148 additions and 303 deletions

View File

@ -1,7 +1,7 @@
from itertools import groupby from itertools import groupby
from atst.utils.localization import translate
import pendulum import pendulum
from decimal import Decimal from decimal import Decimal
from collections import OrderedDict
class ReportingInterface: class ReportingInterface:
@ -295,13 +295,16 @@ class MockReportingProvider(ReportingInterface):
portfolio.active_clins, lambda clin: clin.jedi_clin_type portfolio.active_clins, lambda clin: clin.jedi_clin_type
): ):
obligated_funds = sum(clin.obligated_amount for clin in clins) obligated_funds = sum(clin.obligated_amount for clin in clins)
return_dict[translate(f"JEDICLINType.{jedi_clin.value}")] = { return_dict[jedi_clin.value] = {
"obligated_funds": obligated_funds, "obligated_funds": obligated_funds,
"expended_funds": ( "expended_funds": (
obligated_funds * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS) obligated_funds * Decimal(self.MOCK_PERCENT_EXPENDED_FUNDS)
), ),
} }
return return_dict 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])
)
return {} return {}
def get_expired_task_orders(self, portfolio): def get_expired_task_orders(self, portfolio):

View File

@ -117,8 +117,8 @@ ENV_ROLES = [(role.value, role.value) for role in CSPRole] + [
] ]
JEDI_CLIN_TYPES = [ JEDI_CLIN_TYPES = [
("JEDI_CLIN_1", translate("forms.task_order.clin_01_label")), ("JEDI_CLIN_1", translate("JEDICLINType.JEDI_CLIN_1")),
("JEDI_CLIN_2", translate("forms.task_order.clin_02_label")), ("JEDI_CLIN_2", translate("JEDICLINType.JEDI_CLIN_2")),
("JEDI_CLIN_3", translate("forms.task_order.clin_03_label")), ("JEDI_CLIN_3", translate("JEDICLINType.JEDI_CLIN_3")),
("JEDI_CLIN_4", translate("forms.task_order.clin_04_label")), ("JEDI_CLIN_4", translate("JEDICLINType.JEDI_CLIN_4")),
] ]

View File

@ -1,4 +1,4 @@
from datetime import date, timedelta from datetime import date, datetime, timedelta
from flask import redirect, render_template, url_for, request as http_request, g from flask import redirect, render_template, url_for, request as http_request, g
@ -64,6 +64,7 @@ def reports(portfolio_id):
monthly_totals=Reports.monthly_totals(portfolio), monthly_totals=Reports.monthly_totals(portfolio),
current_month=current_month, current_month=current_month,
prev_month=prev_month, prev_month=prev_month,
now=datetime.now(), # mocked datetime of reporting data retrival
) )

View File

@ -102,3 +102,7 @@ dl {
font-weight: $font-bold; font-weight: $font-bold;
color: $color-black; color: $color-black;
} }
@mixin small-copy {
font-size: $small-font-size;
}

View File

@ -1,264 +1,90 @@
.funding-summary-row { .portfolio-reports {
@include media($medium-screen) { .estimate-warning {
@include grid-row; margin-top: $gap * 3;
margin-bottom: $gap * 3;
flex-wrap: wrap;
} }
&__col { .reporting-section-header {
hr {
margin: (2 * $gap) 0;
}
@include media($medium-screen) {
@include grid-pad;
flex-grow: 1;
display: flex; display: flex;
flex-direction: row; align-items: baseline;
flex-basis: 50%; &__header {
@include ie-only {
max-width: 50%;
}
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
}
}
align-items: stretch;
.panel {
padding: $gap * 2;
width: 100%;
@include ie-only {
max-width: 100%;
}
.subheading {
@include h4;
margin: 0 $gap (2 * $gap) 0;
-ms-flex-negative: 1;
}
// Spending Summary
// ===============================
&.spend-summary {
display: flex;
flex-direction: column;
justify-content: space-between;
.row {
justify-content: space-between;
@include ie-only {
max-width: 100%;
flex-wrap: wrap;
}
}
&__budget {
@include ie-only {
margin: $gap 0 0 0;
}
}
dl {
text-align: left;
margin: 0 0 ($gap / 2) 0;
@include ie-only {
text-align: left;
}
dt {
text-transform: uppercase;
color: $color-gray-light;
margin-right: $gap; margin-right: $gap;
font-weight: bold; }
font-size: $small-font-size; &__subheader {
@include small-copy;
} }
} }
meter { .jedi-clin-funding {
width: 100%; padding-top: $gap * 3;
height: 3rem; padding-bottom: $gap * 3;
margin: ($gap * 2) 0 0;
}
&__spent { &__clin-wrapper {
margin: (2 * $gap) 0; border-bottom: 1px solid $color-gray-light;
display: flex; margin-bottom: $gap * 3;
flex-direction: column; padding-bottom: $gap * 3;
justify-content: flex-end;
dt {
letter-spacing: 0.47px;
} }
} > div:nth-last-child(2) {
}
// Task Order Summary
// ===============================
&.to-summary {
.icon-link {
font-weight: $font-normal;
}
.subheading {
margin-bottom: 0; margin-bottom: 0;
} }
.to-summary__heading { &__header {
@include h4;
margin: 0 $gap 0 0;
}
.to-summary__to-number {
margin: 0;
dd {
&::before {
content: "#";
color: $color-gray;
margin-right: $gap;
}
}
@include media($xlarge-screen) {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
.to-summary__to {
margin: 0 $gap 0 0;
}
.to-summary__expiration {
text-align: right;
flex-grow: 1;
dl {
margin: 0 0 0 $gap;
dd,
dt {
display: inline;
}
}
}
}
}
.to-summary__expiration {
dl {
text-align: right;
margin-top: -2 * $gap;
dd,
dt {
display: inline;
}
dt {
font-size: $small-font-size;
text-transform: uppercase;
font-weight: $font-bold;
color: $color-gray-light;
}
dd.ending-soon {
font-size: $h2-font-size;
white-space: nowrap;
.icon {
@include icon-size(28);
}
}
}
.icon-link {
margin: 0 (-$gap);
}
}
.to-summary__co {
margin: ($gap * 2) 0 0 0;
@include media($xlarge-screen) {
margin: 0; margin: 0;
} }
}
}
}
}
}
.spend-table__month-select { &__subheader {
@include small-copy;
margin: 0; margin: 0;
flex: 1;
} }
table { &__meter {
.spend-table__portfolio { margin: 10px 0;
th, -moz-transform: scale(-1, 1);
td { -webkit-transform: scale(-1, 1);
font-weight: bold; -o-transform: scale(-1, 1);
} -ms-transform: scale(-1, 1);
} transform: scale(-1, 1);
th,
td {
&.previous-month {
color: $color-gray;
}
&.meter-cell {
padding-left: 0;
position: relative;
min-width: 4rem;
@include media($medium-screen) {
min-width: 12rem;
}
meter {
width: 100%; width: 100%;
height: 3rem;
background: $color-white;
display: none;
@include media($medium-screen) { &-values {
display: block; display: flex;
}
&::-webkit-meter-bar {
background: $color-white;
} }
} }
.spend-table__meter-value { &__meta {
@include h5; &--remaining {
margin-left: auto;
text-align: right;
}
&-header {
@include small-copy;
margin-bottom: 0;
}
&-value {
margin-bottom: 0;
line-height: 1.2;
}
}
}
@include media($medium-screen) { .reporting-summary-item {
display: block; border-right: 1px solid $color-gray-light;
color: $color-white; margin-right: $gap * 3;
background-color: rgba($color-blue, 0.65); padding-right: $gap * 3;
border-radius: $gap / 2; &:last-child {
position: absolute; border-right: none;
top: 2.3rem; margin-right: 0;
left: $gap / 2; padding-right: 0;
padding: 0 ($gap / 2); }
} &__header {
} margin: 0;
&-icon {
margin: 0;
padding: 0;
}
}
&__value {
font-size: $lead-font-size;
} }
} }
} }

View File

@ -92,10 +92,10 @@
</div> </div>
</legend> </legend>
<select :id='name' :name='name'> <select :id='name' :name='name'>
<option value="JEDI_CLIN_1">{{ "forms.task_order.clin_01_label" | translate }}</option> <option value="JEDI_CLIN_1">{{ "JEDICLINType.JEDI_CLIN_1" | translate }}</option>
<option value="JEDI_CLIN_2">{{ "forms.task_order.clin_02_label" | translate }}</option> <option value="JEDI_CLIN_2">{{ "JEDICLINType.JEDI_CLIN_2" | translate }}</option>
<option value="JEDI_CLIN_3">{{ "forms.task_order.clin_03_label" | translate }}</option> <option value="JEDI_CLIN_3">{{ "JEDICLINType.JEDI_CLIN_3" | translate }}</option>
<option value="JEDI_CLIN_4">{{ "forms.task_order.clin_04_label" | translate }}</option> <option value="JEDI_CLIN_4">{{ "JEDICLINType.JEDI_CLIN_4" | translate }}</option>
</select> </select>
</fieldset> </fieldset>
</div> </div>

View File

@ -1,8 +1,8 @@
{% from "components/icon.html" import Icon %} {% from "components/icon.html" import Icon %}
{% macro Tooltip(message,title='Help') -%} {% macro Tooltip(message,title='Help', classes="") %}
<button type="button" tabindex="0" class="icon-tooltip" v-tooltip.top="{content: '{{message}}', container: false}"> <button type="button" tabindex="0" class="icon-tooltip {{classes}}" v-tooltip.top="{content: '{{message}}', container: false}">
{{ Icon('help') }}<span>{{ title }}</span> {{ Icon('help') }}<span>{{ title }}</span>
</button> </button>

View File

@ -6,6 +6,7 @@
{% block portfolio_content %} {% block portfolio_content %}
{{ StickyCTA("Reports") }} {{ StickyCTA("Reports") }}
<div class="portfolio-reports col col--grow"> <div class="portfolio-reports col col--grow">
<p class="row estimate-warning">{{ "portfolios.reports.estimate_warning" | translate }}</p>
{% include "portfolios/reports/portfolio_summary.html" %} {% include "portfolios/reports/portfolio_summary.html" %}
<hr> <hr>
{% include "portfolios/reports/obligated_funds.html" %} {% include "portfolios/reports/obligated_funds.html" %}

View File

@ -1,29 +1,42 @@
{% from "components/icon.html" import Icon %}
<section> <section>
<header> <header class="reporting-section-header">
<h2>Current Obligated funds</h2> <h2 class="reporting-section-header__header">Current Obligated funds</h2>
<span>As of DATE</span> <span class="reporting-section-header__subheader">As of {{ now | formattedDate(formatter="%B %d, %Y at %H:%M") }}</span>
</header> </header>
<div class='panel'> <div class='panel'>
<div class='panel__content'> <div class='panel__content jedi-clin-funding'>
<div>
{% for JEDI_clin, funds in current_obligated_funds.items() %} {% for JEDI_clin, funds in current_obligated_funds.items() %}
{{ JEDI_clin }} {% set remaining_funds = (funds["obligated_funds"] - funds["expended_funds"]) %}
<meter value='{{ funds["expended_funds"] }}' min='0' max='{{ funds["obligated_funds"] }}' title='{{ JEDI_clin }}'> <div class="jedi-clin-funding__clin-wrapper">
<div class='meter__fallback' style='width:{{ (funds["expended_funds"] / funds["obligated_funds"]) * 100 }}%;'></div> <h3 class="h5 jedi-clin-funding__header">
{{ "JEDICLINType.{}".format(JEDI_clin) | translate }}
</h3>
<p class="jedi-clin-funding__subheader">Total obligated amount: {{ funds["obligated_funds"] | dollars }}</p>
<meter class="jedi-clin-funding__meter" value='{{remaining_funds}}' min='0' max='{{ funds["obligated_funds"] }}' title='{{ JEDI_clin }}'>
<div class='jedi-clin-funding__meter-fallback' style='width:{{ (funds["expended_funds"] / funds["obligated_funds"]) * 100 }}%;'></div>
</meter> </meter>
<div> <div class="jedi-clin-funding__meter-values">
<p>Remaining funds:</p> <div class="jedi-clin-funding__meta">
<p>{{ (funds["obligated_funds"] - funds["expended_funds"]) | dollars }}</p> <p class="jedi-clin-funding__meta-header">Funds expended:</p>
<p class="h3 jedi-clin-funding__meta-value">{{ funds["expended_funds"] | dollars }}</p>
</div>
<div class="jedi-clin-funding__meta jedi-clin-funding__meta--remaining">
<p class="jedi-clin-funding__meta-header">Remaining funds:</p>
<p class="h3 jedi-clin-funding__meta-value">{{ remaining_funds | dollars }}</p>
</div>
</div> </div>
<div>
<p>Funds expended to date:</p>
<p>{{ funds["expended_funds"] | dollars }}</p>
</div> </div>
<hr>
{% endfor %} {% endfor %}
<div class="jedi-clin-funding__active-task-orders">
<h3 class="h4">
Active Task Orders
</h3>
{% for task_order in portfolio.active_task_orders %} {% for task_order in portfolio.active_task_orders %}
<a href="{{ url_for("task_orders.review_task_order", task_order_id=task_order.id) }}">{{ task_order.number }}</a> <a href="{{ url_for("task_orders.review_task_order", task_order_id=task_order.id) }}">
{{ task_order.number }} {{ Icon("caret_right", classes="icon--tiny icon--blue" ) }}
</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -3,34 +3,34 @@
<section class="row"> <section class="row">
<div class='col col--grow'> <div class='col col--grow reporting-summary-item'>
<p> <h5 class="reporting-summary-item__header">
Total Portfolio Value <span class="reporting-summary-item__header-text">Total Portfolio Value</span>
{{Tooltip(("common.lorem" | translate), title="")}} {{Tooltip(("common.lorem" | translate), title="", classes="reporting-summary-item__header-icon")}}
</p> </h5>
<p>{{ total_portfolio_value | dollars }}</p> <p class="reporting-summary-item__value">{{ total_portfolio_value | dollars }}</p>
</div> </div>
<div class='col col--grow'> <div class='col col--grow reporting-summary-item'>
<p> <h5 class="reporting-summary-item__header">
Funding Duration <span class="reporting-summary-item__header-text">Funding Duration</span>
{{Tooltip(("common.lorem" | translate), title="")}} {{Tooltip(("common.lorem" | translate), title="", classes="reporting-summary-item__header-icon")}}
</p> </h5>
{% set earliest_pop_start_date, latest_pop_end_date = portfolio.funding_duration %} {% set earliest_pop_start_date, latest_pop_end_date = portfolio.funding_duration %}
{% if earliest_pop_start_date and latest_pop_end_date %} {% if earliest_pop_start_date and latest_pop_end_date %}
<p> <p class="reporting-summary-item__value" >
{{ earliest_pop_start_date | formattedDate(formatter="%B %d, %Y") }} {{ earliest_pop_start_date | formattedDate(formatter="%B %d, %Y") }}
- -
{{ latest_pop_end_date | formattedDate(formatter="%B %d, %Y") }} {{ latest_pop_end_date | formattedDate(formatter="%B %d, %Y") }}
</p> </p>
{% else %} {% else %}
<p> - </p> <p class="reporting-summary-item__value"> - </p>
{% endif %} {% endif %}
</div> </div>
<div class='col col--grow'> <div class='col col--grow reporting-summary-item'>
<p> <h5 class="reporting-summary-item__header">
Days Remaining <span class="reporting-summary-item__header-text">Days Remaining</span>
{{Tooltip(("common.lorem" | translate), title="")}} {{Tooltip(("common.lorem" | translate), title="", classes="reporting-summary-item__header-icon")}}
</p> </h5>
<p>{{ portfolio.days_to_funding_expiration }} days</p> <p class="reporting-summary-item__value">{{ portfolio.days_to_funding_expiration }} days</p>
</div> </div>
</section> </section>

View File

@ -182,10 +182,6 @@ forms:
none: Not planning to migrate any applications none: Not planning to migrate any applications
not_sure: Not sure not_sure: Not sure
on_premise: 'Yes, migrating from an on-premise data center' on_premise: 'Yes, migrating from an on-premise data center'
clin_01_label: 'IaaS/PaaS (IDIQ CLIN 0001)'
clin_02_label: 'IDIQ CLIN 0002'
clin_03_label: 'IDIQ CLIN 0003'
clin_04_label: 'IDIQ CLIN 0004'
complexity: complexity:
conus: CONUS access conus: CONUS access
data_analytics: Data analytics data_analytics: Data analytics
@ -450,6 +446,7 @@ portfolios:
portfolio_mgmt: Portfolio management portfolio_mgmt: Portfolio management
reporting: Reporting reporting: Reporting
reports: reports:
estimate_warning: Reports displayed in JEDI are estimates and not a system of record.
empty_state: empty_state:
message: Nothing to report. message: Nothing to report.
sub_message: sub_message:
@ -519,10 +516,10 @@ task_orders:
sign: sign:
digital_signature_description: I acknowledge that the uploaded task order contains the required KO signature. digital_signature_description: I acknowledge that the uploaded task order contains the required KO signature.
JEDICLINType: JEDICLINType:
JEDI_CLIN_1: 'Unclassified IaaS and PaaS (IDIQ CLIN 0001)' JEDI_CLIN_1: 'IDIQ CLIN 0001 Unclassified IaaS/PaaS'
JEDI_CLIN_2: 'Classified IaaS and PaaS (IDIQ CLIN 0002)' JEDI_CLIN_2: 'IDIQ CLIN 0002 Classified IaaS/PaaS'
JEDI_CLIN_3: 'Unclassified Cloud Support Package (IDIQ CLIN 0003)' JEDI_CLIN_3: 'IDIQ CLIN 0003 Unclassified Cloud Support Package'
JEDI_CLIN_4: 'Classified Cloud Support Package (IDIQ CLIN 0004)' JEDI_CLIN_4: 'IDIQ CLIN 0004 Classified Cloud Support Package'
testing: testing:
example_string: Hello World example_string: Hello World
example_with_variables: 'Hello, {name}!' example_with_variables: 'Hello, {name}!'