From d06cadba3d5200764f46764b7716b71ebed845a8 Mon Sep 17 00:00:00 2001 From: mari <2312138+masimons@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:42:03 -0300 Subject: [PATCH 1/6] feat(forecasted grants): update grants table and details for forecasted grants --- .../client/src/components/GrantsTable.vue | 25 ++++++++++++++++--- .../src/components/Modals/SearchPanel.vue | 3 ++- packages/client/src/store/modules/grants.js | 4 +-- .../client/src/views/GrantDetailsView.vue | 16 +++++++++--- packages/server/src/db/index.js | 5 ++++ packages/server/src/routes/grants.js | 2 ++ 6 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/client/src/components/GrantsTable.vue b/packages/client/src/components/GrantsTable.vue index cd7bd6ba7..65792fd24 100644 --- a/packages/client/src/components/GrantsTable.vue +++ b/packages/client/src/components/GrantsTable.vue @@ -279,6 +279,25 @@ 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' }); + if (status === 'forecasted') { + return `est. ${formattedDate}`; + } + return formattedDate; + }; return this.grants.map((grant) => ({ ...grant, title: generateTitle(grant.title), @@ -288,10 +307,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) { diff --git a/packages/client/src/components/Modals/SearchPanel.vue b/packages/client/src/components/Modals/SearchPanel.vue index e993b7eb6..46c0ebd4a 100644 --- a/packages/client/src/components/Modals/SearchPanel.vue +++ b/packages/client/src/components/Modals/SearchPanel.vue @@ -269,7 +269,7 @@ const defaultCriteria = { includeKeywords: null, excludeKeywords: null, opportunityNumber: null, - opportunityStatuses: ['posted'], + opportunityStatuses: ['forecasted', 'posted'], fundingTypes: null, agency: null, bill: null, @@ -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() diff --git a/packages/client/src/store/modules/grants.js b/packages/client/src/store/modules/grants.js index f9042424c..45a890a84 100644 --- a/packages/client/src/store/modules/grants.js +++ b/packages/client/src/store/modules/grants.js @@ -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 diff --git a/packages/client/src/views/GrantDetailsView.vue b/packages/client/src/views/GrantDetailsView.vue index c15317130..51e6aa248 100644 --- a/packages/client/src/views/GrantDetailsView.vue +++ b/packages/client/src/views/GrantDetailsView.vue @@ -223,7 +223,7 @@ 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_AVAILABLE_TEXT = 'Not Available'; export default { components: { @@ -282,7 +282,7 @@ export default { value: this.currentGrant.grant_number, }, { name: 'Open Date', - value: this.formatDate(this.currentGrant.open_date), + value: this.openDateDisplay, }, { name: 'Close Date', value: this.closeDateDisplay, @@ -314,10 +314,20 @@ export default { }, ]; }, + openDateDisplay() { + // make 'forecasted' a constant + if (this.currentGrant.opportunity_status === 'forecasted') { + // check for date validity here and in closeDateDisplay + return `est. ${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) { + if (!this.currentGrant.close_date || this.currentGrant.close_date === FAR_FUTURE_CLOSE_DATE) { return this.currentGrant.close_date_explanation ?? NOT_AVAILABLE_TEXT; + } if (this.currentGrant.opportunity_status === 'forecasted') { + return `est. ${this.formatDate(this.currentGrant.close_date)}`; } return this.formatDate(this.currentGrant.close_date); }, diff --git a/packages/server/src/db/index.js b/packages/server/src/db/index.js index b0cff8338..5cc77f8bb 100755 --- a/packages/server/src/db/index.js +++ b/packages/server/src/db/index.js @@ -714,6 +714,9 @@ function addCsvData(qb) { agencyId: number */ async function getGrantsNew(filters, paginationParams, orderingParams, tenantId, agencyId, toCsv) { + // get grants for grants table + console.log(JSON.stringify([filters, paginationParams, orderingParams, tenantId, agencyId, toCsv])); + const errors = validateSearchFilters(filters); if (errors.length > 0) { throw new Error(`Invalid filters: ${errors.join(', ')}`); @@ -730,6 +733,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', @@ -751,6 +755,7 @@ async function getGrantsNew(filters, paginationParams, orderingParams, tenantId, CASE WHEN grants.archive_date <= now() THEN 'archived' WHEN grants.close_date <= now() THEN 'closed' + WHEN grants.open_date > now() THEN 'forecasted' ELSE 'posted' END as opportunity_status `)) diff --git a/packages/server/src/routes/grants.js b/packages/server/src/routes/grants.js index d7577e00e..05d3431d6 100755 --- a/packages/server/src/routes/grants.js +++ b/packages/server/src/routes/grants.js @@ -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, @@ -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; From e1b6b7e120ef20f7c15fd76453a1a57a3311d07e Mon Sep 17 00:00:00 2001 From: Laurie Reynolds Date: Wed, 16 Oct 2024 16:20:59 -0700 Subject: [PATCH 2/6] feat(grants): formatting fixes for dates --- .../client/src/components/GrantsTable.vue | 4 +++ .../client/src/views/GrantDetailsView.vue | 29 ++++++++++----- packages/server/src/routes/grants.js | 35 ++++++++++++------- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/packages/client/src/components/GrantsTable.vue b/packages/client/src/components/GrantsTable.vue index 65792fd24..9ed79cfd0 100644 --- a/packages/client/src/components/GrantsTable.vue +++ b/packages/client/src/components/GrantsTable.vue @@ -293,6 +293,10 @@ export default { }; 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}`; } diff --git a/packages/client/src/views/GrantDetailsView.vue b/packages/client/src/views/GrantDetailsView.vue index 51e6aa248..d4108bb1f 100644 --- a/packages/client/src/views/GrantDetailsView.vue +++ b/packages/client/src/views/GrantDetailsView.vue @@ -103,7 +103,8 @@ hover > @@ -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: { @@ -283,10 +285,12 @@ export default { }, { name: '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, @@ -315,19 +319,22 @@ export default { ]; }, openDateDisplay() { - // make 'forecasted' a constant - if (this.currentGrant.opportunity_status === 'forecasted') { + 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 `est. ${this.formatDate(this.currentGrant.open_date)}`; + 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 || this.currentGrant.close_date === FAR_FUTURE_CLOSE_DATE) { - return this.currentGrant.close_date_explanation ?? NOT_AVAILABLE_TEXT; - } if (this.currentGrant.opportunity_status === 'forecasted') { - return `est. ${this.formatDate(this.currentGrant.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); }, @@ -360,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'; + }, }, watch: { async currentGrant() { diff --git a/packages/server/src/routes/grants.js b/packages/server/src/routes/grants.js index 05d3431d6..7836c7b8d 100755 --- a/packages/server/src/routes/grants.js +++ b/packages/server/src/routes/grants.js @@ -133,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. From f565549704e72f208c78db0da91fa8e21f946297 Mon Sep 17 00:00:00 2001 From: Laurie Reynolds Date: Mon, 21 Oct 2024 14:23:27 -0700 Subject: [PATCH 3/6] feat(forecasted grants): return correct opportunity status --- packages/server/src/db/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/src/db/index.js b/packages/server/src/db/index.js index 5cc77f8bb..b3dddf9c4 100755 --- a/packages/server/src/db/index.js +++ b/packages/server/src/db/index.js @@ -750,12 +750,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() THEN 'forecasted' + WHEN grants.open_date > now() OR grants.opportunity_status = 'forecasted' THEN 'forecasted' ELSE 'posted' END as opportunity_status `)) From 17257bb33f7a10599dc891519088cd59efe39b04 Mon Sep 17 00:00:00 2001 From: Laurie Reynolds Date: Tue, 22 Oct 2024 10:55:35 -0700 Subject: [PATCH 4/6] feat(grants): fix search for forecasted grants --- packages/server/src/db/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/src/db/index.js b/packages/server/src/db/index.js index b3dddf9c4..b2dea43a9 100755 --- a/packages/server/src/db/index.js +++ b/packages/server/src/db/index.js @@ -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.opportunity_status = 'forecasted' THEN 'forecasted' ELSE 'posted' END IN (${Array(filters.opportunityStatuses.length).fill('?').join(',')})`, filters.opportunityStatuses); } From 036b035265302f1bf9882c1b9b9c03af2eb63c09 Mon Sep 17 00:00:00 2001 From: Laurie Reynolds Date: Tue, 22 Oct 2024 16:35:04 -0700 Subject: [PATCH 5/6] feat(forecasted grants): remove console.log --- packages/server/src/db/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/server/src/db/index.js b/packages/server/src/db/index.js index b2dea43a9..c237dfa62 100755 --- a/packages/server/src/db/index.js +++ b/packages/server/src/db/index.js @@ -715,9 +715,6 @@ function addCsvData(qb) { agencyId: number */ async function getGrantsNew(filters, paginationParams, orderingParams, tenantId, agencyId, toCsv) { - // get grants for grants table - console.log(JSON.stringify([filters, paginationParams, orderingParams, tenantId, agencyId, toCsv])); - const errors = validateSearchFilters(filters); if (errors.length > 0) { throw new Error(`Invalid filters: ${errors.join(', ')}`); From 9823c85a3593d7f1b2c2070e14620ab44bdb9041 Mon Sep 17 00:00:00 2001 From: Laurie Reynolds Date: Tue, 22 Oct 2024 23:17:49 -0700 Subject: [PATCH 6/6] feat(forecasted grants): update filter for opportunityStatuses --- packages/server/src/db/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/db/index.js b/packages/server/src/db/index.js index c237dfa62..29a105c5c 100755 --- a/packages/server/src/db/index.js +++ b/packages/server/src/db/index.js @@ -548,7 +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.opportunity_status = 'forecasted' THEN 'forecasted' + WHEN grants.open_date > now() OR grants.opportunity_status = 'forecasted' THEN 'forecasted' ELSE 'posted' END IN (${Array(filters.opportunityStatuses.length).fill('?').join(',')})`, filters.opportunityStatuses); }