commit
e327a0bada
@ -1,6 +1,6 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from flask import g, render_template, url_for
|
||||
from flask import g, render_template
|
||||
|
||||
from . import task_orders_bp
|
||||
from atst.domain.authz.decorator import user_can_access_decorator as user_can
|
||||
@ -30,22 +30,6 @@ def review_task_order(task_order_id):
|
||||
return render_template("portfolios/task_orders/review.html", task_order=task_order)
|
||||
|
||||
|
||||
def serialize_task_order(task_order):
|
||||
return {
|
||||
key: getattr(task_order, key)
|
||||
for key in [
|
||||
"id",
|
||||
"budget",
|
||||
"time_created",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"display_status",
|
||||
"days_to_expiration",
|
||||
"balance",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@task_orders_bp.route("/portfolios/<portfolio_id>/task_orders")
|
||||
@user_can(Permissions.VIEW_PORTFOLIO_FUNDING, message="view portfolio funding")
|
||||
def portfolio_funding(portfolio_id):
|
||||
@ -53,14 +37,9 @@ def portfolio_funding(portfolio_id):
|
||||
task_orders_by_status = defaultdict(list)
|
||||
|
||||
for task_order in portfolio.task_orders:
|
||||
serialized_task_order = serialize_task_order(task_order)
|
||||
serialized_task_order["url"] = url_for(
|
||||
"task_orders.view_task_order", task_order_id=task_order.id
|
||||
)
|
||||
task_orders_by_status[task_order.status].append(serialized_task_order)
|
||||
task_orders_by_status[task_order.status].append(task_order)
|
||||
|
||||
active_task_orders = task_orders_by_status.get(TaskOrderStatus.ACTIVE, [])
|
||||
total_balance = sum([task_order["balance"] for task_order in active_task_orders])
|
||||
|
||||
return render_template(
|
||||
"portfolios/task_orders/index.html",
|
||||
@ -70,5 +49,4 @@ def portfolio_funding(portfolio_id):
|
||||
),
|
||||
active_task_orders=active_task_orders,
|
||||
expired_task_orders=task_orders_by_status.get(TaskOrderStatus.EXPIRED, []),
|
||||
total_balance=total_balance,
|
||||
)
|
||||
|
@ -22,6 +22,7 @@
|
||||
@import "elements/kpi";
|
||||
@import "elements/graphs";
|
||||
@import "elements/menu";
|
||||
@import "elements/card";
|
||||
|
||||
@import "components/accordion_table";
|
||||
@import "components/topbar";
|
||||
|
@ -20,7 +20,7 @@
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
margin-bottom: $gap * 4;
|
||||
margin-bottom: $gap * 1;
|
||||
|
||||
.col--grow {
|
||||
overflow: inherit;
|
||||
@ -127,7 +127,7 @@
|
||||
}
|
||||
|
||||
.portfolio-content {
|
||||
margin: 6 * $gap $gap 0 $gap;
|
||||
margin: 1 * $gap $gap 0 $gap;
|
||||
|
||||
.panel {
|
||||
@include shadow-panel;
|
||||
@ -373,7 +373,7 @@
|
||||
}
|
||||
|
||||
.portfolio-funding {
|
||||
padding: 2 * $gap;
|
||||
padding: (2 * $gap) 0;
|
||||
|
||||
.panel {
|
||||
@include shadow-panel;
|
||||
|
@ -4,12 +4,12 @@
|
||||
}
|
||||
|
||||
@include media($medium-screen) {
|
||||
margin-left: -$gap * 7;
|
||||
margin-left: -$gap * 5;
|
||||
}
|
||||
}
|
||||
|
||||
.sticky-cta.js-is-sticky {
|
||||
width: 78.5%;
|
||||
width: 80.8%;
|
||||
}
|
||||
|
||||
.sticky-cta-container {
|
||||
|
@ -165,6 +165,7 @@ $checkbox-border-radius: 2px;
|
||||
$border-radius: 3px;
|
||||
$button-border-radius: 5px;
|
||||
$box-shadow: 0px 2px 5px 0px $color-shadow;
|
||||
$box-shadow-big: 0 4px 10px 0 rgba(193, 193, 193, 0.5);
|
||||
$focus-outline: 2px dotted $color-gray-light;
|
||||
$focus-spacing: 3px;
|
||||
$nav-width: 300px;
|
||||
|
34
styles/elements/_card.scss
Normal file
34
styles/elements/_card.scss
Normal file
@ -0,0 +1,34 @@
|
||||
.card {
|
||||
width: 100%;
|
||||
box-shadow: $box-shadow-big;
|
||||
padding: ($gap * 2) ($gap * 2.5) ($gap * 4) ($gap * 3);
|
||||
margin-bottom: 20px;
|
||||
|
||||
.card__status {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-around;
|
||||
|
||||
.card__status-spacer {
|
||||
flex-grow: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.card__header h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.card__body {
|
||||
font-size: $small-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
.label {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
font-size: $small-font-size;
|
||||
}
|
||||
}
|
@ -66,6 +66,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.task-order-list {
|
||||
margin-top: 6 * $gap;
|
||||
}
|
||||
|
||||
.task-order-card .label {
|
||||
font-size: $small-font-size;
|
||||
margin-right: 2 * $gap;
|
||||
}
|
||||
|
||||
.task-order-card__buttons .usa-button {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.task-order-summary {
|
||||
margin: $gap * 4;
|
||||
|
||||
|
@ -7,7 +7,6 @@
|
||||
{% block portfolio_header %}
|
||||
{% include "portfolios/header.html" %}
|
||||
{% endblock %}
|
||||
<div class='line'></div>
|
||||
<div class='portfolio-content'>
|
||||
{% block portfolio_content %}{% endblock %}
|
||||
</div>
|
||||
|
@ -6,121 +6,71 @@
|
||||
|
||||
{% block portfolio_content %}
|
||||
|
||||
{% macro ViewLink(task_order) %}
|
||||
<a href="{{ url_for('task_orders.view_task_order', task_order_id=task_order.id) }}" class="icon-link view-task-order-link">
|
||||
<span>View</span>
|
||||
{{ Icon("caret_right", classes="icon--tiny") }}
|
||||
{% macro ViewLink(task_order, text="Edit") %}
|
||||
<a href="{{ url_for('task_orders.view_task_order', task_order_id=task_order.id) }}" class="usa-button">
|
||||
{{ text }}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TaskOrderList(task_orders, label='success', expired=False, funded=False) %}
|
||||
<task-order-list
|
||||
inline-template
|
||||
v-bind:data='{{ task_orders | tojson }}'
|
||||
v-bind:expired='{{ 'true' if expired else 'false' }}'
|
||||
v-bind:funded='{{'true' if funded else 'false' }}'
|
||||
v-cloak
|
||||
>
|
||||
<div class='responsive-table-wrapper'>
|
||||
<table v-cloak class="atat-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in getColumns()" @click="updateSort(col.displayName)" :width="col.width" :class="col.class" scope="col">
|
||||
!{ col.displayName }
|
||||
<template v-if="col.sortFunc">
|
||||
<span class="sorting-direction" v-if="col.displayName === sortInfo.columnName && sortInfo.isAscending">
|
||||
{{ Icon("caret_down", classes="icon--tiny") }}
|
||||
</span>
|
||||
<span class="sorting-direction" v-if="col.displayName === sortInfo.columnName && !sortInfo.isAscending">
|
||||
{{ Icon("caret_up", classes="icon--tiny") }}
|
||||
</span>
|
||||
</template>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% macro TaskOrderDateTime(dt, className="") %}
|
||||
<local-datetime timestamp="{{ dt }}" format="MMMM D, YYYY" class="{{ className }}"></local-datetime>
|
||||
{% endmacro %}
|
||||
|
||||
<tbody>
|
||||
<tr v-for='taskOrder in taskOrders' :key="taskOrder.id">
|
||||
<td>
|
||||
<span class='label label--{{ label }}'>!{ taskOrder.display_status }</span>
|
||||
</td>
|
||||
<td>
|
||||
<span :class="{ 'to-performance-period': true, 'to-expiring-soon': (taskOrder.days_to_expiration > 0 && taskOrder.days_to_expiration <= days_to_exp_alert_limit), 'funded': funded && taskOrder.display_status === 'Active', 'unfunded': !funded && taskOrder.display_status === 'Active' }">
|
||||
<local-datetime
|
||||
v-bind:timestamp="taskOrder.start_date"
|
||||
format="M/D/YYYY">
|
||||
</local-datetime>
|
||||
-
|
||||
<local-datetime
|
||||
v-bind:timestamp="taskOrder.end_date"
|
||||
format="M/D/YYYY"
|
||||
class="to-end-date"
|
||||
>
|
||||
</local-datetime>
|
||||
<span
|
||||
v-if="taskOrder.days_to_expiration > 0 && taskOrder.days_to_expiration <= days_to_exp_alert_limit && funded"
|
||||
class="to-expiration-alert">
|
||||
{{ Icon('ok') }} Period ending in !{ taskOrder.days_to_expiration } days, but new period funded
|
||||
{% macro TaskOrderDate(task_order) %}
|
||||
<span class="datetime">
|
||||
{% if task_order.is_active %}
|
||||
Began {{ TaskOrderDateTime(task_order.start_date) }} | Ends {{ TaskOrderDateTime(task_order.end_date) }}
|
||||
{% elif task_order.is_expired %}
|
||||
Started {{ TaskOrderDateTime(task_order.start_date) }} | Ended {{ TaskOrderDateTime(task_order.end_date) }}
|
||||
{% else %}
|
||||
Started {{ TaskOrderDateTime(task_order.start_date) }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span
|
||||
v-if="taskOrder.days_to_expiration > 0 && taskOrder.days_to_expiration <= days_to_exp_alert_limit && !funded"
|
||||
class="to-expiration-alert">
|
||||
{{ Icon('alert') }} Period ends in !{ taskOrder.days_to_expiration } days, submit a new task order
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="table-cell--align-right">
|
||||
<span v-html='formatDollars(taskOrder.budget)'></span>
|
||||
</td>
|
||||
<td v-bind:class="{ 'table-cell--align-right': true, 'unused-balance': expired && taskOrder.balance > 0 }">
|
||||
<span v-html='formatDollars(taskOrder.balance)'></span>
|
||||
</td>
|
||||
<td>
|
||||
<a v-bind:href="taskOrder.url" class="icon-link view-task-order-link">
|
||||
<span>View</span>
|
||||
{{ Icon("caret_right", classes="icon--tiny") }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ caller and caller() }}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TaskOrderActions(task_order) %}
|
||||
<div class="task-order-card__buttons">
|
||||
{% if task_order.is_pending %}
|
||||
{{ ViewLink(task_order, text="Edit") }}
|
||||
{% elif task_order.is_active %}
|
||||
{{ ViewLink(task_order, text="Modify") }}
|
||||
{% else %}
|
||||
{{ ViewLink(task_order, text="View") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro TaskOrderList(task_orders, label='success') %}
|
||||
<div class="task-order-list">
|
||||
{% for task_order in task_orders %}
|
||||
<div class="card task-order-card">
|
||||
<div class="card__status">
|
||||
<span class='label label--{{ label }}'>{{ task_order.display_status }}</span>
|
||||
{{ TaskOrderDate(task_order) }}
|
||||
<span class="card__status-spacer"></span>
|
||||
<span class="card__button">
|
||||
{{ TaskOrderActions(task_order) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card__header">
|
||||
<h3>Task Order #{{ task_order.number }}</h3>
|
||||
</div>
|
||||
<div class="card__body">
|
||||
<b>Obligated amount: </b>${{ task_order.total_obligated_funds }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</task-order-list>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="portfolio-funding">
|
||||
|
||||
{% call StickyCTA(text="Funding") %}
|
||||
<div class='portfolio-funding__header row'>
|
||||
<a href="{{ url_for("task_orders.edit", portfolio_id=portfolio.id) }}" class="usa-button">Start a new task order</a>
|
||||
<a href="{{ url_for("task_orders.edit", portfolio_id=portfolio.id) }}" class="usa-button">Add a new task order</a>
|
||||
</div>
|
||||
{% endcall %}
|
||||
|
||||
{% for task_order in pending_task_orders %}
|
||||
<div class='subheading'>
|
||||
Pending
|
||||
</div>
|
||||
<div class='panel pending-task-order row'>
|
||||
<span class='label label--warning'>Pending</span>
|
||||
<div class="pending-task-order__started col">
|
||||
<dt>Started</dt>
|
||||
<dd>
|
||||
<local-datetime
|
||||
timestamp="{{ task_order.time_created }}"
|
||||
format="M/D/YYYY">
|
||||
</local-datetime>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pending-task-order__value col">
|
||||
<dt>Value</dt>
|
||||
<dd>{{ task_order.budget | dollars }}</dd>
|
||||
</div>
|
||||
{{ ViewLink(task_order) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{% if not active_task_orders and not pending_task_orders %}
|
||||
{{ EmptyState(
|
||||
'This portfolio doesn’t have any active or pending task orders.',
|
||||
@ -130,22 +80,16 @@
|
||||
) }}
|
||||
{% endif %}
|
||||
|
||||
{% if pending_task_orders %}
|
||||
{{ TaskOrderList(pending_task_orders, label='warning') }}
|
||||
{% endif %}
|
||||
|
||||
{% if active_task_orders %}
|
||||
<div class='subheading'>Active</div>
|
||||
{% call TaskOrderList(active_task_orders, label='success', funded=funded) %}
|
||||
<tr class='total-balance'>
|
||||
<td colspan='4'>
|
||||
<span class='label label--success'>Total Active Balance</span>
|
||||
<span>{{ total_balance | dollars }}</span>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
{% endcall %}
|
||||
{{ TaskOrderList(active_task_orders, label='success') }}
|
||||
{% endif %}
|
||||
|
||||
{% if expired_task_orders %}
|
||||
<div class='subheading'>Expired</div>
|
||||
{{ TaskOrderList(expired_task_orders, label='expired', expired=True) }}
|
||||
{{ TaskOrderList(expired_task_orders, label='error') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
@ -29,22 +29,6 @@ def user():
|
||||
|
||||
|
||||
class TestPortfolioFunding:
|
||||
def test_portfolio_with_no_task_orders(self, app, user_session, portfolio):
|
||||
user_session(portfolio.owner)
|
||||
|
||||
with captured_templates(app) as templates:
|
||||
response = app.test_client().get(
|
||||
url_for("task_orders.portfolio_funding", portfolio_id=portfolio.id)
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
_, context = templates[0]
|
||||
assert context["funding_end_date"] is None
|
||||
assert context["total_balance"] == 0
|
||||
assert context["pending_task_orders"] == []
|
||||
assert context["active_task_orders"] == []
|
||||
assert context["expired_task_orders"] == []
|
||||
|
||||
@pytest.mark.skip(reason="Update later when CLINs are implemented")
|
||||
def test_funded_portfolio(self, app, user_session, portfolio):
|
||||
user_session(portfolio.owner)
|
||||
|
Loading…
x
Reference in New Issue
Block a user