Skip to content

Commit

Permalink
Refactor and fix bugs in Recent Activity and Upcoming Closing Dates t…
Browse files Browse the repository at this point in the history
…ables.
  • Loading branch information
sanason committed Mar 6, 2024
1 parent 9856006 commit a9b6909
Show file tree
Hide file tree
Showing 16 changed files with 467 additions and 515 deletions.
1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"prettier": "2.6.1",
"sass": "1.52.1",
"sass-loader": "13.0.0",
"sinon": "^17.0.1",
"standard": "^16.0.3",
"typescript": "^4.7.2",
"vue-template-compiler": "^2.7.12",
Expand Down
151 changes: 151 additions & 0 deletions packages/client/src/components/ActivityTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<template>
<b-table
hover
:items="activityItems"
:fields="activityFields"
sort-by="dateSort"
sort-desc
class="table table-borderless"
thead-class="d-none"
selectable
select-mode="single"
@row-selected="onRowSelected"
@row-clicked="onRowClicked"
>
<template #cell(icon)="list">
<div class="gutter-icon row">
<b-icon v-if="list.item.interested === statuses.rejected" icon="x-circle-fill" scale="1" variant="danger"></b-icon>
<b-icon v-if="list.item.interested === statuses.interested" icon="check-circle-fill" scale="1" variant="success"></b-icon>
<b-icon v-if="list.item.interested === statuses.assigned" icon="arrow-right-circle-fill" scale="1"></b-icon>
<b-icon v-if="list.item.interested === statuses.awarded" icon="award-fill" scale="1" class="color-yellow"></b-icon>
<b-icon v-if="list.item.interested === statuses.applied" icon="check-circle-fill" scale="1" class="color-green"></b-icon>
<b-iconstack v-if="list.item.interested === statuses.lost">
<b-icon stacked icon="award-fill" scale="1" class="color-yellow"></b-icon>
<b-icon stacked icon="x-lg" scale="1.2"></b-icon>
</b-iconstack>
</div>
</template>
<template #cell(agencyAndGrant)="agencies">
<div>{{ agencies.item.agency }}
<span v-if="agencies.item.interested === statuses.rejected" class="color-red"> <strong> rejected </strong> </span>
<span v-if="agencies.item.interested === statuses.interested"> is
<span class="color-green">
<strong> interested </strong>
</span> in
</span>
<span v-if="agencies.item.interested === statuses.assigned"> was<strong> assigned </strong> </span>
<span v-if="agencies.item.interested === statuses.awarded"> was<strong><span
class="color-yellow"> awarded </span></strong> </span>
<span v-if="agencies.item.interested === statuses.applied"><strong><span
class="color-green"> applied </span></strong>for </span>
<span v-if="agencies.item.interested === statuses.lost"><strong><span
class="color-yellow"> lost </span></strong> </span>{{ agencies.item.grant }}
</div>
</template>
<template #cell(date)="dates">
<div class="color-gray">{{ dates.item.date }}</div>
</template>
</b-table>
</template>

