Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(grants): Forecasted UI - - MERGE AFTER PR # 3456 #3346

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions packages/client/src/components/GrantsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,29 @@ export default {
txt.innerHTML = t;
return txt.value;
};
const generateCloseDate = (date, status, closeDateExplanation) => {
const formattedDate = new Date(date).toLocaleDateString('en-US', { timeZone: 'UTC' });
const dateExists = date && date !== '2100-01-01';
if (!dateExists) {
return closeDateExplanation ? 'See details' : 'Not yet issued';
}
if (status === 'forecasted') {
return `est. ${formattedDate}`;
}

return formattedDate;
};
const generateOpenDate = (date, status) => {
const formattedDate = new Date(date).toLocaleDateString('en-US', { timeZone: 'UTC' });
const dateExists = date && date !== '2100-01-01';
if (!dateExists) {
return 'Not yet issued';
}
if (status === 'forecasted') {
return `est. ${formattedDate}`;
}
return formattedDate;
};
return this.grants.map((grant) => ({
...grant,
title: generateTitle(grant.title),
Expand All @@ -288,10 +311,10 @@ export default {
viewed_by: grant.viewed_by_agencies
.map((v) => v.agency_abbreviation)
.join(', '),
status: grant.opportunity_status,
status: titleize(grant.opportunity_status),
award_ceiling: grant.award_ceiling,
open_date: new Date(grant.open_date).toLocaleDateString('en-US', { timeZone: 'UTC' }),
close_date: new Date(grant.close_date).toLocaleDateString('en-US', { timeZone: 'UTC' }),
open_date: generateOpenDate(grant.open_date, grant.opportunity_status?.toLowerCase()),
close_date: generateCloseDate(grant.close_date, grant.opportunity_status?.toLowerCase(), grant.close_date_explanation),
_cellVariants: (() => {
const daysUntilClose = daysUntil(grant.close_date);
if (daysUntilClose <= dangerThreshold) {
Expand Down
3 changes: 2 additions & 1 deletion packages/client/src/components/Modals/SearchPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ const defaultCriteria = {
includeKeywords: null,
excludeKeywords: null,
opportunityNumber: null,
opportunityStatuses: ['posted'],
opportunityStatuses: ['forecasted', 'posted'],
fundingTypes: null,
agency: null,
bill: null,
Expand Down Expand Up @@ -310,6 +310,7 @@ export default {
{ code: 'O', name: 'Other' },
],
opportunityStatusOptions: [
{ text: 'Forecasted', value: 'forecasted' },
{ text: 'Posted', value: 'posted' },
// b-form-checkbox-group doesn't handle multiple values well 'archived' is added
// whenever 'closed' is checked, but as post processing step. See apply()
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/store/modules/grants.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ function buildGrantsNextQuery({ filters, ordering, pagination }) {
criteria.fundingActivityCategories = criteria.fundingActivityCategories?.map((c) => c.code);

if (!criteria.opportunityStatuses || criteria.opportunityStatuses.length === 0) {
// by default, only show posted opportunities
criteria.opportunityStatuses = ['posted'];
// by default, only show forecasted and posted opportunities
criteria.opportunityStatuses = ['forecasted', 'posted'];
}
const paginationQuery = Object.entries(pagination)
// filter out undefined and nulls since api expects parameters not present as undefined
Expand Down
33 changes: 28 additions & 5 deletions packages/client/src/views/GrantDetailsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@
hover
>
<template #cell()="data">
<span :class="{ 'text-muted font-weight-normal': data.item.displayMuted }">
<i v-if="data.item.displayEstimatedText">est.&nbsp;</i>
<span :class="{'text-muted font-weight-normal': data.item.displayMuted}">
{{ data.value }}
</span>
</template>
Expand Down Expand Up @@ -223,7 +224,8 @@ import GrantActivity from '@/components/GrantActivity.vue';

const HEADER = '__HEADER__';
const FAR_FUTURE_CLOSE_DATE = '2100-01-01';
const NOT_AVAILABLE_TEXT = 'Not available';
const NOT_YET_ISSUED_TEXT = 'Not yet issued';
const FORECASTED = 'forecasted';

export default {
components: {
Expand Down Expand Up @@ -282,11 +284,13 @@ export default {
value: this.currentGrant.grant_number,
}, {
name: 'Open Date',
value: this.formatDate(this.currentGrant.open_date),
value: this.openDateDisplay,
displayEstimatedText: this.displayEstimatedOpenDateText,
}, {
name: 'Close Date',
value: this.closeDateDisplay,
displayMuted: this.closeDateDisplayMuted,
displayEstimatedText: this.displayEstimatedCloseDateText,
}, {
name: 'Grant ID',
value: this.currentGrant.grant_id,
Expand Down Expand Up @@ -314,10 +318,23 @@ export default {
},
];
},
openDateDisplay() {
if (!this.currentGrant.open_date || this.currentGrant.open_date === FAR_FUTURE_CLOSE_DATE) {
return NOT_YET_ISSUED_TEXT;
}
if (this.currentGrant.opportunity_status === FORECASTED) {
// check for date validity here and in closeDateDisplay
return `${this.formatDate(this.currentGrant.open_date)}`;
}
return this.formatDate(this.currentGrant.open_date);
},
closeDateDisplay() {
// If we have an explainer text instead of a real close date, display that instead
if (this.currentGrant.close_date === FAR_FUTURE_CLOSE_DATE) {
return this.currentGrant.close_date_explanation ?? NOT_AVAILABLE_TEXT;
if (!this.currentGrant.close_date || this.currentGrant.close_date === FAR_FUTURE_CLOSE_DATE) {
return this.currentGrant.close_date_explanation ?? NOT_YET_ISSUED_TEXT;
}
if (this.currentGrant.opportunity_status === FORECASTED) {
return `${this.formatDate(this.currentGrant.close_date)}`;
}
return this.formatDate(this.currentGrant.close_date);
},
Expand Down Expand Up @@ -350,6 +367,12 @@ export default {
statusSubmitButtonDisabled() {
return this.selectedInterestedCode === null;
},
displayEstimatedCloseDateText() {
return this.currentGrant.close_date && this.currentGrant.opportunity_status === 'forecasted';
},
displayEstimatedOpenDateText() {
return this.currentGrant.open_date && this.currentGrant.opportunity_status === 'forecasted';
},
Copy link
Contributor

@lsr-explore lsr-explore Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewer: We do not want to display prefix "est." if the dates are null and the status is forecasted.

valid date - "est. 1/2/2056"
date = null - "not yet issued"

We need this as a separate flag because the requirement is to display "est." in italics.

},
watch: {
async currentGrant() {
Expand Down
4 changes: 4 additions & 0 deletions packages/server/src/db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ function grantsQuery(queryBuilder, filters, agencyId, orderingParams, pagination
CASE
WHEN grants.archive_date <= now() THEN 'archived'
WHEN grants.close_date <= now() THEN 'closed'
WHEN grants.open_date > now() OR grants.opportunity_status = 'forecasted' THEN 'forecasted'
ELSE 'posted'
END IN (${Array(filters.opportunityStatuses.length).fill('?').join(',')})`, filters.opportunityStatuses);
}
Expand Down Expand Up @@ -730,6 +731,7 @@ async function getGrantsNew(filters, paginationParams, orderingParams, tenantId,
'grants.cfda_list',
'grants.open_date',
'grants.close_date',
'grants.close_date_explanation',
'grants.archive_date',
'grants.reviewer_name',
'grants.opportunity_category',
Expand All @@ -746,11 +748,13 @@ async function getGrantsNew(filters, paginationParams, orderingParams, tenantId,
'grants.funding_instrument_codes',
'grants.bill',
'grants.funding_activity_category_codes',
'grants.opportunity_status',
])
.select(knex.raw(`
CASE
WHEN grants.archive_date <= now() THEN 'archived'
WHEN grants.close_date <= now() THEN 'closed'
WHEN grants.open_date > now() OR grants.opportunity_status = 'forecasted' THEN 'forecasted'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewer: Additional check for open_date in case the status is not correctly populated in grants.gov

ELSE 'posted'
END as opportunity_status
`))
Expand Down
37 changes: 24 additions & 13 deletions packages/server/src/routes/grants.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ router.get('/', requireUser, async (req, res) => {
});

function criteriaToFiltersObj(criteria, agencyId) {
// this function makes request to populate grants table
const filters = criteria || {};
const postedWithinOptions = {
'All Time': 0, 'One Week': 7, '30 Days': 30, '60 Days': 60,
Expand All @@ -71,6 +72,7 @@ function criteriaToFiltersObj(criteria, agencyId) {
}

router.get('/next', requireUser, async (req, res) => {
// api call to populate table
const { user } = req.session;

let orderingParams;
Expand Down Expand Up @@ -131,20 +133,29 @@ router.get('/exportCSVNew', requireUser, async (req, res) => {
);

// Generate CSV
const formattedData = data.map((grant) => ({
...grant,
funding_activity_categories: grant.funding_activity_categories.join('|'),
interested_agencies: grant.interested_agencies
.map((v) => v.agency_abbreviation)
.join(', '),
viewed_by: grant.viewed_by_agencies
.map((v) => v.agency_abbreviation)
.join(', '),
open_date: new Date(grant.open_date).toLocaleDateString('en-US', { timeZone: 'UTC' }),
close_date: new Date(grant.close_date).toLocaleDateString('en-US', { timeZone: 'UTC' }),
url: `https://www.grants.gov/search-results-detail/${grant.grant_id}`,
}));
const formattedData = data.map((grant) => {
let openDate = new Date(grant.open_date).toLocaleDateString('en-US', { timeZone: 'UTC' });
let closeDate = new Date(grant.close_date).toLocaleDateString('en-US', { timeZone: 'UTC' });

if (grant.opportunity_status === 'forecasted') {
openDate = grant.open_date ? `est. ${openDate}` : 'not yet issued';
closeDate = grant.close_date ? `est ${closeDate}` : grant.close_date_explanation || 'not yet issued';
}

return ({
...grant,
funding_activity_categories: grant.funding_activity_categories.join('|'),
interested_agencies: grant.interested_agencies
.map((v) => v.agency_abbreviation)
.join(', '),
viewed_by: grant.viewed_by_agencies
.map((v) => v.agency_abbreviation)
.join(', '),
open_date: openDate,
close_date: closeDate,
url: `https://www.grants.gov/search-results-detail/${grant.grant_id}`,
});
});
if (data.length === 0) {
// If there are 0 rows, csv-stringify won't even emit the header, resulting in a totally
// empty file, which is confusing. This adds a single empty row below the header.
Expand Down
Loading