Skip to content
Draft
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
6 changes: 3 additions & 3 deletions frontend/src/pages/RecipientRecord/pages/TTAHistory.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import useSessionFiltersAndReflectInUrl from '../../../hooks/useSessionFiltersAn
import { getUserRegions } from '../../../permissions';
import UserContext from '../../../UserContext';
import { expandFilters, formatDateRange } from '../../../utils';
import Overview from '../../../widgets/DashboardOverview';
import { TTAHistoryOverview } from '../../../widgets/DashboardOverview';
import FrequencyGraph from '../../../widgets/FrequencyGraph';
import TargetPopulationsTable from '../../../widgets/TargetPopulationsTable';
import { TTAHISTORY_FILTER_CONFIG } from './constants';
Expand Down Expand Up @@ -90,8 +90,8 @@ export default function TTAHistory({ recipientName, recipientId, regionId }) {
allUserRegions={regions}
/>
</div>
<Overview
fields={['Activity reports', 'Hours of TTA', 'Participants', 'In person activities']}
<TTAHistoryOverview
fields={['Activity reports', 'Training report sessions', 'Hours of TTA', 'Participants', 'In person activities']}
showTooltips
filters={filtersToApply}
/>
Expand Down
13 changes: 10 additions & 3 deletions frontend/src/pages/RecipientRecord/pages/__tests__/TTAHistory.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('Recipient Record - TTA History', () => {
inPerson: '0',
sumDuration: '1.0',
numParticipants: '1',
numSessions: '3',
};

const tableResponse = {
Expand Down Expand Up @@ -53,10 +54,10 @@ describe('Recipient Record - TTA History', () => {
};

beforeEach(async () => {
const overviewUrl = `/api/widgets/overview?startDate.win=${yearToDate}&region.in[]=1&recipientId.ctn[]=401`;
const ttaHistoryOverviewUrl = `/api/widgets/ttaHistoryOverview?startDate.win=${yearToDate}&region.in[]=1&recipientId.ctn[]=401`;
const tableUrl = `/api/activity-reports?sortBy=updatedAt&sortDir=desc&offset=0&limit=10&startDate.win=${yearToDate}&region.in[]=1&recipientId.ctn[]=401`;

fetchMock.get(overviewUrl, overviewResponse);
fetchMock.get(ttaHistoryOverviewUrl, overviewResponse);
fetchMock.get(tableUrl, tableResponse);

fetchMock.get(
Expand All @@ -79,6 +80,12 @@ describe('Recipient Record - TTA History', () => {
expect(overview).toBeTruthy();
});

it('renders the TR sessions widget', async () => {
act(() => renderTTAHistory());
const trSessionsLabel = await screen.findByText('Training report sessions');
expect(trSessionsLabel).toBeTruthy();
});

it('renders the activity reports table', async () => {
renderTTAHistory();
const reports = await screen.findByText(/approved activity reports/i, { selector: 'h2' });
Expand Down Expand Up @@ -106,7 +113,7 @@ describe('Recipient Record - TTA History', () => {
200
);
fetchMock.get(
'/api/widgets/overview?role.in[]=Family%20Engagement%20Specialist&role.in[]=Grantee%20Specialist&region.in[]=1&recipientId.ctn[]=401',
'/api/widgets/ttaHistoryOverview?role.in[]=Family%20Engagement%20Specialist&role.in[]=Grantee%20Specialist&region.in[]=1&recipientId.ctn[]=401',
overviewResponse
);

Expand Down
14 changes: 14 additions & 0 deletions frontend/src/widgets/DashboardOverview.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ const getDashboardFields = (data, showTooltip) => ({
label1: 'Activity reports',
data: data.numReports,
},
'Training report sessions': {
key: 'training-report-sessions',
showTooltip,
tooltipText: 'The number of approved Training Report sessions.',
icon: faChartColumn,
iconColor: colors.success,
backgroundColor: colors.successLighter,
label1: 'Training report sessions',
data: data.numSessions,
},
'Training reports': {
key: 'training-reports',
showTooltip,
Expand Down Expand Up @@ -230,6 +240,7 @@ DashboardOverviewWidget.propTypes = {
recipientPercentage: PropTypes.string,
totalRecipients: PropTypes.string,
numRecipients: PropTypes.string,
numSessions: PropTypes.string,
percentCompliantFollowUpReviewsWithTtaSupport: PropTypes.string,
totalCompliantFollowUpReviewsWithTtaSupport: PropTypes.string,
totalCompliantFollowUpReviews: PropTypes.string,
Expand All @@ -256,6 +267,7 @@ DashboardOverviewWidget.defaultProps = {
totalRecipients: '0',
recipientPercentage: '0%',
numRecipients: '0',
numSessions: '0',
percentCompliantFollowUpReviewsWithTtaSupport: '0%',
totalCompliantFollowUpReviewsWithTtaSupport: '0',
totalCompliantFollowUpReviews: '0',
Expand All @@ -278,4 +290,6 @@ DashboardOverviewWidget.defaultProps = {
maxToolTipWidth: null,
};

export const TTAHistoryOverview = withWidgetData(DashboardOverviewWidget, 'ttaHistoryOverview');

export default withWidgetData(DashboardOverviewWidget, 'overview');
4 changes: 4 additions & 0 deletions src/widgets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import totalHrsAndRecipientGraph from './totalHrsAndRecipientGraph';
import trHoursOfTrainingByNationalCenter from './trHoursOfTrainingByNationalCenter';
import trOverview from './trOverview';
import trSessionsByTopic from './trSessionsByTopic';
import trSessionsForRecipient from './trSessionsForRecipient';
import trStandardGoalList from './trStandardGoalList';
import ttaHistoryOverview from './ttaHistoryOverview';

/*
All widgets need to be added to this object
Expand All @@ -38,6 +40,8 @@ export default {
trOverview,
trStandardGoalList,
trSessionsByTopic,
trSessionsForRecipient,
ttaHistoryOverview,
trHoursOfTrainingByNationalCenter,
approvalRateByDeadline,

Expand Down
225 changes: 225 additions & 0 deletions src/widgets/trSessionsForRecipient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import { TRAINING_REPORT_STATUSES } from '@ttahub/common';
import db from '../models';
import {
createGrant,
createRecipient,
createSessionReport,
createTrainingReport,
createUser,
} from '../testUtils';
import trSessionsForRecipient from './trSessionsForRecipient';

const { EventReportPilot, Grant, Recipient, SessionReportPilot, User } = db;

// We need to mock this so that we don't try to send emails or otherwise engage the queue
jest.mock('bull');

describe('TR sessions for recipient widget', () => {
let userCreator;

let recipient1;
let recipient2;

let grant1;
let grant2;

let trainingReport1;
let trainingReport2;

beforeAll(async () => {
userCreator = await createUser();

// recipient 1 - the target recipient for counting
recipient1 = await createRecipient();
// recipient 2 - other recipient; sessions on their grants should not count for recipient 1
recipient2 = await createRecipient();

// grant 1 belongs to recipient 1
grant1 = await createGrant({ recipientId: recipient1.id, regionId: userCreator.homeRegionId });
// grant 2 belongs to recipient 2
grant2 = await createGrant({ recipientId: recipient2.id, regionId: userCreator.homeRegionId });

// --- Training report 1: sessions for recipient1 ---
trainingReport1 = await createTrainingReport({
collaboratorIds: [],
pocIds: [],
ownerId: userCreator.id,
});

// Session 1: has recipient1's grant, COMPLETE -> counts for recipient1
await createSessionReport({
eventId: trainingReport1.id,
data: {
deliveryMethod: 'in-person',
duration: 1,
recipients: [{ value: grant1.id }],
numberOfParticipantsVirtually: 0,
numberOfParticipantsInPerson: 0,
numberOfParticipants: 10,
status: TRAINING_REPORT_STATUSES.COMPLETE,
},
});

// Session 2: has recipient1's grant, COMPLETE -> counts for recipient1
await createSessionReport({
eventId: trainingReport1.id,
data: {
deliveryMethod: 'in-person',
duration: 1,
recipients: [{ value: grant1.id }],
numberOfParticipantsVirtually: 0,
numberOfParticipantsInPerson: 0,
numberOfParticipants: 10,
status: TRAINING_REPORT_STATUSES.COMPLETE,
},
});

// Session 3: has recipient1's grant, IN_PROGRESS -> excluded by baseTRScopes (not COMPLETE)
await createSessionReport({
eventId: trainingReport1.id,
data: {
deliveryMethod: 'in-person',
duration: 1,
recipients: [{ value: grant1.id }],
numberOfParticipantsVirtually: 0,
numberOfParticipantsInPerson: 0,
numberOfParticipants: 10,
status: TRAINING_REPORT_STATUSES.IN_PROGRESS,
},
});

await trainingReport1.update({
data: {
...trainingReport1.data,
status: TRAINING_REPORT_STATUSES.COMPLETE,
},
});

// --- Training report 2: session only for recipient2 ---
trainingReport2 = await createTrainingReport({
collaboratorIds: [],
pocIds: [],
ownerId: userCreator.id,
});

// Session 4: only has recipient2's grant, COMPLETE -> counts for recipient2, NOT recipient1
await createSessionReport({
eventId: trainingReport2.id,
data: {
deliveryMethod: 'in-person',
duration: 1,
recipients: [{ value: grant2.id }],
numberOfParticipantsVirtually: 0,
numberOfParticipantsInPerson: 0,
numberOfParticipants: 10,
status: TRAINING_REPORT_STATUSES.COMPLETE,
},
});

await trainingReport2.update({
data: {
...trainingReport2.data,
status: TRAINING_REPORT_STATUSES.COMPLETE,
},
});
});

afterAll(async () => {
await SessionReportPilot.destroy({
where: {
eventId: [trainingReport1.id, trainingReport2.id],
},
});

await EventReportPilot.destroy({
where: {
id: [trainingReport1.id, trainingReport2.id],
},
});

await db.GrantNumberLink.destroy({
where: {
grantId: [grant1.id, grant2.id],
},
force: true,
});

await Grant.destroy({
where: {
id: [grant1.id, grant2.id],
},
individualHooks: true,
});

await Recipient.destroy({
where: {
id: [recipient1.id, recipient2.id],
},
});

await User.destroy({
where: {
id: [userCreator.id],
},
});

await db.sequelize.close();
});

it('counts only COMPLETE sessions where the recipient has a matching grant', async () => {
// Scope restricted to our test data only
const scopes = {
grant: {
where: [{ id: [grant1.id, grant2.id] }],
},
trainingReport: [{ id: [trainingReport1.id, trainingReport2.id] }],
} as any;

const data = await trSessionsForRecipient(scopes, {
'recipientId.ctn': [String(recipient1.id)],
});

// session1 + session2 = 2 (session3 is IN_PROGRESS so excluded; session4 is for recipient2)
expect(data.numSessions).toBe('2');
});

it('does not count sessions belonging only to other recipients', async () => {
const scopes = {
grant: {
where: [{ id: [grant1.id, grant2.id] }],
},
trainingReport: [{ id: [trainingReport1.id, trainingReport2.id] }],
} as any;

const data = await trSessionsForRecipient(scopes, {
'recipientId.ctn': [String(recipient2.id)],
});

// Only session4 has grant2 (recipient2) and is COMPLETE
expect(data.numSessions).toBe('1');
});

it('returns 0 when no recipientId is provided', async () => {
const scopes = {
grant: {
where: [{ id: [grant1.id, grant2.id] }],
},
trainingReport: [{ id: [trainingReport1.id, trainingReport2.id] }],
} as any;

const data = await trSessionsForRecipient(scopes, {});
expect(data.numSessions).toBe('0');
});

it('returns 0 when an empty recipientId array is provided', async () => {
const scopes = {
grant: {
where: [{ id: [grant1.id, grant2.id] }],
},
trainingReport: [{ id: [trainingReport1.id, trainingReport2.id] }],
} as any;

const data = await trSessionsForRecipient(scopes, { 'recipientId.ctn': [] });
expect(data.numSessions).toBe('0');
});
});
Loading
Loading