<script>
export default {
props: {
grantsInterested: {
type: Array,
required: true,
},
onRowSelected: {
type: Function,
required: true,
},
onRowClicked: {
type: Function,
required: true,
},
},
data() {
return {
activityFields: [
{
// col for the check or X icon
key: 'icon',
label: '',
// thStyle: { width: '1%' },
},
{
// col for the agency is interested or not in grant
key: 'agencyAndGrant',
label: '',
// thStyle: { width: '79%' },
},
{
// col for when the event being displayed happened
key: 'date',
label: '',
// thStyle: { width: '20%' },
},
],
statuses: {
rejected: 'Rejected',
interested: 'Interested',
assigned: 'Assigned',
awarded: 'Awarded',
applied: 'Applied',
lost: 'Lost',
},
};
},
computed: {
activityItems() {
const rtf = new Intl.RelativeTimeFormat('en', {
numeric: 'auto',
});
const oneDayInMs = 1000 * 60 * 60 * 24;
return this.grantsInterested.map((grantsInterested) => ({
agency: grantsInterested.name,
grant: grantsInterested.title,
grant_id: grantsInterested.grant_id,
interested: (() => {
let retVal = null;
if (grantsInterested.status_code != null) {
if (grantsInterested.status_code === 'Rejected') {
retVal = this.statuses.rejected;
} else if ((grantsInterested.status_code === 'Interested')) {
retVal = this.statuses.interested;
} else if ((grantsInterested.status_code === 'Result')) {
if (grantsInterested.interested_name === 'Award') {
retVal = this.statuses.awarded;
} else if (grantsInterested.interested_name === 'Pending') {
retVal = this.statuses.applied;
} else if (grantsInterested.interested_name === 'Non-award') {
retVal = this.statuses.lost;
}
}
} else if (grantsInterested.assigned_by != null) {
retVal = this.statuses.assigned;
}
return retVal;
})(),
dateSort: new Date(grantsInterested.created_at),
date: (() => {
const timeSince = rtf.format(Math.round((new Date(grantsInterested.created_at).getTime() - new Date().getTime()) / oneDayInMs), 'day');
const timeSinceInt = parseInt(timeSince, 10);
if (!Number.isNaN(timeSinceInt) && timeSinceInt > 7) {
return new Date(grantsInterested.created_at).toLocaleDateString('en-US');
}
return timeSince.charAt(0).toUpperCase() + timeSince.slice(1);
})(),
}));
},
},
};
</script>

<style scoped>
.gutter-icon.row {
margin-right: -8px;
margin-left: -8px;
margin-top: 3px;
}
</style>
102 changes: 102 additions & 0 deletions packages/client/src/components/ClosingDatesTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<template>
<b-table
hover
:items="closestGrants"
:fields="upcomingFields"
sort-by="close_date"
class="table table-borderless"
thead-class="d-none"
selectable
select-mode="single"
@row-selected="onRowSelected"
@row-clicked="onRowClicked"
>
<template #cell(title)="data">
<div>{{ data.value }}</div>
<div class="color-gray">{{ data.item.interested_agencies.join(', ') }}</div>
</template>
<template #cell(close_date)="data">
<div :class="styleDate(data.value)">{{ formatDate(data.value) }}</div>
</template>
</b-table>
</template>

<script>
import { daysUntil } from '@/helpers/dates';
import { defaultCloseDateThresholds } from '@/helpers/constants';
export default {
props: {
closestGrants: {
type: Array,
required: true,
},
onRowSelected: {
type: Function,
required: true,
},
onRowClicked: {
type: Function,
required: true,
},
dangerThreshold: {
type: Number,
default: defaultCloseDateThresholds.danger,
},
warningThreshold: {
type: Number,
default: defaultCloseDateThresholds.warning,
},
},
data() {
return {
upcomingFields: [
{
// col for Grants and interested agencies
key: 'title',
label: '',
thStyle: { width: '80%' },
},
{
// col for when the grant will be closing
key: 'close_date',
label: '',
thStyle: { width: '20%' },
},
],
};
},
methods: {
styleDate(value) {
// value is an ISO string representing close date of grant
const daysUntilClose = daysUntil(value);
if (daysUntilClose <= this.dangerThreshold) {
return 'dangerDate';
}
if (daysUntilClose <= this.warningThreshold) {
return 'warnDate';
}
return 'normalDate';
},
formatDate(value) {
// value is an ISO string representing the close date of grant
// needs to be treated as local date
return new Date(value).toLocaleDateString('en-US', { timeZone: 'UTC' });
},
},
};
</script>

<style scoped>
.dangerDate {
color: #C22E31;
font-weight: bold;
}
.warnDate {
color: #956F0D;
font-weight: bold;
}
.normalDate {
color: black;
}
</style>
16 changes: 8 additions & 8 deletions packages/client/src/components/GrantsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@
import { mapActions, mapGetters } from 'vuex';
import { newTerminologyEnabled, newGrantsDetailPageEnabled } from '@/helpers/featureFlags';
import { datadogRum } from '@datadog/browser-rum';
import { titleize } from '../helpers/form-helpers';
import { titleize } from '@/helpers/form-helpers';
import { daysUntil } from '@/helpers/dates';
import { defaultCloseDateThresholds } from '@/helpers/constants';
import GrantDetailsLegacy from './Modals/GrantDetailsLegacy.vue';
import SearchPanel from './Modals/SearchPanel.vue';
import SavedSearchPanel from './Modals/SavedSearchPanel.vue';
Expand Down Expand Up @@ -278,10 +280,8 @@ export default {
return this.grantsPagination ? this.grantsPagination.lastPage : 0;
},
formattedGrants() {
const DAYS_TO_MILLISECS = 24 * 60 * 60 * 1000;
const warningThreshold = (this.agency.warning_threshold || 30) * DAYS_TO_MILLISECS;
const dangerThreshold = (this.agency.danger_threshold || 15) * DAYS_TO_MILLISECS;
const now = new Date();
const warningThreshold = this.agency.warning_threshold || defaultCloseDateThresholds.warning;
const dangerThreshold = this.agency.danger_threshold || defaultCloseDateThresholds.danger;
const generateTitle = (t) => {
const txt = document.createElement('textarea');
txt.innerHTML = t;
Expand All @@ -301,11 +301,11 @@ export default {
open_date: new Date(grant.open_date).toLocaleDateString('en-US', { timeZone: 'UTC' }),
close_date: new Date(grant.close_date).toLocaleDateString('en-US', { timeZone: 'UTC' }),
_cellVariants: (() => {
const diff = new Date(grant.close_date) - now;
if (diff <= dangerThreshold) {
const daysUntilClose = daysUntil(grant.close_date);
if (daysUntilClose <= dangerThreshold) {
return { close_date: 'danger' };
}
if (diff <= warningThreshold) {
if (daysUntilClose <= warningThreshold) {
return { close_date: 'warning' };
}
return {};
Expand Down
6 changes: 6 additions & 0 deletions packages/client/src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ const avatarColors = {
'#FCD663': '#000',
};

const defaultCloseDateThresholds = {
warning: 30,
danger: 15,
};

module.exports = {
billOptions,
avatarColors,
defaultCloseDateThresholds,
};
10 changes: 10 additions & 0 deletions packages/client/src/helpers/dates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DateTime } from 'luxon';

// Given a date (represented as a date-only ISO string), return the number of days until that date.
// Specifically, the number of whole days between now and the end of the given day in the local time-zone.
// Example: If today is 2021-01-01, then daysUntil('2021-01-02') returns 1.
// Returns zero if the date is in the past.
export function daysUntil(dateString) {
const daysDiff = DateTime.fromISO(dateString).endOf('day').diffNow('days').days;
return daysDiff < 0 ? 0 : Math.floor(daysDiff);
}
4 changes: 4 additions & 0 deletions packages/client/src/store/modules/grants.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,14 @@ export default {
return fetchApi.get(`/api/organizations/:organizationId/grants/next?${paginationQuery}&${orderingQuery}&${criteriaQuery}`)
.then((data) => commit('SET_GRANTS', data));
},
// Retrieves grants that the user's team (or any subteam) has interacted with (either by setting status or assigning to a user).
// Sorted in descending order by the date on which the interaction occurred (recently interacted with are first).
fetchGrantsInterested({ commit }, { perPage, currentPage }) {
return fetchApi.get(`/api/organizations/:organizationId/grants/grantsInterested/${perPage}/${currentPage}`)
.then((data) => commit('SET_GRANTS_INTERESTED', data));
},
// Retrieves grants that the user's team (or any subteam) is interested in and that have closing dates in the future.
// Sorted in ascending order by the closing date (grants that close soonest are first).
fetchClosestGrants({ commit }, { perPage, currentPage }) {
return fetchApi.get(`/api/organizations/:organizationId/grants/closestGrants/${perPage}/${currentPage}`)
.then((data) => commit('SET_CLOSEST_GRANTS', data));
Expand Down
Loading

0 comments on commit a9b6909

Please sign in to comment.