From 0b05d01d5fe2588abf1939cbe72e26320d514eb0 Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Tue, 1 Oct 2024 11:15:30 -0400 Subject: [PATCH 01/28] start to hook up async calls for qa widgets --- .../RecipientsWithNoTta/__tests__/index.js | 266 +++++---------- .../QADashboard/RecipientsWithNoTta/index.js | 94 ++---- .../__tests__/index.js | 315 +++++++----------- .../RecipientsWithOhsStandardFeiGoal/index.js | 119 ++----- .../src/pages/QADashboard/__tests__/index.js | 236 ++++++++++++- frontend/src/pages/QADashboard/index.js | 252 +++----------- frontend/src/widgets/DeliveryMethodGraph.js | 14 + .../widgets/PercentageActivityReportByRole.js | 10 + .../QualityAssuranceDashboardOverview.js | 3 + frontend/src/widgets/RootCauseFeiGoals.js | 9 + 10 files changed, 588 insertions(+), 730 deletions(-) diff --git a/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js index e2913a19f6..693ea33afe 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js @@ -3,8 +3,11 @@ import React from 'react'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; import { SCOPE_IDS } from '@ttahub/common'; -import { render, screen, act } from '@testing-library/react'; +import { + render, screen, act, waitFor, +} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import fetchMock from 'fetch-mock'; import RecipientsWithNoTta from '../index'; import UserContext from '../../../../UserContext'; @@ -21,205 +24,112 @@ const defaultUser = { }], }; -const renderRecipientsWithNoTta = (data, user = defaultUser) => { +const RecipientsWithNoTtaDataEmpty = { + headers: ['Date of Last TTA', 'Days Since Last TTA'], + RecipientsWithNoTta: [], +}; + +const RecipientsWithNoTtaData = { + headers: ['Date of Last TTA', 'Days Since Last TTA'], + RecipientsWithNoTta: [ + { + id: 1, + heading: 'Test Recipient 1', + name: 'Test Recipient 1', + recipient: 'Test Recipient 1', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Date_of_Last_TTA', + value: '2021-09-01', + }, + { + title: 'Days_Since_Last_TTA', + value: '90', + }], + }, + { + id: 2, + heading: 'Test Recipient 2', + name: 'Test Recipient 2', + recipient: 'Test Recipient 2', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Date_of_Last_TTA', + value: '2021-09-02', + }, + { + title: 'Days_Since_Last_TTA', + value: '91', + }], + }, + { + id: 3, + heading: 'Test Recipient 3', + name: 'Test Recipient 3', + recipient: 'Test Recipient 3', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Date_of_Last_TTA', + value: '2021-09-03', + }, + { + title: 'Days_Since_Last_TTA', + value: '92', + }], + }], +}; + +const renderRecipientsWithNoTta = (user = defaultUser) => { render( - {}} - perPageNumber={10} - /> + , ); }; describe('Recipients With Ohs Standard Fei Goal', () => { + afterEach(() => fetchMock.restore()); + it('renders correctly without data', async () => { - const emptyData = { - headers: ['Recipient', 'Date of Last TTA', 'Days Since Last TTA'], - RecipientsWithNoTta: [], - }; - renderRecipientsWithNoTta(emptyData); + fetchMock.get('/api/ssdi/recipients-with-no-tta?region.in[]=1®ion.in[]=2', RecipientsWithNoTtaDataEmpty); + renderRecipientsWithNoTta(); expect(screen.queryAllByText(/recipients with no tta/i).length).toBe(2); expect(screen.getByText(/Recipients without Activity Reports or Training Reports for more than 90 days./i)).toBeInTheDocument(); }); it('renders correctly with data', async () => { - const data = { - headers: ['Goal created on', 'Goal number', 'Goal status', 'Root cause'], - RecipientsWithOhsStandardFeiGoal: [ - { - heading: 'Test Recipient 1', - name: 'Test Recipient 1', - recipient: 'Test Recipient 1', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-01', - }, - { - title: 'Goal_number', - value: 'G-20628', - }, - { - title: 'Goal_status', - value: 'In progress', - }, - { - title: 'Root_cause', - value: 'Community Partnership, Workforce', - }, - ], - }, - { - heading: 'Test Recipient 2', - name: 'Test Recipient 2', - recipient: 'Test Recipient 2', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-02', - }, - { - title: 'Goal_number', - value: 'G-359813', - }, - { - title: 'Goal_status', - value: 'Not started', - }, - { - title: 'Root_cause', - value: 'Testing', - }], - }, - { - heading: 'Test Recipient 3', - name: 'Test Recipient 3', - recipient: 'Test Recipient 3', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-03', - }, - { - title: 'Goal_number', - value: 'G-457825', - }, - { - title: 'Goal_status', - value: 'Unavailable', - }, - { - title: 'Root_cause', - value: 'Facilities', - }], - }], - }; - renderRecipientsWithNoTta(data); + fetchMock.get('/api/ssdi/recipients-with-no-tta?region.in[]=1®ion.in[]=2', RecipientsWithNoTtaData); + renderRecipientsWithNoTta(); expect(screen.queryAllByText(/recipients with no tta/i).length).toBe(2); expect(screen.getByText(/Recipients without Activity Reports or Training Reports for more than 90 days./i)).toBeInTheDocument(); - expect(screen.getByText(/test recipient 1/i)).toBeInTheDocument(); - expect(screen.getByText(/test recipient 2/i)).toBeInTheDocument(); + await act(async () => { + await waitFor(async () => { + expect(screen.getByText(/test recipient 1/i)).toBeInTheDocument(); + expect(screen.getByText(/test recipient 2/i)).toBeInTheDocument(); - expect(screen.getByText(/date of last tta/i)).toBeInTheDocument(); - expect(screen.getByText(/days since last tta/i)).toBeInTheDocument(); + expect(screen.getByText(/date of last tta/i)).toBeInTheDocument(); + expect(screen.getByText(/days since last tta/i)).toBeInTheDocument(); - expect(screen.getByText(/2021-09-01/i)).toBeInTheDocument(); - expect(screen.getByText(/2021-09-02/i)).toBeInTheDocument(); + expect(screen.getByText(/2021-09-01/i)).toBeInTheDocument(); + expect(screen.getByText(/2021-09-02/i)).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: /90/i })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: /91/i })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: /90/i })).toBeInTheDocument(); + expect(screen.getByRole('cell', { name: /91/i })).toBeInTheDocument(); + }); + }); }); + it('handles a user with only one region', async () => { - const data = { - headers: ['Goal created on', 'Goal number', 'Goal status', 'Root cause'], - RecipientsWithOhsStandardFeiGoal: [ - { - heading: 'Test Recipient 1', - name: 'Test Recipient 1', - recipient: 'Test Recipient 1', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-01', - }, - { - title: 'Goal_number', - value: 'G-20628', - }, - { - title: 'Goal_status', - value: 'In progress', - }, - { - title: 'Root_cause', - value: 'Community Partnership, Workforce', - }, - ], - }, - { - heading: 'Test Recipient 2', - name: 'Test Recipient 2', - recipient: 'Test Recipient 2', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-02', - }, - { - title: 'Goal_number', - value: 'G-359813', - }, - { - title: 'Goal_status', - value: 'Not started', - }, - { - title: 'Root_cause', - value: 'Testing', - }], - }, - { - heading: 'Test Recipient 3', - name: 'Test Recipient 3', - recipient: 'Test Recipient 3', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-03', - }, - { - title: 'Goal_number', - value: 'G-457825', - }, - { - title: 'Goal_status', - value: 'Unavailable', - }, - { - title: 'Root_cause', - value: 'Facilities', - }], - }], - }; + fetchMock.get('/api/ssdi/recipients-with-no-tta?region.in[]=1®ion.in[]=2', RecipientsWithNoTtaData); const u = { homeRegionId: 14, permissions: [{ @@ -227,7 +137,7 @@ describe('Recipients With Ohs Standard Fei Goal', () => { scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS, }], }; - renderRecipientsWithNoTta(data, u); + renderRecipientsWithNoTta(u); expect(screen.queryAllByText(/recipients with no tta/i).length).toBe(2); expect(screen.getByText(/Recipients without Activity Reports or Training Reports for more than 90 days./i)).toBeInTheDocument(); const filters = await screen.findByRole('button', { name: /open filters for this page/i }); diff --git a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js index faa1483a34..8aba68860b 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js @@ -1,14 +1,15 @@ import React, { useContext, - // useState, + useState, useRef, } from 'react'; import { Link } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import useDeepCompareEffect from 'use-deep-compare-effect'; import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; -// import { Grid, Alert } from '@trussworks/react-uswds'; +import { Grid, Alert } from '@trussworks/react-uswds'; import colors from '../../../colors'; import RecipientsWithNoTtaWidget from '../../../widgets/RecipientsWithNoTtaWidget'; import FilterPanel from '../../../components/filter/FilterPanel'; @@ -19,6 +20,7 @@ import ContentFromFeedByTag from '../../../components/ContentFromFeedByTag'; import DrawerTriggerButton from '../../../components/DrawerTriggerButton'; import UserContext from '../../../UserContext'; import { QA_DASHBOARD_FILTER_KEY, QA_DASHBOARD_FILTER_CONFIG } from '../constants'; +import { getSelfServiceData } from '../../../fetchers/ssdi'; const ALLOWED_SUBFILTERS = [ 'region', @@ -28,10 +30,11 @@ const ALLOWED_SUBFILTERS = [ 'recipient', 'stateCode', ]; - export default function RecipientsWithNoTta() { const pageDrawerRef = useRef(null); - // const [error] = useState(); + const [error, updateError] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [recipientsWithNoTTA, setRecipientsWithNoTTA] = useState([]); const { user } = useContext(UserContext); const { // from useUserDefaultRegionFilters @@ -50,6 +53,27 @@ export default function RecipientsWithNoTta() { QA_DASHBOARD_FILTER_CONFIG, ); + useDeepCompareEffect(() => { + async function fetchQaDat() { + setIsLoading(true); + // Filters passed also contains region. + try { + const data = await getSelfServiceData( + 'recipients-with-no-tta', + filters, + ); + setRecipientsWithNoTTA(data); + updateError(''); + } catch (e) { + updateError('Unable to fetch QA data'); + } finally { + setIsLoading(false); + } + } + // Call resources fetch. + fetchQaDat(); + }, [filters]); + return (
@@ -62,13 +86,13 @@ export default function RecipientsWithNoTta() {

Recipients with no TTA

- {/* + {error && ( {error} )} - */} +
); diff --git a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js index 9cc05b2a2d..621899cbf2 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js @@ -1,9 +1,13 @@ import '@testing-library/jest-dom'; import React from 'react'; +import moment from 'moment'; +import fetchMock from 'fetch-mock'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; import { SCOPE_IDS } from '@ttahub/common'; -import { render, screen, act } from '@testing-library/react'; +import { + render, screen, act, waitFor, +} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import RecipientsWithOhsStandardFeiGoal from '../index'; import UserContext from '../../../../UserContext'; @@ -21,214 +25,146 @@ const defaultUser = { }], }; -const renderRecipientsWithOhsStandardFeiGoal = (data, user = defaultUser) => { +const recipientsWithOhsStandardFeiGoalEmptyData = { + headers: ['Recipient', 'Date of Last TTA', 'Days Since Last TTA'], + RecipientsWithNoTta: [], +}; + +const recipientsWithOhsStandardFeiGoalData = { + headers: ['Goal created on', 'Goal number', 'Goal status', 'Root cause'], + RecipientsWithOhsStandardFeiGoal: [ + { + id: 1, + heading: 'Test Recipient 1', + name: 'Test Recipient 1', + recipient: 'Test Recipient 1', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Goal_created_on', + value: moment('2021-09-01').format('MM/DD/YYYY'), + }, + { + title: 'Goal_number', + value: 'G-20628', + }, + { + title: 'Goal_status', + value: 'In progress', + }, + { + title: 'Root_cause', + value: 'Community Partnership, Workforce', + }, + ], + }, + { + id: 2, + heading: 'Test Recipient 2', + name: 'Test Recipient 2', + recipient: 'Test Recipient 2', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Goal_created_on', + value: moment('2021-09-02').format('MM/DD/YYYY'), + }, + { + title: 'Goal_number', + value: 'G-359813', + }, + { + title: 'Goal_status', + value: 'Not started', + }, + { + title: 'Root_cause', + value: 'Testing', + }], + }, + { + id: 3, + heading: 'Test Recipient 3', + name: 'Test Recipient 3', + recipient: 'Test Recipient 3', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Goal_created_on', + value: moment('2021-09-03').format('MM/DD/YYYY'), + }, + { + title: 'Goal_number', + value: 'G-457825', + }, + { + title: 'Goal_status', + value: 'In progress', + }, + { + title: 'Root_cause', + value: 'Facilities', + }], + }], +}; + +const renderRecipientsWithOhsStandardFeiGoal = (user = defaultUser) => { render( - {}} - perPageNumber={10} - /> + , ); }; describe('Recipients With Ohs Standard Fei Goal', () => { + afterEach(() => { + fetchMock.restore(); + }); + it('renders correctly without data', async () => { - const emptyData = { - headers: ['Recipient', 'Date of Last TTA', 'Days Since Last TTA'], - RecipientsWithNoTta: [], - }; - renderRecipientsWithOhsStandardFeiGoal(emptyData); + fetchMock.get('/api/ssdi/recipients-with-ohs-standard-fei-goal?region.in[]=1®ion.in[]=2', recipientsWithOhsStandardFeiGoalEmptyData); + renderRecipientsWithOhsStandardFeiGoal(); expect(screen.queryAllByRole('heading', { name: /recipients with ohs standard fei goal/i }).length).toBe(1); expect(screen.getByText(/root causes were identified through self-reported data\./i)).toBeInTheDocument(); }); it('renders correctly with data', async () => { - const data = { - headers: ['Goal created on', 'Goal number', 'Goal status', 'Root cause'], - RecipientsWithOhsStandardFeiGoal: [ - { - heading: 'Test Recipient 1', - name: 'Test Recipient 1', - recipient: 'Test Recipient 1', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-01', - }, - { - title: 'Goal_number', - value: 'G-20628', - }, - { - title: 'Goal_status', - value: 'In progress', - }, - { - title: 'Root_cause', - value: 'Community Partnership, Workforce', - }, - ], - }, - { - heading: 'Test Recipient 2', - name: 'Test Recipient 2', - recipient: 'Test Recipient 2', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-02', - }, - { - title: 'Goal_number', - value: 'G-359813', - }, - { - title: 'Goal_status', - value: 'Not started', - }, - { - title: 'Root_cause', - value: 'Testing', - }], - }, - { - heading: 'Test Recipient 3', - name: 'Test Recipient 3', - recipient: 'Test Recipient 3', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-03', - }, - { - title: 'Goal_number', - value: 'G-457825', - }, - { - title: 'Goal_status', - value: 'Unavailable', - }, - { - title: 'Root_cause', - value: 'Facilities', - }], - }], - }; - renderRecipientsWithOhsStandardFeiGoal(data); + fetchMock.get('/api/ssdi/recipients-with-ohs-standard-goal?region.in[]=1®ion.in[]=2', recipientsWithOhsStandardFeiGoalData); + renderRecipientsWithOhsStandardFeiGoal(); expect(screen.queryAllByRole('heading', { name: /recipients with ohs standard fei goal/i }).length).toBe(1); expect(screen.getByText(/root causes were identified through self-reported data\./i)).toBeInTheDocument(); - - expect(screen.getByText(/Recipient 1/i)).toBeInTheDocument(); - expect(screen.getByText(/Recipient 2/i)).toBeInTheDocument(); - expect(screen.getByText(/Recipient 3/i)).toBeInTheDocument(); - - expect(screen.getByText('09/01/2021')).toBeInTheDocument(); - expect(screen.getByText('09/02/2021')).toBeInTheDocument(); - expect(screen.getByText('09/03/2021')).toBeInTheDocument(); - - expect(screen.getByText(/G-20628/i)).toBeInTheDocument(); - expect(screen.getByText(/G-359813/i)).toBeInTheDocument(); - expect(screen.getByText(/G-457825/i)).toBeInTheDocument(); - - expect(screen.queryAllByText(/In progress/i).length).toBe(2); - expect(screen.getByText(/Not started/i)).toBeInTheDocument(); - - expect(screen.getByText(/Community Partnership, Workforce/i)).toBeInTheDocument(); - expect(screen.getByText(/Testing/i)).toBeInTheDocument(); - expect(screen.getByText(/Facilities/i)).toBeInTheDocument(); + await act(async () => { + await waitFor(() => { + expect(screen.getByText(/Test Recipient 1/i)).toBeInTheDocument(); + expect(screen.getByText(/Test Recipient 2/i)).toBeInTheDocument(); + expect(screen.getByText(/Test Recipient 3/i)).toBeInTheDocument(); + + expect(screen.getByText('09/01/2021')).toBeInTheDocument(); + expect(screen.getByText('09/02/2021')).toBeInTheDocument(); + expect(screen.getByText('09/03/2021')).toBeInTheDocument(); + + expect(screen.getByText(/G-20628/i)).toBeInTheDocument(); + expect(screen.getByText(/G-359813/i)).toBeInTheDocument(); + expect(screen.getByText(/G-457825/i)).toBeInTheDocument(); + + expect(screen.queryAllByText(/In progress/i).length).toBe(2); + expect(screen.getByText(/Not started/i)).toBeInTheDocument(); + + expect(screen.getByText(/Community Partnership, Workforce/i)).toBeInTheDocument(); + expect(screen.getByText(/Testing/i)).toBeInTheDocument(); + expect(screen.getByText(/Facilities/i)).toBeInTheDocument(); + }); + }); }); + it('handles a user with only one region', async () => { - const data = { - headers: ['Goal created on', 'Goal number', 'Goal status', 'Root cause'], - RecipientsWithOhsStandardFeiGoal: [ - { - heading: 'Test Recipient 1', - name: 'Test Recipient 1', - recipient: 'Test Recipient 1', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-01', - }, - { - title: 'Goal_number', - value: 'G-20628', - }, - { - title: 'Goal_status', - value: 'In progress', - }, - { - title: 'Root_cause', - value: 'Community Partnership, Workforce', - }, - ], - }, - { - heading: 'Test Recipient 2', - name: 'Test Recipient 2', - recipient: 'Test Recipient 2', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-02', - }, - { - title: 'Goal_number', - value: 'G-359813', - }, - { - title: 'Goal_status', - value: 'Not started', - }, - { - title: 'Root_cause', - value: 'Testing', - }], - }, - { - heading: 'Test Recipient 3', - name: 'Test Recipient 3', - recipient: 'Test Recipient 3', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: '2021-09-03', - }, - { - title: 'Goal_number', - value: 'G-457825', - }, - { - title: 'Goal_status', - value: 'Unavailable', - }, - { - title: 'Root_cause', - value: 'Facilities', - }], - }], - }; const u = { homeRegionId: 14, permissions: [{ @@ -236,7 +172,8 @@ describe('Recipients With Ohs Standard Fei Goal', () => { scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS, }], }; - renderRecipientsWithOhsStandardFeiGoal(data, u); + fetchMock.get('/api/ssdi/recipients-with-ohs-standard-fei-goal?region.in[]=1®ion.in[]=2', recipientsWithOhsStandardFeiGoalData); + renderRecipientsWithOhsStandardFeiGoal(u); expect(screen.queryAllByRole('heading', { name: /recipients with ohs standard fei goal/i }).length).toBe(1); const filters = await screen.findByRole('button', { name: /open filters for this page/i }); diff --git a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js index 71149ebf8e..8dd18a429e 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js @@ -1,15 +1,15 @@ import React, { - // useState, + useState, useRef, useContext, } from 'react'; -import moment from 'moment'; import { Link } from 'react-router-dom'; +import useDeepCompareEffect from 'use-deep-compare-effect'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; -import { Grid } from '@trussworks/react-uswds'; +import { Grid, Alert } from '@trussworks/react-uswds'; import colors from '../../../colors'; import RecipientsWithOhsStandardFeiGoalWidget from '../../../widgets/RecipientsWithOhsStandardFeiGoalWidget'; import Drawer from '../../../components/Drawer'; @@ -20,6 +20,7 @@ import FilterPanelContainer from '../../../components/filter/FilterPanelContaine import useFilters from '../../../hooks/useFilters'; import { QA_DASHBOARD_FILTER_KEY, QA_DASHBOARD_FILTER_CONFIG } from '../constants'; import UserContext from '../../../UserContext'; +import { getSelfServiceData } from '../../../fetchers/ssdi'; const ALLOWED_SUBFILTERS = [ 'region', @@ -31,7 +32,9 @@ const ALLOWED_SUBFILTERS = [ ]; export default function RecipientsWithOhsStandardFeiGoal() { const pageDrawerRef = useRef(null); - // const [error] = useState(); + const [error, updateError] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [recipientsWithOhsStandardFeiGoal, setRecipientsWithOhsStandardFeiGoal] = useState([]); const { user } = useContext(UserContext); @@ -52,6 +55,27 @@ export default function RecipientsWithOhsStandardFeiGoal() { QA_DASHBOARD_FILTER_CONFIG, ); + useDeepCompareEffect(() => { + async function fetchQaDat() { + setIsLoading(true); + // Filters passed also contains region. + try { + const data = await getSelfServiceData( + 'recipients-with-ohs-standard-goal', + filters, + ); + setRecipientsWithOhsStandardFeiGoal(data); + updateError(''); + } catch (e) { + updateError('Unable to fetch QA data'); + } finally { + setIsLoading(false); + } + } + // Call resources fetch. + fetchQaDat(); + }, [filters]); + return (
@@ -65,11 +89,11 @@ export default function RecipientsWithOhsStandardFeiGoal() { Recipients with OHS standard FEI goal - {/* {error && ( + {error && ( {error} - )} */} + )}
); diff --git a/frontend/src/pages/QADashboard/__tests__/index.js b/frontend/src/pages/QADashboard/__tests__/index.js index 943d3354b0..88ff9fc041 100644 --- a/frontend/src/pages/QADashboard/__tests__/index.js +++ b/frontend/src/pages/QADashboard/__tests__/index.js @@ -1,6 +1,7 @@ /* eslint-disable max-len */ import '@testing-library/jest-dom'; import React from 'react'; +import join from 'url-join'; import { SCOPE_IDS } from '@ttahub/common'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; @@ -8,6 +9,7 @@ import { render, screen, act, + waitFor, } from '@testing-library/react'; import fetchMock from 'fetch-mock'; import userEvent from '@testing-library/user-event'; @@ -28,6 +30,213 @@ const defaultUser = { scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS, }], }; +const apiCall = join('api', 'ssdi', 'qa-dashboard?region.in[]=1®ion.in[]=2'); +const overviewData = { + recipientsWithNoTTA: { pct: '2.52%', filterApplicable: true }, + recipientsWithOhsStandardFeiGoals: { pct: '73.25%', filterApplicable: false }, + recipientsWithOhsStandardClass: { pct: '14.26%', filterApplicable: false }, +}; + +const DELIVERY_METHOD_GRAPH_DATA = { + total_in_person_count: 8420, + average_in_person_percentage: 73, + total_virtual_count: 2734, + average_virtual_percentage: 24, + total_hybrid_count: 356, + average_hybrid_percentage: 3, + records: [{ + month: 'Jan 23', + in_person_count: 818, + hybrid_count: 0, + in_person_percentage: 80, + virtual_count: 204, + virtual_percentage: 20, + hybrid_percentage: 0, + }, + { + month: 'Feb 23', + in_person_count: 1750, + virtual_count: 174, + hybrid_count: 0, + in_person_percentage: 83, + virtual_percentage: 17, + hybrid_percentage: 0, + }, + { + month: 'Mar 23', + in_person_count: 742, + virtual_count: 143, + hybrid_count: 1, + in_person_percentage: 83, + virtual_percentage: 16, + hybrid_percentage: 1, + }, + { + month: 'Apr 23', + in_person_count: 936, + virtual_count: 255, + hybrid_count: 24, + in_person_percentage: 77, + virtual_percentage: 16, + hybrid_percentage: 1, + }, + { + month: 'May 23', + in_person_count: 742, + virtual_count: 191, + hybrid_count: 29, + in_person_percentage: 77, + virtual_percentage: 20, + hybrid_percentage: 3, + }, + { + month: 'Jun 23', + in_person_count: 650, + in_person_percentage: 83, + virtual_count: 102, + virtual_percentage: 13, + hybrid_count: 31, + hybrid_percentage: 4, + }, + { + month: 'Jul 23', + in_person_count: 827, + in_person_percentage: 84, + virtual_count: 138, + virtual_percentage: 13, + hybrid_count: 20, + hybrid_percentage: 2, + }, { + month: 'Aug 23', + in_person_count: 756, + in_person_percentage: 76, + virtual_count: 206, + virtual_percentage: 21, + hybrid_count: 20, + hybrid_percentage: 2, + }, + { + month: 'Sep 23', + in_person_count: 699, + in_person_percentage: 73, + virtual_count: 258, + virtual_percentage: 26, + hybrid_count: 0, + hybrid_percentage: 0, + }, + { + month: 'Oct 23', + in_person_count: 855, + in_person_percentage: 82, + virtual_count: 177, + virtual_percentage: 17, + hybrid_count: 11, + hybrid_percentage: 1, + }, + { + month: 'Nov 23', + in_person_count: 803, + in_person_percentage: 79, + virtual_count: 290, + virtual_percentage: 16, + hybrid_count: 78, + hybrid_percentage: 5, + }, + { + month: 'Dec 23', + in_person_count: 689, + in_person_percentage: 69, + virtual_count: 596, + virtual_percentage: 29, + hybrid_count: 64, + hybrid_percentage: 2, + }, + ], +}; + +const ROLE_GRAPH_DATA = { + totalNumberOfReports: 11510, + totalPercentage: 100, + records: [ + { + role_name: 'ECM', + role_count: 6, + percentage: 1, + }, + { + role_name: 'ECS', + role_count: 6892, + percentage: 58, + }, + { + role_name: 'FES', + role_count: 135, + percentage: 2, + }, + { + role_name: 'GS', + role_count: 4258, + percentage: 36, + }, + { + role_name: 'GSM', + role_count: 23, + percentage: 1, + }, + { + role_name: 'HS', + role_count: 153, + percentage: 2, + }, + { + role_name: 'SS', + role_count: 0, + percentage: 0, + }, + { + role_name: 'TTAC', + role_count: 0, + percentage: 0, + }, + ], +}; + +const ROOT_CAUSE_FEI_GOALS_DATA = { + totalNumberOfGoals: 11510, + totalNumberOfRootCauses: 21637, + records: [ + { + rootCause: 'Community Partnerships', + response_count: 2532, + percentage: 22, + }, + { + rootCause: 'Facilities', + response_count: 2186, + percentage: 19, + }, + { + rootCause: 'Family Circumstances', + response_count: 2762, + percentage: 24, + }, + { + rootCause: 'Other ECE Care Options', + response_count: 3683, + percentage: 32, + }, + { + rootCause: 'Unavailable', + response_count: 115, + percentage: 1, + }, + { + rootCause: 'Workforce', + response_count: 10359, + percentage: 90, + }, + ], +}; describe('Resource Dashboard page', () => { afterEach(() => fetchMock.restore()); @@ -44,6 +253,14 @@ describe('Resource Dashboard page', () => { }; it('renders correctly', async () => { + // Mock the API call and return data that contains a property overviewData. + fetchMock.get(apiCall, { + overviewData, + deliveryMethod: DELIVERY_METHOD_GRAPH_DATA, + roleGraph: ROLE_GRAPH_DATA, + rootCauseFeiGoalsGraph: ROOT_CAUSE_FEI_GOALS_DATA, + }); + renderQADashboard(); // Header @@ -55,11 +272,24 @@ describe('Resource Dashboard page', () => { expect(await screen.findByText('Recipients with OHS standard CLASS goal')).toBeVisible(); // Assert test data. - expect(await screen.findByText('2.52%')).toBeVisible(); - expect(await screen.findByText('73.25%')).toBeVisible(); - expect(await screen.findByText('14.26%')).toBeVisible(); + await act(async () => { + await waitFor(() => { + expect(screen.getByText('2.52%')).toBeVisible(); + expect(screen.getByText('73.25%')).toBeVisible(); + expect(screen.getByText('14.26%')).toBeVisible(); + }); + }); }); + it('removes region filter when user has only one region', async () => { + // Mock the API call and return data that contains a property overviewData. + fetchMock.get(apiCall, { + overviewData, + deliveryMethod: DELIVERY_METHOD_GRAPH_DATA, + roleGraph: ROLE_GRAPH_DATA, + rootCauseFeiGoalsGraph: ROOT_CAUSE_FEI_GOALS_DATA, + }); + const u = { homeRegionId: 14, permissions: [{ diff --git a/frontend/src/pages/QADashboard/index.js b/frontend/src/pages/QADashboard/index.js index 10a6f18a67..6967366ef2 100644 --- a/frontend/src/pages/QADashboard/index.js +++ b/frontend/src/pages/QADashboard/index.js @@ -1,10 +1,13 @@ import React, { useContext, useRef, + useState, } from 'react'; import { Helmet } from 'react-helmet'; +import useDeepCompareEffect from 'use-deep-compare-effect'; import { Grid, + Alert, } from '@trussworks/react-uswds'; import QAOverview from '../../widgets/QualityAssuranceDashboardOverview'; import DeliveryMethod from '../../widgets/DeliveryMethodGraph'; @@ -18,6 +21,7 @@ import Drawer from '../../components/Drawer'; import ContentFromFeedByTag from '../../components/ContentFromFeedByTag'; import PercentageActivityReportByRole from '../../widgets/PercentageActivityReportByRole'; import RootCauseFeiGoals from '../../widgets/RootCauseFeiGoals'; +import { getSelfServiceData } from '../../fetchers/ssdi'; const DISALLOWED_FILTERS = [ 'domainClassroomOrganization', @@ -27,210 +31,12 @@ const DISALLOWED_FILTERS = [ const ALLOWED_SUBFILTERS = QA_DASHBOARD_FILTER_CONFIG.map(({ id }) => id) .filter((id) => !DISALLOWED_FILTERS.includes(id)); -const DELIVERY_METHOD_GRAPH_DATA = { - total_in_person_count: 8420, - average_in_person_percentage: 73, - total_virtual_count: 2734, - average_virtual_percentage: 24, - total_hybrid_count: 356, - average_hybrid_percentage: 3, - records: [{ - month: 'Jan 23', - in_person_count: 818, - hybrid_count: 0, - in_person_percentage: 80, - virtual_count: 204, - virtual_percentage: 20, - hybrid_percentage: 0, - }, - { - month: 'Feb 23', - in_person_count: 1750, - virtual_count: 174, - hybrid_count: 0, - in_person_percentage: 83, - virtual_percentage: 17, - hybrid_percentage: 0, - }, - { - month: 'Mar 23', - in_person_count: 742, - virtual_count: 143, - hybrid_count: 1, - in_person_percentage: 83, - virtual_percentage: 16, - hybrid_percentage: 1, - }, - { - month: 'Apr 23', - in_person_count: 936, - virtual_count: 255, - hybrid_count: 24, - in_person_percentage: 77, - virtual_percentage: 16, - hybrid_percentage: 1, - }, - { - month: 'May 23', - in_person_count: 742, - virtual_count: 191, - hybrid_count: 29, - in_person_percentage: 77, - virtual_percentage: 20, - hybrid_percentage: 3, - }, - { - month: 'Jun 23', - in_person_count: 650, - in_person_percentage: 83, - virtual_count: 102, - virtual_percentage: 13, - hybrid_count: 31, - hybrid_percentage: 4, - }, - { - month: 'Jul 23', - in_person_count: 827, - in_person_percentage: 84, - virtual_count: 138, - virtual_percentage: 13, - hybrid_count: 20, - hybrid_percentage: 2, - }, { - month: 'Aug 23', - in_person_count: 756, - in_person_percentage: 76, - virtual_count: 206, - virtual_percentage: 21, - hybrid_count: 20, - hybrid_percentage: 2, - }, - { - month: 'Sep 23', - in_person_count: 699, - in_person_percentage: 73, - virtual_count: 258, - virtual_percentage: 26, - hybrid_count: 0, - hybrid_percentage: 0, - }, - { - month: 'Oct 23', - in_person_count: 855, - in_person_percentage: 82, - virtual_count: 177, - virtual_percentage: 17, - hybrid_count: 11, - hybrid_percentage: 1, - }, - { - month: 'Nov 23', - in_person_count: 803, - in_person_percentage: 79, - virtual_count: 290, - virtual_percentage: 16, - hybrid_count: 78, - hybrid_percentage: 5, - }, - { - month: 'Dec 23', - in_person_count: 689, - in_person_percentage: 69, - virtual_count: 596, - virtual_percentage: 29, - hybrid_count: 64, - hybrid_percentage: 2, - }, - ], -}; - -const ROLE_GRAPH_DATA = { - totalNumberOfReports: 11510, - totalPercentage: 100, - records: [ - { - role_name: 'ECM', - role_count: 6, - percentage: 1, - }, - { - role_name: 'ECS', - role_count: 6892, - percentage: 58, - }, - { - role_name: 'FES', - role_count: 135, - percentage: 2, - }, - { - role_name: 'GS', - role_count: 4258, - percentage: 36, - }, - { - role_name: 'GSM', - role_count: 23, - percentage: 1, - }, - { - role_name: 'HS', - role_count: 153, - percentage: 2, - }, - { - role_name: 'SS', - role_count: 0, - percentage: 0, - }, - { - role_name: 'TTAC', - role_count: 0, - percentage: 0, - }, - ], -}; - -const ROOT_CAUSE_FEI_GOALS_DATA = { - totalNumberOfGoals: 11510, - totalNumberOfRootCauses: 21637, - records: [ - { - rootCause: 'Community Partnerships', - response_count: 2532, - percentage: 22, - }, - { - rootCause: 'Facilities', - response_count: 2186, - percentage: 19, - }, - { - rootCause: 'Family Circumstances', - response_count: 2762, - percentage: 24, - }, - { - rootCause: 'Other ECE Care Options', - response_count: 3683, - percentage: 32, - }, - { - rootCause: 'Unavailable', - response_count: 115, - percentage: 1, - }, - { - rootCause: 'Workforce', - response_count: 10359, - percentage: 90, - }, - ], -}; - export default function QADashboard() { const { user } = useContext(UserContext); const drawerTriggerRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); + const [error, updateError] = useState(); + const [qaData, setQaData] = useState({}); const { // from useUserDefaultRegionFilters regions, @@ -248,6 +54,27 @@ export default function QADashboard() { QA_DASHBOARD_FILTER_CONFIG, ); + useDeepCompareEffect(() => { + async function fetchQaDat() { + setIsLoading(true); + // Filters passed also contains region. + try { + const data = await getSelfServiceData( + 'qa-dashboard', + filters, + ); + setQaData(data); + updateError(''); + } catch (e) { + updateError('Unable to fetch QA data'); + } finally { + setIsLoading(false); + } + } + // Call resources fetch. + fetchQaDat(); + }, [filters]); + return ( <> @@ -257,6 +84,11 @@ export default function QADashboard() {

Quality assurance dashboard

+ {error && ( + + {error} + + )}
- + - + diff --git a/frontend/src/widgets/DeliveryMethodGraph.js b/frontend/src/widgets/DeliveryMethodGraph.js index 442e1c9cc3..1404ce3704 100644 --- a/frontend/src/widgets/DeliveryMethodGraph.js +++ b/frontend/src/widgets/DeliveryMethodGraph.js @@ -77,6 +77,20 @@ export default function DeliveryMethodGraph({ data }) { // records is an array of objects // and the other fields need to be converted to camelCase useEffect(() => { + if (!data) { + setTabularData([]); + setTraces([]); + setTotals({ + totalInPerson: 0, + averageInPersonPercentage: 0, + totalVirtualCount: 0, + averageVirtualPercentage: 0, + totalHybridCount: 0, + averageHybridPercentage: 0, + }); + return; + } + // take the API data // and transform it into the format // that the LineGraph component expects diff --git a/frontend/src/widgets/PercentageActivityReportByRole.js b/frontend/src/widgets/PercentageActivityReportByRole.js index b610cc0ba0..493333d8d1 100644 --- a/frontend/src/widgets/PercentageActivityReportByRole.js +++ b/frontend/src/widgets/PercentageActivityReportByRole.js @@ -64,6 +64,16 @@ export default function PercentageActivityReportByRole({ data }) { // records is an array of objects // and the other fields need to be converted to camelCase useEffect(() => { + if (!data) { + setTabularData([]); + setTrace([]); + setTotals({ + totalNumberOfReports: 0, + totalPercentage: 100, + }); + return; + } + // take the API data // and transform it into the format // that the LineGraph component expects diff --git a/frontend/src/widgets/QualityAssuranceDashboardOverview.js b/frontend/src/widgets/QualityAssuranceDashboardOverview.js index c09dc05863..fb6443c499 100644 --- a/frontend/src/widgets/QualityAssuranceDashboardOverview.js +++ b/frontend/src/widgets/QualityAssuranceDashboardOverview.js @@ -47,6 +47,9 @@ const createOverviewFieldArray = (data) => ([ export function QualityAssuranceDashboardOverview({ data, loading, }) { + if (!data) { + return null; + } const DASHBOARD_FIELDS = createOverviewFieldArray(data); return ( { + if (!data) { + setTabularData([]); + setTrace([]); + setTotals({ + totalNumberOfGoals: 0, + totalNumberOfRootCauses: 0, + }); + return; + } // take the API data // and transform it into the format // that the LineGraph component expects From 540d8548fccba41878b7beae25dfb9ca81031d4a Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Tue, 1 Oct 2024 15:36:54 -0400 Subject: [PATCH 02/28] recipients with ohs standard fei goals async --- frontend/src/fetchers/ssdi.js | 8 +- .../__tests__/index.js | 49 +++++++++--- .../index.js | 78 +++++++------------ ...RecipientsWithClassScoresAndGoalsWidget.js | 4 +- 4 files changed, 77 insertions(+), 62 deletions(-) diff --git a/frontend/src/fetchers/ssdi.js b/frontend/src/fetchers/ssdi.js index 2f8e71e908..aeb8e8b444 100644 --- a/frontend/src/fetchers/ssdi.js +++ b/frontend/src/fetchers/ssdi.js @@ -20,6 +20,13 @@ const allowedTopicsForQuery = { 'grantNumber', 'stateCode', ], + 'recipients-with-class-scores-and-goals': [ + 'region', + 'startDate', + 'endDate', + 'grantNumber', + 'stateCode', + ], 'qa-dashboard': [...QA_DASHBOARD_FILTER_CONFIG.map((filter) => filter.id), 'region'], }; @@ -42,7 +49,6 @@ const getSelfServiceUrl = (filterName, filters) => { export const getSelfServiceData = async (filterName, filters) => { const url = getSelfServiceUrl(filterName, filters); - const response = await get(url); if (!response.ok) { throw new Error('Error fetching self service data'); diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js index 26d74dc35c..b007145d96 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js @@ -1,15 +1,17 @@ import '@testing-library/jest-dom'; import React from 'react'; -import { render, screen } from '@testing-library/react'; +import fetchMock from 'fetch-mock'; +import { + render, screen, act, waitFor, +} from '@testing-library/react'; import { Router } from 'react-router'; import { createMemoryHistory } from 'history'; import RecipientsWithClassScoresAndGoals from '../index'; import UserContext from '../../../../UserContext'; -/* const recipients = [{ id: 1, - name: 'Recipient 1', + name: 'Abernathy, Mraz and Bogan', lastArStartDate: '01/02/2021', emotionalSupport: 6.0430, classroomOrganization: 5.0430, @@ -41,34 +43,45 @@ const recipients = [{ goals: [ { goalNumber: 'G-68745', - status: 'In progress', + status: 'Complete', creator: 'Bill Parks', collaborator: 'Jack Jones', }, ], }]; -*/ -const renderRecipientsWithClassScoresAndGoals = (data) => { +const recipientsWithClassScoresAndGoalsData = { + headers: ['Emotional Support', 'Classroom Organization', 'Instructional Support', 'Report Received Date', 'Goals'], + RecipientsWithOhsStandardFeiGoal: recipients, +}; + +const renderRecipientsWithClassScoresAndGoals = () => { const history = createMemoryHistory(); render( - + , ); }; describe('Recipients With Class and Scores and Goals', () => { + afterEach(() => { + fetchMock.restore(); + }); + it('renders correctly with data', async () => { + fetchMock.get('/api/ssdi/recipients-with-class-scores-and-goals', recipientsWithClassScoresAndGoalsData); renderRecipientsWithClassScoresAndGoals(); expect(screen.queryAllByText(/Recipients with CLASS® scores/i).length).toBe(2); - expect(screen.getByText(/1-2 of 2/i)).toBeInTheDocument(); + + await act(async () => { + await waitFor(() => { + expect(screen.getByText(/1-2 of 2/i)).toBeInTheDocument(); + }); + }); expect(screen.getByText('Abernathy, Mraz and Bogan')).toBeInTheDocument(); expect(screen.getByText('01/02/2021')).toBeInTheDocument(); @@ -108,7 +121,15 @@ describe('Recipients With Class and Scores and Goals', () => { }); it('selects and unselects all recipients', async () => { + fetchMock.get('/api/ssdi/recipients-with-class-scores-and-goals', recipientsWithClassScoresAndGoalsData); renderRecipientsWithClassScoresAndGoals(); + + await act(async () => { + await waitFor(() => { + expect(screen.getByText(/1-2 of 2/i)).toBeInTheDocument(); + }); + }); + const selectAllButton = screen.getByRole('checkbox', { name: /select all recipients/i }); selectAllButton.click(); expect(screen.getByText(/2 selected/i)).toBeInTheDocument(); @@ -117,7 +138,13 @@ describe('Recipients With Class and Scores and Goals', () => { }); it('Shows the selected pill and unselects when pill is removed', async () => { + fetchMock.get('/api/ssdi/recipients-with-class-scores-and-goals', recipientsWithClassScoresAndGoalsData); renderRecipientsWithClassScoresAndGoals(); + await act(async () => { + await waitFor(() => { + expect(screen.getByText(/1-2 of 2/i)).toBeInTheDocument(); + }); + }); const selectAllButton = screen.getByRole('checkbox', { name: /select all recipients/i }); selectAllButton.click(); expect(screen.getByText(/2 selected/i)).toBeInTheDocument(); diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js index 21ebb1619c..215eb739b8 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js @@ -5,7 +5,7 @@ import React, { } from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; - +import useDeepCompareEffect from 'use-deep-compare-effect'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; import { Helmet } from 'react-helmet'; @@ -22,6 +22,7 @@ import useFilters from '../../../hooks/useFilters'; import RecipientsWithClassScoresAndGoalsWidget from '../../../widgets/RecipientsWithClassScoresAndGoalsWidget'; import { QA_DASHBOARD_FILTER_KEY, QA_DASHBOARD_FILTER_CONFIG } from '../constants'; import UserContext from '../../../UserContext'; +import { getSelfServiceData } from '../../../fetchers/ssdi'; const ALLOWED_SUBFILTERS = [ 'domainClassroomOrganization', @@ -35,50 +36,12 @@ const ALLOWED_SUBFILTERS = [ 'region', 'stateCode', ]; - -const recipients = [{ - id: 1, - name: 'Abernathy, Mraz and Bogan', - lastArStartDate: '01/02/2021', - emotionalSupport: 6.0430, - classroomOrganization: 5.0430, - instructionalSupport: 4.0430, - reportReceivedDate: '03/01/2022', - goals: [ - { - goalNumber: 'G-45641', - status: 'In progress', - creator: 'John Doe', - collaborator: 'Jane Doe', - }, - { - goalNumber: 'G-25858', - status: 'Suspended', - creator: 'Bill Smith', - collaborator: 'Bob Jones', - }, - ], -}, -{ - id: 2, - name: 'Recipient 2', - lastArStartDate: '04/02/2021', - emotionalSupport: 5.254, - classroomOrganization: 8.458, - instructionalSupport: 1.214, - reportReceivedDate: '05/01/2022', - goals: [ - { - goalNumber: 'G-68745', - status: 'Complete', - creator: 'Bill Parks', - collaborator: 'Jack Jones', - }, - ], -}]; export default function RecipientsWithClassScoresAndGoals() { const pageDrawerRef = useRef(null); - const [error] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [error, updateError] = useState(); + const [recipientsWithClassScoresAndGoalsData, + setRecipientsWithClassScoresAndGoalsData] = useState({}); const { user } = useContext(UserContext); const { // from useUserDefaultRegionFilters @@ -97,6 +60,27 @@ export default function RecipientsWithClassScoresAndGoals() { QA_DASHBOARD_FILTER_CONFIG, ); + useDeepCompareEffect(() => { + async function fetchQaDat() { + setIsLoading(true); + // Filters passed also contains region. + try { + const data = await getSelfServiceData( + 'recipients-with-class-scores-and-goals', + filters, + ); + setRecipientsWithClassScoresAndGoalsData(data); + updateError(''); + } catch (e) { + updateError('Unable to fetch QA data'); + } finally { + setIsLoading(false); + } + } + // Call resources fetch. + fetchQaDat(); + }, [filters]); + return (
@@ -139,12 +123,8 @@ export default function RecipientsWithClassScoresAndGoals() {
); diff --git a/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js b/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js index 6ec9ac49b1..7d66ad2b93 100644 --- a/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js +++ b/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js @@ -22,6 +22,7 @@ import './QaDetailsDrawer.scss'; function RecipientsWithClassScoresAndGoalsWidget({ data, + parentLoading, }) { const titleDrawerRef = useRef(null); const subtitleRef = useRef(null); @@ -163,7 +164,7 @@ function RecipientsWithClassScoresAndGoalsWidget({ return ( Date: Wed, 9 Oct 2024 11:25:17 -0400 Subject: [PATCH 03/28] updates to for ssdi --- frontend/src/fetchers/ssdi.js | 49 +++++++++--- frontend/src/pages/QADashboard/index.js | 80 ++++++++++++++++++- frontend/src/widgets/DeliveryMethodGraph.js | 8 +- .../QualityAssuranceDashboardOverview.js | 22 ++--- frontend/src/widgets/RootCauseFeiGoals.js | 2 + 5 files changed, 133 insertions(+), 28 deletions(-) diff --git a/frontend/src/fetchers/ssdi.js b/frontend/src/fetchers/ssdi.js index aeb8e8b444..3aa9205269 100644 --- a/frontend/src/fetchers/ssdi.js +++ b/frontend/src/fetchers/ssdi.js @@ -5,29 +5,52 @@ import { QA_DASHBOARD_FILTER_CONFIG } from '../pages/QADashboard/constants'; const ssdiUrl = join('/', 'api', 'ssdi'); +const urlsForQuery = { + 'recipients-with-no-tta': join(ssdiUrl, 'api', 'dashboards', 'qa', 'no-tta.sql'), + 'recipients-with-ohs-standard-fei-goal': join(ssdiUrl, 'api', 'dashboards', 'qa', 'fei.sql'), + 'recipients-with-class-scores-and-goals': join(ssdiUrl, 'api', 'dashboards', 'qa', 'class.sql'), + 'qa-dashboard': join(ssdiUrl, 'api', 'dashboards', 'qa', 'dashboard.sql'), +}; + const allowedTopicsForQuery = { 'recipients-with-no-tta': [ - 'region', - 'startDate', - 'endDate', + 'recipient', 'grantNumber', + 'programType', 'stateCode', - ], - 'recipients-with-ohs-standard-goal': [ - 'region', + // 'region', + 'regionIds', 'startDate', 'endDate', + 'dataSetSelection', + ], + 'recipients-with-ohs-standard-fei-goal': [ + 'recipient', 'grantNumber', + 'programType', 'stateCode', + //'region', + 'regionIds', + 'group', + 'createDate', + 'reason', ], 'recipients-with-class-scores-and-goals': [ - 'region', - 'startDate', - 'endDate', + 'recipient', 'grantNumber', + 'programType', 'stateCode', + // 'region', + 'regionIds', + 'domainEmotionalSupport', + 'domainClassroomOrganization', + 'domainInstructionalSupport', + 'createDate', + ], + 'qa-dashboard': [...QA_DASHBOARD_FILTER_CONFIG.map((filter) => filter), + // 'region' + 'regionIds', ], - 'qa-dashboard': [...QA_DASHBOARD_FILTER_CONFIG.map((filter) => filter.id), 'region'], }; export const getSelfServiceDataQueryString = (filterName, filters) => { @@ -43,13 +66,13 @@ export const getSelfServiceDataQueryString = (filterName, filters) => { const getSelfServiceUrl = (filterName, filters) => { const queryString = getSelfServiceDataQueryString(filterName, filters); - const baseUrl = join(ssdiUrl, filterName); + const baseUrl = join(urlsForQuery[filterName]); return `${baseUrl}?${queryString}`; }; -export const getSelfServiceData = async (filterName, filters) => { +export const getSelfServiceData = async (filterName, filters, dataSetSelection) => { const url = getSelfServiceUrl(filterName, filters); - const response = await get(url); + const response = await get(url + (dataSetSelection.length ? dataSetSelection.map((s) => `&dataSetSelection[]=${s}`).join('') : '')); if (!response.ok) { throw new Error('Error fetching self service data'); } diff --git a/frontend/src/pages/QADashboard/index.js b/frontend/src/pages/QADashboard/index.js index 421f3d616f..59ca6446b3 100644 --- a/frontend/src/pages/QADashboard/index.js +++ b/frontend/src/pages/QADashboard/index.js @@ -55,15 +55,87 @@ export default function QADashboard() { ); useDeepCompareEffect(() => { - async function fetchQaDat() { + async function fetchQaData() { setIsLoading(true); // Filters passed also contains region. try { - const data = await getSelfServiceData( + /* + // Recipient with no tta data. + const recipientsWithNoTtaData = await getSelfServiceData( + 'recipients-with-no-tta', + filters, + ['no_tta_widget'], + ); + const noTTAData = recipientsWithNoTtaData.find((item) => item.data_set === 'no_tta_widget'); + + // FEI data. + const feiData = await getSelfServiceData( + 'recipients-with-ohs-standard-fei-goal', + filters, + ['with_fei_widget', 'with_fei_graph'], + ); + const feiOverviewData = feiData.find((item) => item.data_set === 'with_fei_widget'); + const feiGraphData = feiData.find((item) => item.data_set === 'with_fei_graph'); + + const rootCauseFeiGoalsGraph = { + records: feiGraphData.data, + totalNumberOfGoals: feiOverviewData.data[0].total, + totalNumberOfRootCauses: feiOverviewData.data[0]['recipients with fei'], + }; + + // CLASS data. + const classData = await getSelfServiceData( + 'recipients-with-class-scores-and-goals', + filters, + ['with_class_widget'], + ); + const classOverviewData = classData.find((item) => item.data_set === 'with_class_widget'); + + // Build overview data. + const overviewData = { + recipientsWithNoTTA: { + pct: noTTAData.data[0]['% recipients without tta'] || '0%', + }, + recipientsWithOhsStandardFeiGoals: { + pct: feiOverviewData.data[0]['% recipients with fei'] || '0%', + }, + recipientsWithOhsStandardClass: { + pct: classOverviewData.data[0]['% recipients with class'] || '0%', + }, + }; + */ + // Dashboard data. + const dashboardData = await getSelfServiceData( 'qa-dashboard', filters, + ['delivery_method_graph', 'role_graph'], ); - setQaData(data); + + console.log('----------------> await come back with data: ', dashboardData); + + const deliveryMethodData = dashboardData.find((item) => item.data_set === 'delivery_method_graph'); + const roleGraphData = dashboardData.find((item) => item.data_set === 'role_graph'); + + const deliveryMethod = { + records: deliveryMethodData.data, + totalInPerson: 0, + averageInPersonPercentage: 0, + totalVirtualCount: 0, + averageVirtualPercentage: 0, + totalHybridCount: 0, + averageHybridPercentage: 0, + }; + + console.log('-----ALL: deliveryMethod ', deliveryMethod); + + + + // Set data. + setQaData({ + //overviewData, + //rootCauseFeiGoalsGraph, + deliveryMethod, + }); updateError(''); } catch (e) { updateError('Unable to fetch QA data'); @@ -72,7 +144,7 @@ export default function QADashboard() { } } // Call resources fetch. - fetchQaDat(); + fetchQaData(); }, [filters]); return ( diff --git a/frontend/src/widgets/DeliveryMethodGraph.js b/frontend/src/widgets/DeliveryMethodGraph.js index 1404ce3704..78cbab6a39 100644 --- a/frontend/src/widgets/DeliveryMethodGraph.js +++ b/frontend/src/widgets/DeliveryMethodGraph.js @@ -33,7 +33,7 @@ const DEFAULT_SORT_CONFIG = { const KEY_COLUMNS = ['Months']; -export default function DeliveryMethodGraph({ data }) { +export default function DeliveryMethodGraph({ data, loading }) { const widgetRef = useRef(null); const capture = useMediaCapture(widgetRef, 'Total TTA hours'); const [showTabularData, setShowTabularData] = useState(false); @@ -106,6 +106,8 @@ export default function DeliveryMethodGraph({ data }) { average_hybrid_percentage: averageHybridPercentage, } = data; + console.log('data passed to DeliveryMethodGraph', data); + const tableData = []; // use a map for quick lookup const traceMap = new Map(); @@ -230,7 +232,7 @@ export default function DeliveryMethodGraph({ data }) { return ( ([ label1: 'Recipients with no TTA', iconColor: colors.ttahubBlue, backgroundColor: colors.ttahubBlueLight, - data: data.recipientsWithNoTTA.pct, + data: data && data.recipientsWithNoTTA ? data.recipientsWithNoTTA.pct : 0, route: 'qa-dashboard/recipients-with-no-tta', - filterApplicable: data.recipientsWithNoTTA.filterApplicable, + filterApplicable: false, }, { icon: faBus, @@ -27,9 +27,11 @@ const createOverviewFieldArray = (data) => ([ label1: 'Recipients with OHS standard FEI goal', iconColor: colors.ttahubOrange, backgroundColor: colors.ttahubOrangeLight, - data: data.recipientsWithOhsStandardFeiGoals.pct, + data: data && data.recipientsWithOhsStandardFeiGoals + ? data.recipientsWithOhsStandardFeiGoals.pct + : 0, route: 'qa-dashboard/recipients-with-ohs-standard-fei-goal', - filterApplicable: data.recipientsWithOhsStandardFeiGoals.filterApplicable, + filterApplicable: false, }, { key: 'recipients-with-ohs-standard-class-goals', @@ -38,9 +40,11 @@ const createOverviewFieldArray = (data) => ([ label1: 'Recipients with OHS standard CLASS goal', iconColor: colors.success, backgroundColor: colors.ttahubDeepTealLight, - data: data.recipientsWithOhsStandardClass.pct, + data: data && data.recipientsWithOhsStandardClass + ? data.recipientsWithOhsStandardClass.pct + : 0, route: 'qa-dashboard/recipients-with-class-scores-and-goals', - filterApplicable: data.recipientsWithOhsStandardClass.filterApplicable, + filterApplicable: false, }, ]); @@ -62,13 +66,13 @@ export function QualityAssuranceDashboardOverview({ QualityAssuranceDashboardOverview.propTypes = { data: PropTypes.shape({ recipientsWithNoTTA: PropTypes.shape({ - pct: PropTypes.string, + pct: PropTypes.number, }), recipientsWithOhsStandardFeiGoals: PropTypes.shape({ - pct: PropTypes.string, + pct: PropTypes.number, }), recipientsWithOhsStandardClass: PropTypes.shape({ - pct: PropTypes.string, + pct: PropTypes.number, }), }), loading: PropTypes.bool, diff --git a/frontend/src/widgets/RootCauseFeiGoals.js b/frontend/src/widgets/RootCauseFeiGoals.js index ad5a4dc809..6658c66db0 100644 --- a/frontend/src/widgets/RootCauseFeiGoals.js +++ b/frontend/src/widgets/RootCauseFeiGoals.js @@ -87,6 +87,8 @@ export default function RootCauseFeiGoals({ data }) { totalNumberOfRootCauses, } = data; + console.log('records>>>>>', records); + const tableData = []; const traceData = []; From 7b9c1d07c9180aacce40e35d3ea61e6b2f8218c7 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 9 Oct 2024 13:45:05 -0400 Subject: [PATCH 04/28] Connect delivery method graph --- frontend/src/components/Menu.js | 2 +- frontend/src/pages/QADashboard/index.js | 14 ++----- frontend/src/widgets/DeliveryMethodGraph.js | 46 ++++++++++++--------- frontend/src/widgets/LineGraph.js | 4 +- 4 files changed, 31 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/Menu.js b/frontend/src/components/Menu.js index feb8d3814d..4696a064de 100644 --- a/frontend/src/components/Menu.js +++ b/frontend/src/components/Menu.js @@ -152,7 +152,7 @@ function Menu({
    {menuItems.map((item) => (
  • -
diff --git a/frontend/src/widgets/DeliveryMethodGraph.js b/frontend/src/widgets/DeliveryMethodGraph.js index 78cbab6a39..bdea525176 100644 --- a/frontend/src/widgets/DeliveryMethodGraph.js +++ b/frontend/src/widgets/DeliveryMethodGraph.js @@ -5,6 +5,7 @@ import React, { useMemo, } from 'react'; import PropTypes from 'prop-types'; +import moment from 'moment'; import LineGraph from './LineGraph'; import WidgetContainer from '../components/WidgetContainer'; import useMediaCapture from '../hooks/useMediaCapture'; @@ -88,6 +89,7 @@ export default function DeliveryMethodGraph({ data, loading }) { totalHybridCount: 0, averageHybridPercentage: 0, }); + return; } @@ -96,17 +98,21 @@ export default function DeliveryMethodGraph({ data, loading }) { // that the LineGraph component expects // (an object for each trace) // and the table (an array of objects in the format defined by proptypes) - const { - records, - total_in_person_count: totalInPerson, - average_in_person_percentage: averageInPersonPercentage, - total_virtual_count: totalVirtualCount, - average_virtual_percentage: averageVirtualPercentage, - total_hybrid_count: totalHybridCount, - average_hybrid_percentage: averageHybridPercentage, - } = data; - console.log('data passed to DeliveryMethodGraph', data); + const { records: unfilteredRecords } = data; + const total = [...unfilteredRecords].pop(); + const records = unfilteredRecords.filter((record) => record.month !== 'Total'); + + console.log(total); + + const { + in_person_count: totalInPerson, + in_person_percentage: averageInPersonPercentage, + virtual_count: totalVirtualCount, + virtual_percentage: averageVirtualPercentage, + hybrid_count: totalHybridCount, + hybrid_percentage: averageHybridPercentage, + } = total; const tableData = []; // use a map for quick lookup @@ -123,7 +129,7 @@ export default function DeliveryMethodGraph({ data, loading }) { records.forEach((dataset, index) => { tableData.push({ - heading: dataset.month, + heading: moment(dataset.month, 'YYYY-MM-DD').format('MMM YYYY'), sortKey: index + 1, id: index + 1, data: [ @@ -160,25 +166,25 @@ export default function DeliveryMethodGraph({ data, loading }) { ], }); - traceMap.get('In person').x.push(dataset.month); + traceMap.get('In person').x.push(moment(dataset.month, 'YYYY-MM-DD').format('MMM YYYY')); traceMap.get('In person').y.push(dataset.in_person_percentage); - traceMap.get('Virtual').x.push(dataset.month); + traceMap.get('Virtual').x.push(moment(dataset.month, 'YYYY-MM-DD').format('MMM YYYY')); traceMap.get('Virtual').y.push(dataset.virtual_percentage); - traceMap.get('Hybrid').x.push(dataset.month); + traceMap.get('Hybrid').x.push(moment(dataset.month, 'YYYY-MM-DD').format('MMM YYYY')); traceMap.get('Hybrid').y.push(dataset.hybrid_percentage); }); setTraces(Array.from(traceMap.values())); setTabularData(tableData); setTotals({ - totalInPerson, - averageInPersonPercentage, - totalVirtualCount, - averageVirtualPercentage, - totalHybridCount, - averageHybridPercentage, + totalInPerson: totalInPerson.toLocaleString('en-us'), + averageInPersonPercentage: `${averageInPersonPercentage}%`, + totalVirtualCount: totalVirtualCount.toLocaleString('en-us'), + averageVirtualPercentage: `${averageVirtualPercentage}%`, + totalHybridCount: totalHybridCount.toLocaleString('en-us'), + averageHybridPercentage: `${averageHybridPercentage}%`, }); }, [data]); // end use effect diff --git a/frontend/src/widgets/LineGraph.js b/frontend/src/widgets/LineGraph.js index cc0aaca45c..aa53d3bf96 100644 --- a/frontend/src/widgets/LineGraph.js +++ b/frontend/src/widgets/LineGraph.js @@ -186,12 +186,10 @@ export default function LineGraph({ bgcolor: colors.textInk, }, }, - ]; const tracesToDraw = legends.map((legend, index) => (legend.selected ? traces[index] : null)) - .filter((trace) => trace !== null); - + .filter((trace) => Boolean(trace)); // draw the plot Plotly.newPlot(lines.current, tracesToDraw, layout, { displayModeBar: false, hovermode: 'none', responsive: true }); }, [data, hideYAxis, legends, showTabularData, xAxisTitle, yAxisTitle]); From 88cc0a28af4dd85a3d433847cd852da2def7f943 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 9 Oct 2024 13:45:42 -0400 Subject: [PATCH 05/28] Remove console.log --- frontend/src/widgets/DeliveryMethodGraph.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/widgets/DeliveryMethodGraph.js b/frontend/src/widgets/DeliveryMethodGraph.js index bdea525176..e2ea5ebbe1 100644 --- a/frontend/src/widgets/DeliveryMethodGraph.js +++ b/frontend/src/widgets/DeliveryMethodGraph.js @@ -103,8 +103,6 @@ export default function DeliveryMethodGraph({ data, loading }) { const total = [...unfilteredRecords].pop(); const records = unfilteredRecords.filter((record) => record.month !== 'Total'); - console.log(total); - const { in_person_count: totalInPerson, in_person_percentage: averageInPersonPercentage, From 906c8738567de1cd11f6de3fce0b83ff58f309ef Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Wed, 9 Oct 2024 19:15:02 -0400 Subject: [PATCH 06/28] updates to widgets and tests for ssdi --- frontend/src/fetchers/__tests__/ssdi.js | 10 +- frontend/src/fetchers/ssdi.js | 11 +- .../__tests__/index.js | 8 +- .../index.js | 1 + .../RecipientsWithNoTta/__tests__/index.js | 6 +- .../QADashboard/RecipientsWithNoTta/index.js | 1 + .../__tests__/index.js | 6 +- .../RecipientsWithOhsStandardFeiGoal/index.js | 3 +- .../src/pages/QADashboard/__tests__/index.js | 370 ++++++++---------- frontend/src/pages/QADashboard/index.js | 17 +- .../widgets/PercentageActivityReportByRole.js | 9 +- .../QualityAssuranceDashboardOverview.js | 22 +- frontend/src/widgets/RootCauseFeiGoals.js | 11 +- 13 files changed, 219 insertions(+), 256 deletions(-) diff --git a/frontend/src/fetchers/__tests__/ssdi.js b/frontend/src/fetchers/__tests__/ssdi.js index 1ee58c12b1..0a8739ccb7 100644 --- a/frontend/src/fetchers/__tests__/ssdi.js +++ b/frontend/src/fetchers/__tests__/ssdi.js @@ -2,7 +2,7 @@ import fetchMock from 'fetch-mock'; import join from 'url-join'; import { getSelfServiceData } from '../ssdi'; -const ssdiUrl = join('/', 'api', 'ssdi'); +const ssdiUrl = join('/', 'api', 'ssdi', 'api', 'dashboards', 'qa'); beforeEach(() => { fetchMock.restore(); @@ -11,7 +11,7 @@ beforeEach(() => { describe('SSDI fetcher', () => { it('should fetch data for qa-dashboard', async () => { const mockData = [{ data: 'Expected' }]; - const url = join(ssdiUrl, 'qa-dashboard', '?region.in[]=14'); + const url = join(ssdiUrl, 'dashboard.sql', '?region.in[]=14'); fetchMock.getOnce(url, mockData); const res = await getSelfServiceData('qa-dashboard', [{ id: '9ac8381c-2507-4b4a-a30c-6f1f87a00901', @@ -24,10 +24,10 @@ describe('SSDI fetcher', () => { }); it('recipients-with-ohs-standard-goal', async () => { const mockData = [{ data: 'Expected' }]; - const url = join(ssdiUrl, 'recipients-with-ohs-standard-goal', '?region.in[]=14'); + const url = join(ssdiUrl, 'fei.sql', '?region.in[]=14'); fetchMock.getOnce(url, mockData); - const res = await getSelfServiceData('recipients-with-ohs-standard-goal', [ + const res = await getSelfServiceData('recipients-with-ohs-standard-fei-goal', [ { id: '9ac8381c-2507-4b4a-a30c-6f1f87a00901', topic: 'region', @@ -46,7 +46,7 @@ describe('SSDI fetcher', () => { }); it('recipients-with-no-tta', async () => { const mockData = [{ data: 'Expected' }]; - const url = join(ssdiUrl, 'recipients-with-no-tta', '?region.in[]=14'); + const url = join(ssdiUrl, 'no-tta.sql', '?region.in[]=14'); fetchMock.getOnce(url, mockData); const res = await getSelfServiceData('recipients-with-no-tta', [ diff --git a/frontend/src/fetchers/ssdi.js b/frontend/src/fetchers/ssdi.js index 3aa9205269..92a46102d9 100644 --- a/frontend/src/fetchers/ssdi.js +++ b/frontend/src/fetchers/ssdi.js @@ -18,7 +18,7 @@ const allowedTopicsForQuery = { 'grantNumber', 'programType', 'stateCode', - // 'region', + 'region', 'regionIds', 'startDate', 'endDate', @@ -29,7 +29,7 @@ const allowedTopicsForQuery = { 'grantNumber', 'programType', 'stateCode', - //'region', + 'region', 'regionIds', 'group', 'createDate', @@ -40,7 +40,7 @@ const allowedTopicsForQuery = { 'grantNumber', 'programType', 'stateCode', - // 'region', + 'region', 'regionIds', 'domainEmotionalSupport', 'domainClassroomOrganization', @@ -48,7 +48,7 @@ const allowedTopicsForQuery = { 'createDate', ], 'qa-dashboard': [...QA_DASHBOARD_FILTER_CONFIG.map((filter) => filter), - // 'region' + 'region', 'regionIds', ], }; @@ -72,7 +72,8 @@ const getSelfServiceUrl = (filterName, filters) => { export const getSelfServiceData = async (filterName, filters, dataSetSelection) => { const url = getSelfServiceUrl(filterName, filters); - const response = await get(url + (dataSetSelection.length ? dataSetSelection.map((s) => `&dataSetSelection[]=${s}`).join('') : '')); + const urlToUse = url + (dataSetSelection && dataSetSelection.length ? dataSetSelection.map((s) => `&dataSetSelection[]=${s}`).join('') : ''); + const response = await get(urlToUse); if (!response.ok) { throw new Error('Error fetching self service data'); } diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js index b007145d96..d883dc3bc5 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js @@ -9,6 +9,8 @@ import { createMemoryHistory } from 'history'; import RecipientsWithClassScoresAndGoals from '../index'; import UserContext from '../../../../UserContext'; +const dashboardApi = '/api/ssdi/api/dashboards/qa/class.sql?&dataSetSelection[]=delivery_method_graph&dataSetSelection[]=role_graph'; + const recipients = [{ id: 1, name: 'Abernathy, Mraz and Bogan', @@ -72,7 +74,7 @@ describe('Recipients With Class and Scores and Goals', () => { }); it('renders correctly with data', async () => { - fetchMock.get('/api/ssdi/recipients-with-class-scores-and-goals', recipientsWithClassScoresAndGoalsData); + fetchMock.get(dashboardApi, recipientsWithClassScoresAndGoalsData); renderRecipientsWithClassScoresAndGoals(); expect(screen.queryAllByText(/Recipients with CLASS® scores/i).length).toBe(2); @@ -121,7 +123,7 @@ describe('Recipients With Class and Scores and Goals', () => { }); it('selects and unselects all recipients', async () => { - fetchMock.get('/api/ssdi/recipients-with-class-scores-and-goals', recipientsWithClassScoresAndGoalsData); + fetchMock.get(dashboardApi, recipientsWithClassScoresAndGoalsData); renderRecipientsWithClassScoresAndGoals(); await act(async () => { @@ -138,7 +140,7 @@ describe('Recipients With Class and Scores and Goals', () => { }); it('Shows the selected pill and unselects when pill is removed', async () => { - fetchMock.get('/api/ssdi/recipients-with-class-scores-and-goals', recipientsWithClassScoresAndGoalsData); + fetchMock.get(dashboardApi, recipientsWithClassScoresAndGoalsData); renderRecipientsWithClassScoresAndGoals(); await act(async () => { await waitFor(() => { diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js index 215eb739b8..5774e11e30 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js @@ -68,6 +68,7 @@ export default function RecipientsWithClassScoresAndGoals() { const data = await getSelfServiceData( 'recipients-with-class-scores-and-goals', filters, + ['delivery_method_graph', 'role_graph'], ); setRecipientsWithClassScoresAndGoalsData(data); updateError(''); diff --git a/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js index 693ea33afe..8a5bde6db3 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js @@ -99,14 +99,14 @@ describe('Recipients With Ohs Standard Fei Goal', () => { afterEach(() => fetchMock.restore()); it('renders correctly without data', async () => { - fetchMock.get('/api/ssdi/recipients-with-no-tta?region.in[]=1®ion.in[]=2', RecipientsWithNoTtaDataEmpty); + fetchMock.get('/api/ssdi/api/dashboards/qa/no-tta.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=no_tta_widget', RecipientsWithNoTtaDataEmpty); renderRecipientsWithNoTta(); expect(screen.queryAllByText(/recipients with no tta/i).length).toBe(2); expect(screen.getByText(/Recipients without Activity Reports or Training Reports for more than 90 days./i)).toBeInTheDocument(); }); it('renders correctly with data', async () => { - fetchMock.get('/api/ssdi/recipients-with-no-tta?region.in[]=1®ion.in[]=2', RecipientsWithNoTtaData); + fetchMock.get('/api/ssdi/api/dashboards/qa/no-tta.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=no_tta_widget', RecipientsWithNoTtaData); renderRecipientsWithNoTta(); expect(screen.queryAllByText(/recipients with no tta/i).length).toBe(2); expect(screen.getByText(/Recipients without Activity Reports or Training Reports for more than 90 days./i)).toBeInTheDocument(); @@ -129,7 +129,7 @@ describe('Recipients With Ohs Standard Fei Goal', () => { }); it('handles a user with only one region', async () => { - fetchMock.get('/api/ssdi/recipients-with-no-tta?region.in[]=1®ion.in[]=2', RecipientsWithNoTtaData); + fetchMock.get('/api/ssdi/api/dashboards/qa/no-tta.sql?&dataSetSelection[]=no_tta_widget', RecipientsWithNoTtaData); const u = { homeRegionId: 14, permissions: [{ diff --git a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js index 8aba68860b..71178687ae 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js @@ -61,6 +61,7 @@ export default function RecipientsWithNoTta() { const data = await getSelfServiceData( 'recipients-with-no-tta', filters, + ['no_tta_widget'], ); setRecipientsWithNoTTA(data); updateError(''); diff --git a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js index 621899cbf2..fffc78ddc6 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js @@ -127,7 +127,7 @@ describe('Recipients With Ohs Standard Fei Goal', () => { }); it('renders correctly without data', async () => { - fetchMock.get('/api/ssdi/recipients-with-ohs-standard-fei-goal?region.in[]=1®ion.in[]=2', recipientsWithOhsStandardFeiGoalEmptyData); + fetchMock.get('/api/ssdi/api/dashboards/qa/fei.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_graph', recipientsWithOhsStandardFeiGoalEmptyData); renderRecipientsWithOhsStandardFeiGoal(); expect(screen.queryAllByRole('heading', { name: /recipients with ohs standard fei goal/i }).length).toBe(1); @@ -135,7 +135,7 @@ describe('Recipients With Ohs Standard Fei Goal', () => { }); it('renders correctly with data', async () => { - fetchMock.get('/api/ssdi/recipients-with-ohs-standard-goal?region.in[]=1®ion.in[]=2', recipientsWithOhsStandardFeiGoalData); + fetchMock.get('/api/ssdi/api/dashboards/qa/fei.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_graph', recipientsWithOhsStandardFeiGoalData); renderRecipientsWithOhsStandardFeiGoal(); expect(screen.queryAllByRole('heading', { name: /recipients with ohs standard fei goal/i }).length).toBe(1); @@ -172,7 +172,7 @@ describe('Recipients With Ohs Standard Fei Goal', () => { scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS, }], }; - fetchMock.get('/api/ssdi/recipients-with-ohs-standard-fei-goal?region.in[]=1®ion.in[]=2', recipientsWithOhsStandardFeiGoalData); + fetchMock.get('/api/ssdi/api/dashboards/qa/fei.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_graph', recipientsWithOhsStandardFeiGoalData); renderRecipientsWithOhsStandardFeiGoal(u); expect(screen.queryAllByRole('heading', { name: /recipients with ohs standard fei goal/i }).length).toBe(1); diff --git a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js index 8dd18a429e..bbfd322401 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js @@ -61,8 +61,9 @@ export default function RecipientsWithOhsStandardFeiGoal() { // Filters passed also contains region. try { const data = await getSelfServiceData( - 'recipients-with-ohs-standard-goal', + 'recipients-with-ohs-standard-fei-goal', filters, + ['with_fei_widget', 'with_fei_graph'], ); setRecipientsWithOhsStandardFeiGoal(data); updateError(''); diff --git a/frontend/src/pages/QADashboard/__tests__/index.js b/frontend/src/pages/QADashboard/__tests__/index.js index 88ff9fc041..c2d133e9cb 100644 --- a/frontend/src/pages/QADashboard/__tests__/index.js +++ b/frontend/src/pages/QADashboard/__tests__/index.js @@ -1,7 +1,6 @@ /* eslint-disable max-len */ import '@testing-library/jest-dom'; import React from 'react'; -import join from 'url-join'; import { SCOPE_IDS } from '@ttahub/common'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; @@ -30,216 +29,177 @@ const defaultUser = { scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS, }], }; -const apiCall = join('api', 'ssdi', 'qa-dashboard?region.in[]=1®ion.in[]=2'); -const overviewData = { - recipientsWithNoTTA: { pct: '2.52%', filterApplicable: true }, - recipientsWithOhsStandardFeiGoals: { pct: '73.25%', filterApplicable: false }, - recipientsWithOhsStandardClass: { pct: '14.26%', filterApplicable: false }, -}; -const DELIVERY_METHOD_GRAPH_DATA = { - total_in_person_count: 8420, - average_in_person_percentage: 73, - total_virtual_count: 2734, - average_virtual_percentage: 24, - total_hybrid_count: 356, - average_hybrid_percentage: 3, - records: [{ - month: 'Jan 23', - in_person_count: 818, - hybrid_count: 0, - in_person_percentage: 80, - virtual_count: 204, - virtual_percentage: 20, - hybrid_percentage: 0, - }, - { - month: 'Feb 23', - in_person_count: 1750, - virtual_count: 174, - hybrid_count: 0, - in_person_percentage: 83, - virtual_percentage: 17, - hybrid_percentage: 0, - }, - { - month: 'Mar 23', - in_person_count: 742, - virtual_count: 143, - hybrid_count: 1, - in_person_percentage: 83, - virtual_percentage: 16, - hybrid_percentage: 1, - }, - { - month: 'Apr 23', - in_person_count: 936, - virtual_count: 255, - hybrid_count: 24, - in_person_percentage: 77, - virtual_percentage: 16, - hybrid_percentage: 1, - }, - { - month: 'May 23', - in_person_count: 742, - virtual_count: 191, - hybrid_count: 29, - in_person_percentage: 77, - virtual_percentage: 20, - hybrid_percentage: 3, - }, +const baseSsdiApi = '/api/ssdi/api/dashboards/qa/'; +const noTtaApi = `${baseSsdiApi}no-tta.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=no_tta_widget`; +const feiApi = `${baseSsdiApi}fei.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_graph`; +const dashboardApi = `${baseSsdiApi}dashboard.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=delivery_method_graph&dataSetSelection[]=role_graph`; +const classApi = `${baseSsdiApi}class.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_class_widget`; + +const RECIPIENTS_WITH_NO_TTA_DATA = [ { - month: 'Jun 23', - in_person_count: 650, - in_person_percentage: 83, - virtual_count: 102, - virtual_percentage: 13, - hybrid_count: 31, - hybrid_percentage: 4, + data_set: 'no_tta_widget', + records: '1', + data: [ + { + total: 1460, + 'recipients without tta': 794, + '% recipients without tta': 54.38, + }, + ], }, +]; + +const RECIPIENT_CLASS_DATA = [ { - month: 'Jul 23', - in_person_count: 827, - in_person_percentage: 84, - virtual_count: 138, - virtual_percentage: 13, - hybrid_count: 20, - hybrid_percentage: 2, - }, { - month: 'Aug 23', - in_person_count: 756, - in_person_percentage: 76, - virtual_count: 206, - virtual_percentage: 21, - hybrid_count: 20, - hybrid_percentage: 2, + data_set: 'with_class_widget', + records: '1', + data: [ + { + total: 1550, + 'recipients with class': 283, + '% recipients with class': 18.26, + }, + ], + active_filters: [ + 'regionIds', + 'currentUserId', + ], }, +]; + +const DASHBOARD_DATA = [ { - month: 'Sep 23', - in_person_count: 699, - in_person_percentage: 73, - virtual_count: 258, - virtual_percentage: 26, - hybrid_count: 0, - hybrid_percentage: 0, + data_set: 'role_graph', + records: '8', + data: [ + { + role_name: 'TTAC', + percentage: 0.06, + role_count: 16, + }, + { + role_name: 'SS', + percentage: 0.24, + role_count: 66, + }, + { + role_name: 'HS', + percentage: 6.82, + role_count: 1876, + }, + ], }, { - month: 'Oct 23', - in_person_count: 855, - in_person_percentage: 82, - virtual_count: 177, - virtual_percentage: 17, - hybrid_count: 11, - hybrid_percentage: 1, + data_set: 'delivery_method_graph', + records: '48', + data: [ + { + month: '2024-01-01', + hybrid_count: 18, + virtual_count: 613, + in_person_count: 310, + hybrid_percentage: 1.91, + virtual_percentage: 65.14, + in_person_percentage: 32.94, + }, + { + month: '2024-02-01', + hybrid_count: 8, + virtual_count: 581, + in_person_count: 307, + hybrid_percentage: 0.89, + virtual_percentage: 64.84, + in_person_percentage: 34.26, + }, + { + month: '2024-03-01', + hybrid_count: 22, + virtual_count: 619, + in_person_count: 360, + hybrid_percentage: 2.2, + virtual_percentage: 61.84, + in_person_percentage: 35.96, + }, + ], }, +]; + +const ROOT_CAUSE_FEI_GOALS_DATA = [ { - month: 'Nov 23', - in_person_count: 803, - in_person_percentage: 79, - virtual_count: 290, - virtual_percentage: 16, - hybrid_count: 78, - hybrid_percentage: 5, + data_set: 'with_fei_graph', + record: '6', + data: [ + { + rootCause: 'Facilities', + percentage: 9, + response_count: 335, + }, + { + rootCause: 'Workforce', + percentage: 46, + response_count: 1656, + }, + { + rootCause: 'Community Partnerships', + percentage: 8, + response_count: 275, + }, + { + rootCause: 'Family Circumstances', + percentage: 11, + response_count: 391, + }, + { + rootCause: 'Other ECE Care Options', + percentage: 18, + response_count: 637, + }, + { + rootCause: 'Unavailable', + percentage: 8, + response_count: 295, + }, + ], + active_filters: [ + 'regionIds', + 'currentUserId', + ], }, { - month: 'Dec 23', - in_person_count: 689, - in_person_percentage: 69, - virtual_count: 596, - virtual_percentage: 29, - hybrid_count: 64, - hybrid_percentage: 2, + data_set: 'with_fei_widget', + records: '1', + data: [ + { + total: 1550, + 'recipients with fei': 858, + '% recipients with fei': 55.35, + }, + ], + active_filters: [ + 'regionIds', + 'currentUserId', + ], }, - ], -}; +]; -const ROLE_GRAPH_DATA = { - totalNumberOfReports: 11510, - totalPercentage: 100, - records: [ - { - role_name: 'ECM', - role_count: 6, - percentage: 1, - }, - { - role_name: 'ECS', - role_count: 6892, - percentage: 58, - }, - { - role_name: 'FES', - role_count: 135, - percentage: 2, - }, - { - role_name: 'GS', - role_count: 4258, - percentage: 36, - }, - { - role_name: 'GSM', - role_count: 23, - percentage: 1, - }, - { - role_name: 'HS', - role_count: 153, - percentage: 2, - }, - { - role_name: 'SS', - role_count: 0, - percentage: 0, - }, - { - role_name: 'TTAC', - role_count: 0, - percentage: 0, - }, - ], -}; +describe('Resource Dashboard page', () => { + beforeEach(() => { + // Mock Recipients with no TTA data. + fetchMock.get(noTtaApi, RECIPIENTS_WITH_NO_TTA_DATA); -const ROOT_CAUSE_FEI_GOALS_DATA = { - totalNumberOfGoals: 11510, - totalNumberOfRootCauses: 21637, - records: [ - { - rootCause: 'Community Partnerships', - response_count: 2532, - percentage: 22, - }, - { - rootCause: 'Facilities', - response_count: 2186, - percentage: 19, - }, - { - rootCause: 'Family Circumstances', - response_count: 2762, - percentage: 24, - }, - { - rootCause: 'Other ECE Care Options', - response_count: 3683, - percentage: 32, - }, - { - rootCause: 'Unavailable', - response_count: 115, - percentage: 1, - }, - { - rootCause: 'Workforce', - response_count: 10359, - percentage: 90, - }, - ], -}; + // Mock Recipients with OHS standard FEI goal data. + fetchMock.get(feiApi, ROOT_CAUSE_FEI_GOALS_DATA); + + // Mock Recipients with OHS standard CLASS data. + fetchMock.get(classApi, RECIPIENT_CLASS_DATA); + + // Mock Dashboard data. + fetchMock.get(dashboardApi, DASHBOARD_DATA); + }); -describe('Resource Dashboard page', () => { afterEach(() => fetchMock.restore()); + const renderQADashboard = (user = defaultUser) => { render( @@ -253,14 +213,6 @@ describe('Resource Dashboard page', () => { }; it('renders correctly', async () => { - // Mock the API call and return data that contains a property overviewData. - fetchMock.get(apiCall, { - overviewData, - deliveryMethod: DELIVERY_METHOD_GRAPH_DATA, - roleGraph: ROLE_GRAPH_DATA, - rootCauseFeiGoalsGraph: ROOT_CAUSE_FEI_GOALS_DATA, - }); - renderQADashboard(); // Header @@ -274,22 +226,14 @@ describe('Resource Dashboard page', () => { // Assert test data. await act(async () => { await waitFor(() => { - expect(screen.getByText('2.52%')).toBeVisible(); - expect(screen.getByText('73.25%')).toBeVisible(); - expect(screen.getByText('14.26%')).toBeVisible(); + expect(screen.getByText('54.38%')).toBeVisible(); + expect(screen.getByText('18.26%')).toBeVisible(); + expect(screen.getByText('55.35%')).toBeVisible(); }); }); }); it('removes region filter when user has only one region', async () => { - // Mock the API call and return data that contains a property overviewData. - fetchMock.get(apiCall, { - overviewData, - deliveryMethod: DELIVERY_METHOD_GRAPH_DATA, - roleGraph: ROLE_GRAPH_DATA, - rootCauseFeiGoalsGraph: ROOT_CAUSE_FEI_GOALS_DATA, - }); - const u = { homeRegionId: 14, permissions: [{ diff --git a/frontend/src/pages/QADashboard/index.js b/frontend/src/pages/QADashboard/index.js index 9f7d3eb032..653d593e57 100644 --- a/frontend/src/pages/QADashboard/index.js +++ b/frontend/src/pages/QADashboard/index.js @@ -59,7 +59,6 @@ export default function QADashboard() { setIsLoading(true); // Filters passed also contains region. try { - /* // Recipient with no tta data. const recipientsWithNoTtaData = await getSelfServiceData( 'recipients-with-no-tta', @@ -103,7 +102,7 @@ export default function QADashboard() { pct: classOverviewData.data[0]['% recipients with class'] || '0%', }, }; - */ + // Dashboard data. const dashboardData = await getSelfServiceData( 'qa-dashboard', @@ -112,7 +111,7 @@ export default function QADashboard() { ); const deliveryMethodData = dashboardData.find((item) => item.data_set === 'delivery_method_graph'); - // const roleGraphData = dashboardData.find((item) => item.data_set === 'role_graph'); + const roleGraphData = dashboardData.find((item) => item.data_set === 'role_graph'); const deliveryMethod = { records: deliveryMethodData.data, @@ -124,11 +123,16 @@ export default function QADashboard() { averageHybridPercentage: 0, }; + const roleGraph = { + records: roleGraphData.data, + }; + // Set data. setQaData({ - // overviewData, - // rootCauseFeiGoalsGraph, + overviewData, + rootCauseFeiGoalsGraph, deliveryMethod, + roleGraph, }); updateError(''); } catch (e) { @@ -187,11 +191,12 @@ export default function QADashboard() { - + diff --git a/frontend/src/widgets/PercentageActivityReportByRole.js b/frontend/src/widgets/PercentageActivityReportByRole.js index 493333d8d1..cd3db70a61 100644 --- a/frontend/src/widgets/PercentageActivityReportByRole.js +++ b/frontend/src/widgets/PercentageActivityReportByRole.js @@ -24,7 +24,7 @@ const DEFAULT_SORT_CONFIG = { activePage: 1, }; -export default function PercentageActivityReportByRole({ data }) { +export default function PercentageActivityReportByRole({ data, loading }) { const widgetRef = useRef(null); const capture = useMediaCapture(widgetRef, 'Percentage of activity reports by role'); const [showTabularData, setShowTabularData] = useState(false); @@ -133,7 +133,7 @@ export default function PercentageActivityReportByRole({ data }) {
([ label1: 'Recipients with no TTA', iconColor: colors.ttahubBlue, backgroundColor: colors.ttahubBlueLight, - data: data && data.recipientsWithNoTTA ? data.recipientsWithNoTTA.pct : 0, + data: data && data.recipientsWithNoTTA ? `${data.recipientsWithNoTTA.pct}%` : '0%', route: 'qa-dashboard/recipients-with-no-tta', - filterApplicable: false, + filterApplicable: data.recipientsWithNoTTA.filterApplicable, }, { icon: faBus, @@ -28,10 +28,10 @@ const createOverviewFieldArray = (data) => ([ iconColor: colors.ttahubOrange, backgroundColor: colors.ttahubOrangeLight, data: data && data.recipientsWithOhsStandardFeiGoals - ? data.recipientsWithOhsStandardFeiGoals.pct - : 0, + ? `${data.recipientsWithOhsStandardFeiGoals.pct}%` + : '0%', route: 'qa-dashboard/recipients-with-ohs-standard-fei-goal', - filterApplicable: false, + filterApplicable: data.recipientsWithOhsStandardFeiGoals.filterApplicable, }, { key: 'recipients-with-ohs-standard-class-goals', @@ -41,10 +41,10 @@ const createOverviewFieldArray = (data) => ([ iconColor: colors.success, backgroundColor: colors.ttahubDeepTealLight, data: data && data.recipientsWithOhsStandardClass - ? data.recipientsWithOhsStandardClass.pct - : 0, + ? `${data.recipientsWithOhsStandardClass.pct}%` + : '0%', route: 'qa-dashboard/recipients-with-class-scores-and-goals', - filterApplicable: false, + filterApplicable: data.recipientsWithOhsStandardClass.filterApplicable, }, ]); @@ -81,15 +81,15 @@ QualityAssuranceDashboardOverview.propTypes = { QualityAssuranceDashboardOverview.defaultProps = { data: { recipientsWithNoTTA: { - pct: '0%', + pct: 0, filterApplicable: false, }, recipientsWithOhsStandardFeiGoals: { - pct: '0%', + pct: 0, filterApplicable: false, }, recipientsWithOhsStandardClass: { - pct: '0%', + pct: 0, filterApplicable: false, }, }, diff --git a/frontend/src/widgets/RootCauseFeiGoals.js b/frontend/src/widgets/RootCauseFeiGoals.js index 6658c66db0..7cf3a5d30f 100644 --- a/frontend/src/widgets/RootCauseFeiGoals.js +++ b/frontend/src/widgets/RootCauseFeiGoals.js @@ -27,7 +27,7 @@ const DEFAULT_SORT_CONFIG = { activePage: 1, }; -export default function RootCauseFeiGoals({ data }) { +export default function RootCauseFeiGoals({ data, loading }) { const widgetRef = useRef(null); const capture = useMediaCapture(widgetRef, 'RootCauseOnFeiGoals'); const [showTabularData, setShowTabularData] = useState(false); @@ -87,8 +87,6 @@ export default function RootCauseFeiGoals({ data }) { totalNumberOfRootCauses, } = data; - console.log('records>>>>>', records); - const tableData = []; const traceData = []; @@ -137,7 +135,7 @@ export default function RootCauseFeiGoals({ data }) {
Date: Thu, 10 Oct 2024 15:04:30 -0400 Subject: [PATCH 07/28] hook up reciepents without tta --- frontend/src/fetchers/ssdi.js | 3 - .../RecipientsWithNoTta/__tests__/index.js | 110 +++++++++--------- .../QADashboard/RecipientsWithNoTta/index.js | 43 ++++++- .../src/widgets/RecipientsWithNoTtaWidget.js | 52 +++++---- .../__tests__/RecipientsWithNoTtaWidget.js | 71 ++++++----- src/routes/ssdi/handlers.ts | 10 +- 6 files changed, 168 insertions(+), 121 deletions(-) diff --git a/frontend/src/fetchers/ssdi.js b/frontend/src/fetchers/ssdi.js index 92a46102d9..44db3ea050 100644 --- a/frontend/src/fetchers/ssdi.js +++ b/frontend/src/fetchers/ssdi.js @@ -19,7 +19,6 @@ const allowedTopicsForQuery = { 'programType', 'stateCode', 'region', - 'regionIds', 'startDate', 'endDate', 'dataSetSelection', @@ -30,7 +29,6 @@ const allowedTopicsForQuery = { 'programType', 'stateCode', 'region', - 'regionIds', 'group', 'createDate', 'reason', @@ -41,7 +39,6 @@ const allowedTopicsForQuery = { 'programType', 'stateCode', 'region', - 'regionIds', 'domainEmotionalSupport', 'domainClassroomOrganization', 'domainInstructionalSupport', diff --git a/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js index 8a5bde6db3..e3d2ada256 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js @@ -24,66 +24,62 @@ const defaultUser = { }], }; -const RecipientsWithNoTtaDataEmpty = { - headers: ['Date of Last TTA', 'Days Since Last TTA'], - RecipientsWithNoTta: [], -}; - -const RecipientsWithNoTtaData = { - headers: ['Date of Last TTA', 'Days Since Last TTA'], - RecipientsWithNoTta: [ - { - id: 1, - heading: 'Test Recipient 1', - name: 'Test Recipient 1', - recipient: 'Test Recipient 1', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Date_of_Last_TTA', - value: '2021-09-01', +const RecipientsWithNoTtaDataEmpty = [ + { + data_set: 'no_tta_widget', + records: '1', + data: [ + { + total: 0, + 'recipients without tta': 0, + '% recipients without tta': 0, }, + ], + }, + { + data_set: 'no_tta_page', + records: '0', + data: [], + }, +]; + +const RecipientsWithNoTtaDataSSdi = [ + { + data_set: 'no_tta_widget', + records: '1', + data: [ { - title: 'Days_Since_Last_TTA', - value: '90', - }], - }, - { - id: 2, - heading: 'Test Recipient 2', - name: 'Test Recipient 2', - recipient: 'Test Recipient 2', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Date_of_Last_TTA', - value: '2021-09-02', + total: 1460, + 'recipients without tta': 794, + '% recipients without tta': 54.38, }, + ], + }, + { + data_set: 'no_tta_page', + records: '799', + data: [ { - title: 'Days_Since_Last_TTA', - value: '91', - }], - }, - { - id: 3, - heading: 'Test Recipient 3', - name: 'Test Recipient 3', - recipient: 'Test Recipient 3', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Date_of_Last_TTA', - value: '2021-09-03', + 'recipient id': 1, + 'recipient name': 'Test Recipient 1', + 'last tta': '2021-09-01', + 'days since last tta': 90, }, { - title: 'Days_Since_Last_TTA', - value: '92', - }], - }], -}; + 'recipient id': 2, + 'recipient name': 'Test Recipient 2', + 'last tta': '2021-09-02', + 'days since last tta': 91, + }, + { + 'recipient id': 3, + 'recipient name': 'Test Recipient 3', + 'last tta': '2021-09-03', + 'days since last tta': 92, + }, + ], + }, +]; const renderRecipientsWithNoTta = (user = defaultUser) => { render( @@ -99,14 +95,14 @@ describe('Recipients With Ohs Standard Fei Goal', () => { afterEach(() => fetchMock.restore()); it('renders correctly without data', async () => { - fetchMock.get('/api/ssdi/api/dashboards/qa/no-tta.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=no_tta_widget', RecipientsWithNoTtaDataEmpty); + fetchMock.get('/api/ssdi/api/dashboards/qa/no-tta.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=no_tta_widget&dataSetSelection[]=no_tta_page', RecipientsWithNoTtaDataEmpty); renderRecipientsWithNoTta(); expect(screen.queryAllByText(/recipients with no tta/i).length).toBe(2); expect(screen.getByText(/Recipients without Activity Reports or Training Reports for more than 90 days./i)).toBeInTheDocument(); }); it('renders correctly with data', async () => { - fetchMock.get('/api/ssdi/api/dashboards/qa/no-tta.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=no_tta_widget', RecipientsWithNoTtaData); + fetchMock.get('/api/ssdi/api/dashboards/qa/no-tta.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=no_tta_widget&dataSetSelection[]=no_tta_page', RecipientsWithNoTtaDataSSdi); renderRecipientsWithNoTta(); expect(screen.queryAllByText(/recipients with no tta/i).length).toBe(2); expect(screen.getByText(/Recipients without Activity Reports or Training Reports for more than 90 days./i)).toBeInTheDocument(); @@ -129,7 +125,7 @@ describe('Recipients With Ohs Standard Fei Goal', () => { }); it('handles a user with only one region', async () => { - fetchMock.get('/api/ssdi/api/dashboards/qa/no-tta.sql?&dataSetSelection[]=no_tta_widget', RecipientsWithNoTtaData); + fetchMock.get('/api/ssdi/api/dashboards/qa/no-tta.sql?region.in[]=2&dataSetSelection[]=no_tta_widget&dataSetSelection[]=no_tta_page', RecipientsWithNoTtaDataSSdi); const u = { homeRegionId: 14, permissions: [{ diff --git a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js index 71178687ae..7aa74023e0 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js @@ -61,9 +61,48 @@ export default function RecipientsWithNoTta() { const data = await getSelfServiceData( 'recipients-with-no-tta', filters, - ['no_tta_widget'], + ['no_tta_widget', 'no_tta_page'], ); - setRecipientsWithNoTTA(data); + + const pageData = data.filter((d) => d.data_set === 'no_tta_page'); + const widgetData = data.filter((d) => d.data_set === 'no_tta_widget'); + + // Format the recipient data for the generic widget. + let formattedRecipientPageData = pageData[0].data.map((item) => { + const recipientId = item['recipient id']; + const recipientName = item['recipient name']; + const dateOfLastTta = item['last tta']; + const daysSinceLastTta = item['days since last tta']; + return { + id: recipientId, + heading: recipientName, + name: recipientName, + isURL: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', // TODO: Set to correct link. + data: [ + { + title: 'Date_of_Last_TTA', + value: dateOfLastTta, + }, + { + title: 'Days_Since_Last_TTA', + value: daysSinceLastTta, + }, + ], + }; + }); + + // Add headers. + formattedRecipientPageData = { + headers: ['Date of Last TTA', 'Days Since Last TTA'], + RecipientsWithNoTta: [...formattedRecipientPageData], + }; + + setRecipientsWithNoTTA({ + pageData: formattedRecipientPageData, + widgetData: widgetData[0].data[0], + }); updateError(''); } catch (e) { updateError('Unable to fetch QA data'); diff --git a/frontend/src/widgets/RecipientsWithNoTtaWidget.js b/frontend/src/widgets/RecipientsWithNoTtaWidget.js index 78c1499fd5..7d1379653c 100644 --- a/frontend/src/widgets/RecipientsWithNoTtaWidget.js +++ b/frontend/src/widgets/RecipientsWithNoTtaWidget.js @@ -17,7 +17,8 @@ function RecipientsWithNoTtaWidget({ resetPagination, setResetPagination, }) { - const [numberOfRecipientsPerPage, setNumberOfRecipientsPerPage] = useState([]); + const { pageData, widgetData } = data; + const [allRecipientData, setAllRecipientData] = useState([]); const [recipientCount, setRecipientCount] = useState(0); const [localLoading, setLocalLoading] = useState(false); const [recipientsPerPage, setRecipientsPerPage] = useState([]); @@ -31,12 +32,12 @@ function RecipientsWithNoTtaWidget({ exportRows, sortConfig, } = useWidgetPaging( - data.headers, + pageData ? pageData.headers : [], 'recipientsWithNoTta', defaultSortConfig, RECIPIENTS_WITH_NO_TTA_PER_PAGE, - numberOfRecipientsPerPage, - setNumberOfRecipientsPerPage, + allRecipientData, // data to use. + setAllRecipientData, resetPagination, setResetPagination, loading, @@ -52,17 +53,17 @@ function RecipientsWithNoTtaWidget({ try { // Set local data. setLocalLoading(true); - const recipientToUse = data.RecipientsWithNoTta || []; - setNumberOfRecipientsPerPage(recipientToUse); - setRecipientCount(recipientToUse.length); + setAllRecipientData(pageData ? pageData.RecipientsWithNoTta : []); // TODO: Put this back. + setRecipientCount(widgetData ? widgetData['recipients without tta'] : 0); } finally { setLocalLoading(false); } }, [data]); const getSubtitleWithPct = () => { - const totalRecipients = 159; - return `${recipientCount} of ${totalRecipients} (${((recipientCount / totalRecipients) * 100).toFixed(2)}%) recipients`; + const totalRecipients = widgetData ? widgetData.total : 0; + const pct = widgetData ? widgetData['% recipients without tta'] : 0; + return `${recipientCount} of ${totalRecipients} (${pct}%) recipients`; }; const menuItems = [ @@ -96,7 +97,7 @@ function RecipientsWithNoTtaWidget({ menuItems={menuItems} > { it('renders correctly with data', async () => { const data = { - headers: ['Date of Last TTA', 'Days Since Last TTA'], - RecipientsWithNoTta: [ - { - heading: 'Test Recipient 1', - name: 'Test Recipient 1', - recipient: 'Test Recipient 1', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Date of Last TTA', - value: '2021-09-01', - }, + widgetData: { + total: 1460, + 'recipients without tta': 794, + '% recipients without tta': 54.38, + }, + pageData: { + headers: ['Date of Last TTA', 'Days Since Last TTA'], + RecipientsWithNoTta: [ { - title: 'Days Since Last TTA', - value: '90', - }], - }, - { - heading: 'Test Recipient 2', - name: 'Test Recipient 2', - recipient: 'Test Recipient 2', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Date of Last TTA', - value: '2021-09-02', + heading: 'Test Recipient 1', + name: 'Test Recipient 1', + recipient: 'Test Recipient 1', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Date of Last TTA', + value: '2021-09-01', + }, + { + title: 'Days Since Last TTA', + value: '90', + }], }, { - title: 'Days Since Last TTA', - value: '91', - }], - }, - ], + heading: 'Test Recipient 2', + name: 'Test Recipient 2', + recipient: 'Test Recipient 2', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Date of Last TTA', + value: '2021-09-02', + }, + { + title: 'Days Since Last TTA', + value: '91', + }], + }, + ], + }, }; rendersRecipientsWithNoTta(data); diff --git a/src/routes/ssdi/handlers.ts b/src/routes/ssdi/handlers.ts index 991ac22136..7a68f242ef 100644 --- a/src/routes/ssdi/handlers.ts +++ b/src/routes/ssdi/handlers.ts @@ -159,16 +159,16 @@ const runQuery = async (req: Request, res: Response) => { const policy = new User(user); // Handle regionIds with policy filtering - if (filterValues.region && Array.isArray(filterValues.region)) { - filterValues.region = filterValues.region + if (filterValues['region.in'] && Array.isArray(filterValues['region.in'])) { + filterValues['region.in'] = filterValues['region.in'] .map(Number) .filter((num) => !Number.isNaN(num)); - filterValues.region = policy.filterRegions(filterValues.region); + filterValues['region.in'] = policy.filterRegions(filterValues['region.in']); } else { - filterValues.region = policy.getAllAccessibleRegions(); + filterValues['region.in'] = policy.getAllAccessibleRegions(); } - if (!filterValues.region || filterValues.region.length === 0) { + if (!filterValues['region.in'] || filterValues['region.in'].length === 0) { res.status(401).json({ error: 'Access forbidden: User has no region access configured.' }); return; } From 8a1f13c27e3cad825fb924a69fee03b38420b3c1 Mon Sep 17 00:00:00 2001 From: GarrettEHill Date: Thu, 10 Oct 2024 13:37:26 -0700 Subject: [PATCH 08/28] Add number of grants with FEI to widget --- src/queries/api/dashboards/qa/fei.sql | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/queries/api/dashboards/qa/fei.sql b/src/queries/api/dashboards/qa/fei.sql index 8d908d7714..116ea12348 100644 --- a/src/queries/api/dashboards/qa/fei.sql +++ b/src/queries/api/dashboards/qa/fei.sql @@ -56,6 +56,12 @@ JSON: { "type": "number", "nullable": false, "description": "Total number of recipients." + }, + { + "columnName": "grants with fei", + "type": "number", + "nullable": false, + "description": "Number of grants with a FEI goal." } ] }, @@ -554,7 +560,8 @@ WITH with_fei AS ( SELECT r.id, - COUNT(DISTINCT fg.id) FILTER (WHERE COALESCE(g."goalTemplateId",0) = 19017) > 0 has_fei + COUNT(DISTINCT fg.id) FILTER (WHERE COALESCE(g."goalTemplateId",0) = 19017) > 0 has_fei, + COUNT(DISTINCT gr.id) FILTER (WHERE COALESCE(g."goalTemplateId",0) = 19017) grant_count FROM "Recipients" r JOIN "Grants" gr ON r.id = gr."recipientId" @@ -572,7 +579,8 @@ WITH (((COUNT(DISTINCT wf.id) FILTER (WHERE has_fei)::decimal/ COUNT(DISTINCT wf.id)))*100)::decimal(5,2) "% recipients with fei", COUNT(DISTINCT wf.id) FILTER (WHERE wf.has_fei) "recipients with fei", - COUNT(DISTINCT wf.id) total + COUNT(DISTINCT wf.id) total, + SUM(grant_count) "grants with fei" FROM with_fei wf ), @@ -632,7 +640,8 @@ WITH JSONB_AGG(JSONB_BUILD_OBJECT( '% recipients with fei', "% recipients with fei", 'recipients with fei', "recipients with fei", - 'total', total + 'total', total, + 'grants with fei', "grants with fei" )) AS data, af.active_filters FROM with_fei_widget From 988f57cc42f9bcc4ae4ca171579ff2c3a6b78164 Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Thu, 10 Oct 2024 17:34:26 -0400 Subject: [PATCH 09/28] hook up fei page --- .../QADashboard/RecipientsWithNoTta/index.js | 1 + .../__tests__/index.js | 141 +++++++---------- .../RecipientsWithOhsStandardFeiGoal/index.js | 56 ++++++- .../src/widgets/RecipientsWithNoTtaWidget.js | 2 +- .../RecipientsWithOhsStandardFeiGoalWidget.js | 66 ++++---- .../RecipientsWithOhsStandardFeiGoalWidget.js | 146 +++++++++--------- 6 files changed, 231 insertions(+), 181 deletions(-) diff --git a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js index 7aa74023e0..d2548bb804 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js @@ -64,6 +64,7 @@ export default function RecipientsWithNoTta() { ['no_tta_widget', 'no_tta_page'], ); + // Get summary and row data. const pageData = data.filter((d) => d.data_set === 'no_tta_page'); const widgetData = data.filter((d) => d.data_set === 'no_tta_widget'); diff --git a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js index fffc78ddc6..f07e88f85c 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js @@ -1,6 +1,5 @@ import '@testing-library/jest-dom'; import React from 'react'; -import moment from 'moment'; import fetchMock from 'fetch-mock'; import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; @@ -25,91 +24,73 @@ const defaultUser = { }], }; -const recipientsWithOhsStandardFeiGoalEmptyData = { - headers: ['Recipient', 'Date of Last TTA', 'Days Since Last TTA'], - RecipientsWithNoTta: [], -}; - -const recipientsWithOhsStandardFeiGoalData = { - headers: ['Goal created on', 'Goal number', 'Goal status', 'Root cause'], - RecipientsWithOhsStandardFeiGoal: [ - { - id: 1, - heading: 'Test Recipient 1', - name: 'Test Recipient 1', - recipient: 'Test Recipient 1', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: moment('2021-09-01').format('MM/DD/YYYY'), - }, - { - title: 'Goal_number', - value: 'G-20628', - }, - { - title: 'Goal_status', - value: 'In progress', - }, - { - title: 'Root_cause', - value: 'Community Partnership, Workforce', - }, - ], - }, - { - id: 2, - heading: 'Test Recipient 2', - name: 'Test Recipient 2', - recipient: 'Test Recipient 2', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: moment('2021-09-02').format('MM/DD/YYYY'), - }, +const recipientsWithOhsStandardFeiGoalEmptyData = [ + { + data_set: 'with_fei_widget', + records: '1', + data: [ { - title: 'Goal_number', - value: 'G-359813', + total: 1550, + '% recipients with fei': 0, + 'grants with fei': 0, + 'recipients with fei': 0, }, + ], + }, + { + data_set: 'with_fei_page', + records: '0', + data: [], + }, +]; + +const recipientsWithOhsStandardFeiGoalSsdiData = [ + { + data_set: 'with_fei_widget', + records: '1', + data: [ { - title: 'Goal_status', - value: 'Not started', + total: 1550, + '% recipients with fei': 55.35, + 'grants with fei': 1093, + 'recipients with fei': 858, }, + ], + }, + { + data_set: 'with_fei_page', + records: '799', + data: [ { - title: 'Root_cause', - value: 'Testing', - }], - }, - { - id: 3, - heading: 'Test Recipient 3', - name: 'Test Recipient 3', - recipient: 'Test Recipient 3', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: moment('2021-09-03').format('MM/DD/YYYY'), + recipientId: 1, + recipientName: 'Test Recipient 1', + createdAt: '2021-09-01T13:05:17.944+00:00', + goalId: 20628, + goalStatus: 'In progress', + grantNumber: '234234', + rootCause: ['Community Partnership', 'Workforce'], }, { - title: 'Goal_number', - value: 'G-457825', + recipientId: 2, + recipientName: 'Test Recipient 2', + createdAt: '2021-09-02T13:05:17.944+00:00', + goalId: 359813, + goalStatus: 'Not started', + grantNumber: '234234', + rootCause: ['Testing'], }, { - title: 'Goal_status', - value: 'In progress', + recipientId: 3, + recipientName: 'Test Recipient 3', + createdAt: '2021-09-03T13:05:17.944+00:00', + goalId: 457825, + goalStatus: 'In progress', + grantNumber: '234234', + rootCause: ['Facilities'], }, - { - title: 'Root_cause', - value: 'Facilities', - }], - }], -}; + ], + }, +]; const renderRecipientsWithOhsStandardFeiGoal = (user = defaultUser) => { render( @@ -125,9 +106,8 @@ describe('Recipients With Ohs Standard Fei Goal', () => { afterEach(() => { fetchMock.restore(); }); - it('renders correctly without data', async () => { - fetchMock.get('/api/ssdi/api/dashboards/qa/fei.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_graph', recipientsWithOhsStandardFeiGoalEmptyData); + fetchMock.get('/api/ssdi/api/dashboards/qa/fei.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_page', recipientsWithOhsStandardFeiGoalEmptyData); renderRecipientsWithOhsStandardFeiGoal(); expect(screen.queryAllByRole('heading', { name: /recipients with ohs standard fei goal/i }).length).toBe(1); @@ -135,7 +115,7 @@ describe('Recipients With Ohs Standard Fei Goal', () => { }); it('renders correctly with data', async () => { - fetchMock.get('/api/ssdi/api/dashboards/qa/fei.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_graph', recipientsWithOhsStandardFeiGoalData); + fetchMock.get('/api/ssdi/api/dashboards/qa/fei.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_page', recipientsWithOhsStandardFeiGoalSsdiData); renderRecipientsWithOhsStandardFeiGoal(); expect(screen.queryAllByRole('heading', { name: /recipients with ohs standard fei goal/i }).length).toBe(1); @@ -156,7 +136,6 @@ describe('Recipients With Ohs Standard Fei Goal', () => { expect(screen.queryAllByText(/In progress/i).length).toBe(2); expect(screen.getByText(/Not started/i)).toBeInTheDocument(); - expect(screen.getByText(/Community Partnership, Workforce/i)).toBeInTheDocument(); expect(screen.getByText(/Testing/i)).toBeInTheDocument(); expect(screen.getByText(/Facilities/i)).toBeInTheDocument(); @@ -172,7 +151,7 @@ describe('Recipients With Ohs Standard Fei Goal', () => { scopeId: SCOPE_IDS.READ_ACTIVITY_REPORTS, }], }; - fetchMock.get('/api/ssdi/api/dashboards/qa/fei.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_graph', recipientsWithOhsStandardFeiGoalData); + fetchMock.get('/api/ssdi/api/dashboards/qa/fei.sql?region.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_page', recipientsWithOhsStandardFeiGoalSsdiData); renderRecipientsWithOhsStandardFeiGoal(u); expect(screen.queryAllByRole('heading', { name: /recipients with ohs standard fei goal/i }).length).toBe(1); diff --git a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js index bbfd322401..79cdcb9a18 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js @@ -3,6 +3,7 @@ import React, { useRef, useContext, } from 'react'; +import moment from 'moment'; import { Link } from 'react-router-dom'; import useDeepCompareEffect from 'use-deep-compare-effect'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -63,9 +64,60 @@ export default function RecipientsWithOhsStandardFeiGoal() { const data = await getSelfServiceData( 'recipients-with-ohs-standard-fei-goal', filters, - ['with_fei_widget', 'with_fei_graph'], + ['with_fei_widget', 'with_fei_page'], ); - setRecipientsWithOhsStandardFeiGoal(data); + + // Get summary and row data. + const pageData = data.filter((d) => d.data_set === 'with_fei_page'); + const widgetData = data.filter((d) => d.data_set === 'with_fei_widget'); + + // Convert data to format that widget expects. + let formattedRecipientPageData = pageData[0].data.map((item) => { + const { recipientId } = item; + const { recipientName } = item; + const { goalId } = item; + const { goalStatus } = item; + // const { grantNumber } = item; + const { createdAt } = item; + const { rootCause } = item; + return { + id: recipientId, + heading: recipientName, + name: recipientName, + isURL: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', // TODO: Set to correct link. + data: [ + { + title: 'Goal_created_on', + value: moment(createdAt).format('MM/DD/YYYY'), + }, + { + title: 'Goal_number', + value: `G-${goalId}`, + }, + { + title: 'Goal_status', + value: goalStatus, + }, + { + title: 'Root_cause', + value: rootCause && rootCause.length ? rootCause.join(', ') : '', // Convert array to string. + }, + ], + }; + }); + + // Add headers. + formattedRecipientPageData = { + headers: ['Goal created on', 'Goal number', 'Goal status', 'Root cause'], + RecipientsWithOhsStandardFeiGoal: [...formattedRecipientPageData], + }; + + setRecipientsWithOhsStandardFeiGoal({ + pageData: formattedRecipientPageData, + widgetData: widgetData[0].data[0], + }); updateError(''); } catch (e) { updateError('Unable to fetch QA data'); diff --git a/frontend/src/widgets/RecipientsWithNoTtaWidget.js b/frontend/src/widgets/RecipientsWithNoTtaWidget.js index 7d1379653c..89a0b23e61 100644 --- a/frontend/src/widgets/RecipientsWithNoTtaWidget.js +++ b/frontend/src/widgets/RecipientsWithNoTtaWidget.js @@ -58,7 +58,7 @@ function RecipientsWithNoTtaWidget({ } finally { setLocalLoading(false); } - }, [data]); + }, [pageData, widgetData]); const getSubtitleWithPct = () => { const totalRecipients = widgetData ? widgetData.total : 0; diff --git a/frontend/src/widgets/RecipientsWithOhsStandardFeiGoalWidget.js b/frontend/src/widgets/RecipientsWithOhsStandardFeiGoalWidget.js index 40a5c227c2..38c0e6fd66 100644 --- a/frontend/src/widgets/RecipientsWithOhsStandardFeiGoalWidget.js +++ b/frontend/src/widgets/RecipientsWithOhsStandardFeiGoalWidget.js @@ -14,13 +14,14 @@ function RecipientsWithOhsStandardFeiGoalWidget({ resetPagination, setResetPagination, }) { + const { pageData, widgetData } = data; const defaultSortConfig = { sortBy: '1', direction: 'desc', activePage: 1, }; - const [numberOfRecipientsPerPage, setNumberOfRecipientsPerPage] = useState([]); + const [recipientDataToUse, setRecipientDataToUse] = useState([]); const [recipientCount, setRecipientCount] = useState(0); const [localLoading, setLocalLoading] = useState(false); const [recipientsPerPage, setRecipientsPerPage] = useState([]); @@ -37,20 +38,20 @@ function RecipientsWithOhsStandardFeiGoalWidget({ exportRows, sortConfig, } = useWidgetPaging( - data.headers, + pageData ? pageData.headers : [], 'recipientsWithOhsStandardFeiGoal', defaultSortConfig, RECIPIENTS_WITH_OHS_STANDARD_FEI_GOAL_PER_PAGE, - numberOfRecipientsPerPage, - setNumberOfRecipientsPerPage, + recipientDataToUse, // Data to use. + setRecipientDataToUse, resetPagination, setResetPagination, loading, checkBoxes, 'RecipientsWithOhsStandardFeiGoal', setRecipientsPerPage, - ['Recipient', 'Goal_number', 'Goal_status', 'Root_cause'], - ['Goal_created_on'], + ['Recipient', 'Goal number', 'Goal status', 'Root cause'], + ['Goal created on'], 'recipientsWithOhsStandardFeiGoal.csv', ); @@ -58,18 +59,19 @@ function RecipientsWithOhsStandardFeiGoalWidget({ try { // Set local data. setLocalLoading(true); - const recipientToUse = data.RecipientsWithOhsStandardFeiGoal || []; - setNumberOfRecipientsPerPage(recipientToUse); - setRecipientCount(recipientToUse.length); + const recipientToUse = pageData ? pageData.RecipientsWithOhsStandardFeiGoal : []; + setRecipientDataToUse(recipientToUse); + setRecipientCount(widgetData ? widgetData['recipients with fei'] : 0); } finally { setLocalLoading(false); } - }, [data]); + }, [pageData, widgetData]); - const numberOfGrants = 70; + const numberOfGrants = widgetData ? widgetData['grants with fei'] : 0; const getSubtitleWithPct = () => { - const totalRecipients = 159; - return `${recipientCount} of ${totalRecipients} (${((recipientCount / totalRecipients) * 100).toFixed(2)}%) recipients (${numberOfGrants} grants)`; + const totalRecipients = widgetData ? widgetData.total : 0; + const pct = widgetData ? widgetData['% recipients with fei'] : 0; + return `${recipientCount} of ${totalRecipients} (${pct}%) recipients (${numberOfGrants} grants)`; }; const menuItems = [ @@ -137,7 +139,7 @@ function RecipientsWithOhsStandardFeiGoalWidget({ exportRows={exportRows} > { it('renders correctly with data', async () => { const data = { - headers: ['Goal created on', 'Goal number', 'Goal status', 'Root cause'], - RecipientsWithOhsStandardFeiGoal: [ - { - heading: 'Test Recipient 1', - name: 'Test Recipient 1', - recipient: 'Test Recipient 1', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: moment('2021-09-01').format('MM/DD/YYYY'), - }, - { - title: 'Goal_number', - value: 'G-20628', - }, - { - title: 'Goal_status', - value: 'In progress', - }, - { - title: 'Root_cause', - value: 'Community Partnership, Workforce', - }, - ], - }, - { - heading: 'Test Recipient 2', - name: 'Test Recipient 2', - recipient: 'Test Recipient 2', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: moment('2021-09-02').format('MM/DD/YYYY'), - }, - { - title: 'Goal_number', - value: 'G-359813', - }, - { - title: 'Goal_status', - value: 'Not started', - }, - { - title: 'Root_cause', - value: 'Testing', - }], - }, - { - heading: 'Test Recipient 3', - name: 'Test Recipient 3', - recipient: 'Test Recipient 3', - isUrl: true, - hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', - data: [{ - title: 'Goal_created_on', - value: moment('2021-09-03').format('MM/DD/YYYY'), - }, + widgetData: { + total: 1550, + '% recipients with fei': 55.35, + 'grants with fei': 1093, + 'recipients with fei': 858, + }, + pageData: { + headers: ['Goal created on', 'Goal number', 'Goal status', 'Root cause'], + RecipientsWithOhsStandardFeiGoal: [ { - title: 'Goal_number', - value: 'G-457825', + heading: 'Recipient 1', + name: 'Test Recipient 1', + recipient: 'Test Recipient 1', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Goal_created_on', + value: moment('2021-09-01').format('MM/DD/YYYY'), + }, + { + title: 'Goal_number', + value: 'G-20628', + }, + { + title: 'Goal_status', + value: 'In progress', + }, + { + title: 'Root_cause', + value: 'Community Partnership, Workforce', + }, + ], }, { - title: 'Goal_status', - value: 'Unavailable', + heading: 'Test Recipient 2', + name: 'Test Recipient 2', + recipient: 'Test Recipient 2', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Goal_created_on', + value: moment('2021-09-02').format('MM/DD/YYYY'), + }, + { + title: 'Goal_number', + value: 'G-359813', + }, + { + title: 'Goal_status', + value: 'Not started', + }, + { + title: 'Root_cause', + value: 'Testing', + }], }, { - title: 'Root_cause', - value: 'Facilities', + heading: 'Test Recipient 3', + name: 'Test Recipient 3', + recipient: 'Test Recipient 3', + isUrl: true, + hideLinkIcon: true, + link: '/recipient-tta-records/376/region/1/profile', + data: [{ + title: 'Goal_created_on', + value: moment('2021-09-03').format('MM/DD/YYYY'), + }, + { + title: 'Goal_number', + value: 'G-457825', + }, + { + title: 'Goal_status', + value: 'Unavailable', + }, + { + title: 'Root_cause', + value: 'Facilities', + }], }], - }], + }, }; renderRecipientsWithOhsStandardFeiGoalWidget(data); From a9d6168dd3c104de8d6051f9e6ff3c217cf1728f Mon Sep 17 00:00:00 2001 From: GarrettEHill Date: Fri, 11 Oct 2024 09:50:04 -0700 Subject: [PATCH 10/28] adding creator and collaborators to the with_class_page --- src/queries/api/dashboards/qa/class.sql | 61 +++++++++++++++++++------ 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/src/queries/api/dashboards/qa/class.sql b/src/queries/api/dashboards/qa/class.sql index e08b3bb791..ad579a2a93 100644 --- a/src/queries/api/dashboards/qa/class.sql +++ b/src/queries/api/dashboards/qa/class.sql @@ -129,6 +129,18 @@ JSON: { "type": "date", "nullable": true, "description": "Date when the monitoring report was delivered." + }, + { + "columnName": "creator", + "type": "string", + "nullable": true, + "description": "User who created the goal" + }, + { + "columnName": "colaborators", + "type": "string[]", + "nullable": true, + "description": "Users who collaborated on the goal" } ] }, @@ -729,17 +741,19 @@ WITH ), with_class_page AS ( SELECT - r.id "recipientId", - r.name "recipientName", - gr.number "grantNumber", - (ARRAY_AGG(g.id ORDER BY g.id DESC))[1] "goalId", - (ARRAY_AGG(g."createdAt" ORDER BY g.id DESC))[1] "goalCreatedAt", - (ARRAY_AGG(g.status ORDER BY g.id DESC))[1] "goalStatus", - (ARRAY_AGG(a."startDate" ORDER BY a."startDate" DESC))[1] "lastARStartDate", - (ARRAY_AGG(mcs."emotionalSupport" ORDER BY mr."reportDeliveryDate" DESC) FILTER (WHERE mr.id IS NOT NULL))[1] "emotionalSupport", - (ARRAY_AGG(mcs."classroomOrganization" ORDER BY mr."reportDeliveryDate" DESC) FILTER (WHERE mr.id IS NOT NULL))[1] "classroomOrganization", - (ARRAY_AGG(mcs."instructionalSupport" ORDER BY mr."reportDeliveryDate" DESC) FILTER (WHERE mr.id IS NOT NULL))[1] "instructionalSupport", - (ARRAY_AGG(mr."reportDeliveryDate" ORDER BY mr."reportDeliveryDate" DESC) FILTER (WHERE mr.id IS NOT NULL))[1] "reportDeliveryDate" + r.id "recipientId", + r.name "recipientName", + gr.number "grantNumber", + (ARRAY_AGG(g.id ORDER BY g.id DESC))[1] "goalId", + (ARRAY_AGG(g."createdAt" ORDER BY g.id DESC))[1] "goalCreatedAt", + (ARRAY_AGG(g.status ORDER BY g.id DESC))[1] "goalStatus", + (ARRAY_AGG(a."startDate" ORDER BY a."startDate" DESC))[1] "lastARStartDate", + (ARRAY_AGG(mcs."emotionalSupport" ORDER BY mr."reportDeliveryDate" DESC) FILTER (WHERE mr.id IS NOT NULL))[1] "emotionalSupport", + (ARRAY_AGG(mcs."classroomOrganization" ORDER BY mr."reportDeliveryDate" DESC) FILTER (WHERE mr.id IS NOT NULL))[1] "classroomOrganization", + (ARRAY_AGG(mcs."instructionalSupport" ORDER BY mr."reportDeliveryDate" DESC) FILTER (WHERE mr.id IS NOT NULL))[1] "instructionalSupport", + (ARRAY_AGG(mr."reportDeliveryDate" ORDER BY mr."reportDeliveryDate" DESC) FILTER (WHERE mr.id IS NOT NULL))[1] "reportDeliveryDate", + (ARRAY_AGG(DISTINCT u.name || ', ' || COALESCE(ur.agg_roles, 'No Roles')) FILTER (WHERE ct.name = 'Creator'))[1] "creator", + (ARRAY_AGG(DISTINCT u.name || ', ' || COALESCE(ur.agg_roles, 'No Roles')) FILTER (WHERE ct.name = 'Collaborator')) "collaborators" FROM with_class wc JOIN "Recipients" r ON wc.id = r.id @@ -752,6 +766,23 @@ WITH ON gr.id = g."grantId" AND has_class AND g."goalTemplateId" = 18172 + LEFT JOIN "GoalCollaborators" gc + ON g.id = gc."goalId" + LEFT JOIN "CollaboratorTypes" ct + ON gc."collaboratorTypeId" = ct.id + AND ct.name IN ('Creator', 'Collaborator') + JOIN "ValidFor" vf + ON ct."validForId" = vf.id + AND vf.name = 'Goals' + JOIN "Users" u + ON gc."userId" = u.id + LEFT JOIN LATERAL ( + SELECT ur."userId", STRING_AGG(r.name, ', ') AS agg_roles + FROM "UserRoles" ur + JOIN "Roles" r ON ur."roleId" = r.id + WHERE ur."userId" = u.id + GROUP BY ur."userId" + ) ur ON u.id = ur."userId" LEFT JOIN "ActivityReportGoals" arg ON g.id = arg."goalId" LEFT JOIN "ActivityReports" a @@ -770,8 +801,8 @@ WITH AND (has_class OR has_scores) AND (g.id IS NOT NULL OR mcs.id IS NOT NULL) AND (mrs.id IS NULL OR mrs.name = 'Complete') - GROUP BY 1,2,3 - ORDER BY 1,3 + GROUP BY 1, 2, 3 + ORDER BY 1, 3 ), -- CTE for fetching active filters using NULLIF() to handle empty strings @@ -820,7 +851,9 @@ WITH 'emotionalSupport', "emotionalSupport", 'classroomOrganization', "classroomOrganization", 'instructionalSupport', "instructionalSupport", - 'reportDeliveryDate', "reportDeliveryDate" + 'reportDeliveryDate', "reportDeliveryDate", + 'creator', "creator", + 'collaborators', "collaborators" )) AS data, af.active_filters -- Use precomputed active_filters FROM with_class_page From ffcd8f234be9ffff16ab38782895125fe360b7eb Mon Sep 17 00:00:00 2001 From: GarrettEHill Date: Fri, 11 Oct 2024 11:43:03 -0700 Subject: [PATCH 11/28] add grant count to class widget data --- src/queries/api/dashboards/qa/class.sql | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/queries/api/dashboards/qa/class.sql b/src/queries/api/dashboards/qa/class.sql index ad579a2a93..d880d6deb0 100644 --- a/src/queries/api/dashboards/qa/class.sql +++ b/src/queries/api/dashboards/qa/class.sql @@ -56,6 +56,12 @@ JSON: { "type": "number", "nullable": false, "description": "Total number of recipients." + }, + { + "columnName": "grants with fei", + "type": "number", + "nullable": false, + "description": "Number of grants with a FEI goal." } ] }, @@ -711,7 +717,8 @@ WITH SELECT r.id, COUNT(DISTINCT g.id) FILTER (WHERE COALESCE(g."goalTemplateId",0) = 18172) > 0 has_class, - COUNT(DISTINCT mcs.id) > 0 has_scores + COUNT(DISTINCT mcs.id) > 0 has_scores, + COUNT(DISTINCT gr.id) FILTER (WHERE COALESCE(g."goalTemplateId",0) = 18172) grant_count FROM "Recipients" r JOIN "Grants" gr ON r.id = gr."recipientId" @@ -736,7 +743,8 @@ WITH (((COUNT(DISTINCT wc.id) FILTER (WHERE has_class)::decimal/ COUNT(DISTINCT wc.id)))*100)::decimal(5,2) "% recipients with class", COUNT(DISTINCT wc.id) FILTER (WHERE wc.has_class) "recipients with class", - COUNT(DISTINCT wc.id) total + COUNT(DISTINCT wc.id) total, + SUM(grant_count) "grants with class" FROM with_class wc ), with_class_page AS ( @@ -828,7 +836,8 @@ WITH JSONB_AGG(JSONB_BUILD_OBJECT( '% recipients with class', "% recipients with class", 'recipients with class', "recipients with class", - 'total', total + 'total', total, + 'grants with class', "grants with class" )) AS data, af.active_filters -- Use precomputed active_filters FROM with_class_widget From 57bc831ec8cfa5a19fd8e81eb047d1a6dd7d1cd5 Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Fri, 11 Oct 2024 17:16:00 -0400 Subject: [PATCH 12/28] hook up class --- .../QADashboard/Components/RecipientCard.js | 17 ++- .../Components/__tests__/RecipientCard.js | 4 +- .../__tests__/index.js | 114 ++++++++++-------- .../index.js | 70 ++++++++++- ...RecipientsWithClassScoresAndGoalsWidget.js | 25 ++-- ...RecipientsWithClassScoresAndGoalsWidget.js | 99 ++++++++------- 6 files changed, 214 insertions(+), 115 deletions(-) diff --git a/frontend/src/pages/QADashboard/Components/RecipientCard.js b/frontend/src/pages/QADashboard/Components/RecipientCard.js index 8a90b2197a..3be440c67d 100644 --- a/frontend/src/pages/QADashboard/Components/RecipientCard.js +++ b/frontend/src/pages/QADashboard/Components/RecipientCard.js @@ -1,10 +1,9 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Checkbox } from '@trussworks/react-uswds'; -// import { TRAINING_REPORT_STATUSES } from '@ttahub/common'; +import moment from 'moment'; import { v4 as uuidv4 } from 'uuid'; import { Link } from 'react-router-dom'; -import { checkForDate } from '../../../utils'; import ExpanderButton from '../../../components/ExpanderButton'; import GoalCard, { goalPropTypes } from './GoalCard'; import { getScoreBadge } from '../../../components/ClassScoreBadge'; @@ -52,33 +51,33 @@ function RecipientCard({

Last AR start date

- {checkForDate(recipient.lastArStartDate)} + {moment(recipient.lastARStartDate).format('MM/DD/YYYY')}

Emotional support

{recipient.emotionalSupport}

- {getScoreBadge('ES', recipient.emotionalSupport, recipient.reportReceivedDate, 'ttahub-recipient-card__recipient-column__badge')} + {getScoreBadge('ES', recipient.emotionalSupport, recipient.reportDeliveryDate, 'ttahub-recipient-card__recipient-column__badge')}

Classroom organization

{recipient.classroomOrganization}

- {getScoreBadge('CO', recipient.classroomOrganization, recipient.reportReceivedDate, 'ttahub-recipient-card__recipient-column__badge')} + {getScoreBadge('CO', recipient.classroomOrganization, recipient.reportDeliveryDate, 'ttahub-recipient-card__recipient-column__badge')}

Instructional support

{recipient.instructionalSupport}

- {getScoreBadge('IS', recipient.instructionalSupport, recipient.reportReceivedDate, 'ttahub-recipient-card__recipient-column__badge')} + {getScoreBadge('IS', recipient.instructionalSupport, recipient.reportDeliveryDate, 'ttahub-recipient-card__recipient-column__badge')}

Report received date

-

{checkForDate(recipient.reportReceivedDate)}

+

{moment(recipient.reportDeliveryDate, 'YYYY-MM-DD').format('MM/DD/YYYY')}

{ const history = createMemoryHistory(); diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js index 5774e11e30..c32a0d5bf1 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js @@ -68,9 +68,75 @@ export default function RecipientsWithClassScoresAndGoals() { const data = await getSelfServiceData( 'recipients-with-class-scores-and-goals', filters, - ['delivery_method_graph', 'role_graph'], + ['with_class_widget', 'with_class_page'], ); - setRecipientsWithClassScoresAndGoalsData(data); + + // Get summary and row data. + const pageData = data.filter((d) => d.data_set === 'with_class_page'); + const widgetData = data.filter((d) => d.data_set === 'with_class_widget'); + + // Convert data to the format the widget expects. + const reducedRecipientData = pageData[0].data.reduce((acc, item) => { + const { + recipientId, + recipientName, + classroomOrganization, + emotionalSupport, + goalCreatedAt, + goalId, + goalStatus, + grantNumber, + instructionalSupport, + lastARStartDate, + reportDeliveryDate, + collaborators, + creator, + } = item; + + // Check if recipientId is already in the accumulator. + const existingRecipient = acc.find((recipient) => recipient.id === recipientId); + if (existingRecipient) { + // Add goal info. + existingRecipient.goals.push({ + id: goalId, + goalNumber: `G-${goalId}`, + status: goalStatus, + creator, + collaborator: collaborators, + goalCreatedAt, + }); + return acc; + } + + // Else add a new recipient. + const newRecipient = { + id: recipientId, + name: recipientName, + emotionalSupport, + classroomOrganization, + instructionalSupport, + grantNumber, + goals: [ + { + id: goalId, + goalNumber: `G-${goalId}`, + status: goalStatus, + creator, + collaborator: collaborators, + goalCreatedAt, + }, + ], + lastARStartDate, + reportDeliveryDate, + }; + + return [...acc, newRecipient]; + }, []); + + setRecipientsWithClassScoresAndGoalsData({ + widgetData: widgetData[0].data[0], + pageData: reducedRecipientData, + }); updateError(''); } catch (e) { updateError('Unable to fetch QA data'); diff --git a/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js b/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js index 7d66ad2b93..47abd124f6 100644 --- a/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js +++ b/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js @@ -24,6 +24,7 @@ function RecipientsWithClassScoresAndGoalsWidget({ data, parentLoading, }) { + const { widgetData, pageData } = data; const titleDrawerRef = useRef(null); const subtitleRef = useRef(null); const [loading, setLoading] = useState(false); @@ -51,7 +52,7 @@ function RecipientsWithClassScoresAndGoalsWidget({ 'recipientsWithClassScoresAndGoals', defaultSort, RECIPIENTS_WITH_CLASS_SCORES_AND_GOALS_GOAL_PER_PAGE, - allRecipientsData, + allRecipientsData, // data to use. setAllRecipientsData, resetPagination, setResetPagination, @@ -81,10 +82,12 @@ function RecipientsWithClassScoresAndGoalsWidget({ // Handle sort by, not sure how we will handle this yet. }; - const numberOfGrants = 70; const getSubtitleWithPct = () => { - const totalRecipients = 159; - return `${allRecipientsData.length} of ${totalRecipients} (${((allRecipientsData.length / totalRecipients) * 100).toFixed(2)}%) recipients (${numberOfGrants} grants)`; + const totalRecipients = widgetData ? widgetData.total : 0; + const grants = widgetData ? widgetData['grants with class'] : 0; + const pct = widgetData ? widgetData['% recipients with class'] : 0; + const recipoientsWithClass = widgetData ? widgetData['recipients with class'] : 0; + return `${recipoientsWithClass} of ${totalRecipients} (${pct}%) recipients (${grants} grants)`; }; const makeRecipientCheckboxes = (goalsArr, checked) => ( @@ -119,12 +122,11 @@ function RecipientsWithClassScoresAndGoalsWidget({ try { // Set local data. setLoading(true); - const recipientToUse = data.RecipientsWithOhsStandardFeiGoal || []; - setAllRecipientsData(recipientToUse); + setAllRecipientsData(pageData || []); } finally { setLoading(false); } - }, [data.RecipientsWithOhsStandardFeiGoal]); + }, [pageData]); useEffect(() => { const recipientIds = allRecipientsData.map((g) => g.id); @@ -314,8 +316,13 @@ function RecipientsWithClassScoresAndGoalsWidget({ RecipientsWithClassScoresAndGoalsWidget.propTypes = { data: PropTypes.shape({ - headers: PropTypes.arrayOf(PropTypes.string), - RecipientsWithOhsStandardFeiGoal: PropTypes.oneOfType([ + widgetData: PropTypes.shape({ + total: PropTypes.number, + '% recipients with class': PropTypes.number, + 'recipients with class': PropTypes.number, + 'grants with class': PropTypes.number, + }), + pageData: PropTypes.oneOfType([ PropTypes.shape({ id: PropTypes.number, name: PropTypes.string, diff --git a/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js b/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js index 01038bc05e..d445aa58f9 100644 --- a/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js +++ b/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js @@ -6,26 +6,36 @@ import { createMemoryHistory } from 'history'; import RecipientsWithClassScoresAndGoalsWidget from '../RecipientsWithClassScoresAndGoalsWidget'; import UserContext from '../../UserContext'; -const recipient = { - id: 1, - name: 'Action for Boston Community Development, Inc.', - lastArStartDate: '01/02/2021', - emotionalSupport: 6.0430, - classroomOrganization: 5.0430, - instructionalSupport: 4.0430, - reportReceivedDate: '03/01/2022', - goals: [ +const recipientData = { + widgetData: { + '% recipients with class': 18.26, + 'grants with class': 346, + 'recipients with class': 283, + total: 1550, + }, + pageData: [ { - goalNumber: 'G-45641', - status: 'In progress', - creator: 'John Doe', - collaborator: 'Jane Doe', - }, - { - goalNumber: 'G-25858', - status: 'Suspended', - creator: 'Bill Smith', - collaborator: 'Bob Jones', + id: 1, + name: 'Action for Boston Community Development, Inc.', + lastARStartDate: '01/02/2021', + emotionalSupport: 6.0430, + classroomOrganization: 5.0430, + instructionalSupport: 4.0430, + reportDeliveryDate: '2022-03-01T04:00:00+00:00', + goals: [ + { + goalNumber: 'G-45641', + status: 'In progress', + creator: 'John Doe', + collaborator: 'Jane Doe', + }, + { + goalNumber: 'G-25858', + status: 'Suspended', + creator: 'Bill Smith', + collaborator: 'Bob Jones', + }, + ], }, ], }; @@ -46,48 +56,45 @@ const renderRecipientsWithClassScoresAndGoalsWidget = (data) => { describe('Recipients With Class and Scores and Goals Widget', () => { it('renders correctly without data', async () => { - const data = { - headers: [], - RecipientsWithOhsStandardFeiGoal: [], + const emptyData = { + widgetData: { + '% recipients with class': 0, + 'grants with class': 0, + 'recipients with class': 0, + total: 0, + }, + pageData: [], }; - renderRecipientsWithClassScoresAndGoalsWidget(data); + renderRecipientsWithClassScoresAndGoalsWidget(emptyData); expect(screen.getByText(/Recipients with CLASS® scores/i)).toBeInTheDocument(); expect(screen.getByText(/0-0 of 0/i)).toBeInTheDocument(); }); it('renders correctly with data', async () => { - const data = { - headers: ['Emotional Support', 'Classroom Organization', 'Instructional Support', 'Report Received Date', 'Goals'], - RecipientsWithOhsStandardFeiGoal: [ - { - ...recipient, - }, - ], - }; - renderRecipientsWithClassScoresAndGoalsWidget(data); + renderRecipientsWithClassScoresAndGoalsWidget(recipientData); expect(screen.getByText(/Recipients with CLASS® scores/i)).toBeInTheDocument(); expect(screen.getByText(/1-1 of 1/i)).toBeInTheDocument(); - expect(screen.getByText(recipient.name)).toBeInTheDocument(); - expect(screen.getByText(recipient.lastArStartDate)).toBeInTheDocument(); - expect(screen.getByText(recipient.emotionalSupport)).toBeInTheDocument(); - expect(screen.getByText(recipient.classroomOrganization)).toBeInTheDocument(); - expect(screen.getByText(recipient.instructionalSupport)).toBeInTheDocument(); - expect(screen.getByText(recipient.reportReceivedDate)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].name)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].lastARStartDate)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].emotionalSupport)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].classroomOrganization)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].instructionalSupport)).toBeInTheDocument(); + expect(screen.getByText('03/01/2022')).toBeInTheDocument(); // Expand the goals. const goalsButton = screen.getByRole('button', { name: /view goals for recipient action for boston community development, inc\./i }); expect(goalsButton).toBeInTheDocument(); goalsButton.click(); - expect(screen.getByText(recipient.goals[0].goalNumber)).toBeInTheDocument(); - expect(screen.getByText(recipient.goals[0].status)).toBeInTheDocument(); - expect(screen.getByText(recipient.goals[0].creator)).toBeInTheDocument(); - expect(screen.getByText(recipient.goals[0].collaborator)).toBeInTheDocument(); - expect(screen.getByText(recipient.goals[1].goalNumber)).toBeInTheDocument(); - expect(screen.getByText(recipient.goals[1].status)).toBeInTheDocument(); - expect(screen.getByText(recipient.goals[1].creator)).toBeInTheDocument(); - expect(screen.getByText(recipient.goals[1].collaborator)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].goals[0].goalNumber)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].goals[0].status)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].goals[0].creator)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].goals[0].collaborator)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].goals[1].goalNumber)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].goals[1].status)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].goals[1].creator)).toBeInTheDocument(); + expect(screen.getByText(recipientData.pageData[0].goals[1].collaborator)).toBeInTheDocument(); }); }); From 2123b4c5f9a18de860d1d2d2377a8dcf5f9bdcbf Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Sun, 13 Oct 2024 19:50:44 -0400 Subject: [PATCH 13/28] hook up sorting and clean up --- frontend/src/hooks/useWidgetSorting.js | 13 +++++++---- .../QADashboard/Components/RecipientCard.js | 5 ++-- .../Components/__tests__/RecipientCard.js | 5 ++-- .../index.js | 18 ++++++++++----- ...RecipientsWithClassScoresAndGoalsWidget.js | 23 ++++++++++--------- ...RecipientsWithClassScoresAndGoalsWidget.js | 2 +- 6 files changed, 38 insertions(+), 28 deletions(-) diff --git a/frontend/src/hooks/useWidgetSorting.js b/frontend/src/hooks/useWidgetSorting.js index 7e9102dcb7..b15497cbbe 100644 --- a/frontend/src/hooks/useWidgetSorting.js +++ b/frontend/src/hooks/useWidgetSorting.js @@ -42,20 +42,23 @@ export default function useWidgetSorting( // default is "value", otherwise use the key from the lookup const sortingBy = sorts[sortBy] || 'value'; - let valuesToSort; switch (sortingBy) { case 'string': valuesToSort = dataToUse.map((t) => ({ ...t, - sortBy: t.heading, + sortBy: !t.heading + ? t[sortBy].toString().toLowerCase() // If we don't have heading data, use the value. + : t.heading.toString().toLowerCase(), })); break; case 'date': valuesToSort = dataToUse.map((t) => ( { ...t, - sortBy: new Date(t.data.find((tp) => (tp.sortKey || tp.title) === sortBy).value), + sortBy: !t.data + ? new Date(t[sortBy]) // If we don't have data, use the value. + : new Date(t.data.find((tp) => (tp.sortKey || tp.title) === sortBy).value), })); break; case 'key': @@ -69,7 +72,9 @@ export default function useWidgetSorting( valuesToSort = dataToUse.map((t) => ( { ...t, - sortBy: parseValue(t.data.find((tp) => (tp.sortKey || tp.title) === sortBy).value), + sortBy: !t.data + ? parseValue(t[sortBy]) // If we don't have data, use the value. + : parseValue(t.data.find((tp) => (tp.sortKey || tp.title) === sortBy).value), })); break; } diff --git a/frontend/src/pages/QADashboard/Components/RecipientCard.js b/frontend/src/pages/QADashboard/Components/RecipientCard.js index 3be440c67d..801ca3c2ae 100644 --- a/frontend/src/pages/QADashboard/Components/RecipientCard.js +++ b/frontend/src/pages/QADashboard/Components/RecipientCard.js @@ -1,7 +1,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Checkbox } from '@trussworks/react-uswds'; -import moment from 'moment'; import { v4 as uuidv4 } from 'uuid'; import { Link } from 'react-router-dom'; import ExpanderButton from '../../../components/ExpanderButton'; @@ -51,7 +50,7 @@ function RecipientCard({

Last AR start date

- {moment(recipient.lastARStartDate).format('MM/DD/YYYY')} + {recipient.lastARStartDate}

@@ -77,7 +76,7 @@ function RecipientCard({

Report received date

-

{moment(recipient.reportDeliveryDate, 'YYYY-MM-DD').format('MM/DD/YYYY')}

+

{recipient.reportDeliveryDate}

a.name.localeCompare(b.name), + ); + setRecipientsWithClassScoresAndGoalsData({ widgetData: widgetData[0].data[0], - pageData: reducedRecipientData, + pageData: sortedReducedRecipients, }); updateError(''); } catch (e) { diff --git a/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js b/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js index 47abd124f6..447096a9e5 100644 --- a/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js +++ b/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js @@ -33,17 +33,17 @@ function RecipientsWithClassScoresAndGoalsWidget({ const [selectedRecipientCheckBoxes, setSelectedRecipientCheckBoxes] = useState({}); const [allRecipientsChecked, setAllRecipientsChecked] = useState(false); const [resetPagination, setResetPagination] = useState(false); - const [perPage, setPerPage] = useState(RECIPIENTS_WITH_CLASS_SCORES_AND_GOALS_GOAL_PER_PAGE); + const [perPage, setPerPage] = useState([RECIPIENTS_WITH_CLASS_SCORES_AND_GOALS_GOAL_PER_PAGE]); const defaultSort = { - sortBy: 'Recipient', + sortBy: 'name', direction: 'asc', activePage: 1, }; - // Probably we WONT use the useWidgetPaging hook here. const { handlePageChange, + requestSort, exportRows, sortConfig, setSortConfig, @@ -60,8 +60,8 @@ function RecipientsWithClassScoresAndGoalsWidget({ selectedRecipientCheckBoxes, 'recipientsWithClassScoresAndGoals', setRecipientsDataToDisplay, - [], - [], + ['name'], + ['lastARStartDate', 'reportDeliveryDate'], 'recipientsWithClassScoresAndGoals.csv', ); @@ -78,8 +78,9 @@ function RecipientsWithClassScoresAndGoalsWidget({ setPerPage(perPageValue); }; - const setSortBy = () => { - // Handle sort by, not sure how we will handle this yet. + const setSortBy = (e) => { + const [sortBy, direction] = e.target.value.split('-'); + requestSort(sortBy, direction); }; const getSubtitleWithPct = () => { @@ -233,10 +234,10 @@ function RecipientsWithClassScoresAndGoalsWidget({ > - - - - + + + +
diff --git a/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js b/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js index d445aa58f9..4475a404c2 100644 --- a/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js +++ b/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js @@ -21,7 +21,7 @@ const recipientData = { emotionalSupport: 6.0430, classroomOrganization: 5.0430, instructionalSupport: 4.0430, - reportDeliveryDate: '2022-03-01T04:00:00+00:00', + reportDeliveryDate: '03/01/2022', goals: [ { goalNumber: 'G-45641', From b6c0880ba3557a823e3050dbb5e503fcfa888522 Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Sun, 13 Oct 2024 19:53:44 -0400 Subject: [PATCH 14/28] fix for per page --- frontend/src/hooks/useWidgetPaging.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/hooks/useWidgetPaging.js b/frontend/src/hooks/useWidgetPaging.js index d776c46a6a..37c4001569 100644 --- a/frontend/src/hooks/useWidgetPaging.js +++ b/frontend/src/hooks/useWidgetPaging.js @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import { DECIMAL_BASE } from '@ttahub/common'; import useWidgetSorting from './useWidgetSorting'; import useWidgetExport from './useWidgetExport'; +import { set } from 'lodash'; export const parseValue = (value) => { const noCommasValue = value.replaceAll(',', ''); @@ -98,5 +99,6 @@ export default function useWidgetPaging( requestSort: sort, exportRows, sortConfig, + setSortConfig, }; } From 66f33500852e2460ae2062dc035e23f1c7addd49 Mon Sep 17 00:00:00 2001 From: GarrettEHill Date: Tue, 15 Oct 2024 07:44:37 -0700 Subject: [PATCH 15/28] adding region id --- src/queries/api/dashboards/qa/class.sql | 10 +++++++++- src/queries/api/dashboards/qa/fei.sql | 8 ++++++++ src/queries/api/dashboards/qa/no-tta.sql | 14 ++++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/queries/api/dashboards/qa/class.sql b/src/queries/api/dashboards/qa/class.sql index d880d6deb0..37e272de3c 100644 --- a/src/queries/api/dashboards/qa/class.sql +++ b/src/queries/api/dashboards/qa/class.sql @@ -88,6 +88,12 @@ JSON: { "nullable": true, "description": "Grant number associated with the recipient." }, + { + "columnName": "region id", + "type": "number", + "nullable": true, + "description": "Region number associated with the recipient's grant." + }, { "columnName": "goalId", "type": "number", @@ -752,6 +758,7 @@ WITH r.id "recipientId", r.name "recipientName", gr.number "grantNumber", + gr."regionId", (ARRAY_AGG(g.id ORDER BY g.id DESC))[1] "goalId", (ARRAY_AGG(g."createdAt" ORDER BY g.id DESC))[1] "goalCreatedAt", (ARRAY_AGG(g.status ORDER BY g.id DESC))[1] "goalStatus", @@ -809,7 +816,7 @@ WITH AND (has_class OR has_scores) AND (g.id IS NOT NULL OR mcs.id IS NOT NULL) AND (mrs.id IS NULL OR mrs.name = 'Complete') - GROUP BY 1, 2, 3 + GROUP BY 1, 2, 3, 4 ORDER BY 1, 3 ), @@ -853,6 +860,7 @@ WITH 'recipientId', "recipientId", 'recipientName', "recipientName", 'grantNumber', "grantNumber", + 'region id', "regionId", 'goalId', "goalId", 'goalCreatedAt', "goalCreatedAt", 'goalStatus', "goalStatus", diff --git a/src/queries/api/dashboards/qa/fei.sql b/src/queries/api/dashboards/qa/fei.sql index 116ea12348..1e536b9e7d 100644 --- a/src/queries/api/dashboards/qa/fei.sql +++ b/src/queries/api/dashboards/qa/fei.sql @@ -88,6 +88,12 @@ JSON: { "nullable": true, "description": "Grant number associated with the recipient." }, + { + "columnName": "region id", + "type": "number", + "nullable": true, + "description": "Region number associated with the recipient's grant." + }, { "columnName": "goalId", "type": "number", @@ -604,6 +610,7 @@ WITH r.id "recipientId", r.name "recipientName", gr.number "grantNumber", + gr."regionId", g.id "goalId", g."createdAt", g.status "goalStatus", @@ -657,6 +664,7 @@ WITH 'recipientId', "recipientId", 'recipientName', "recipientName", 'grantNumber', "grantNumber", + 'region id', "regionId", 'goalId', "goalId", 'createdAt', "createdAt", 'goalStatus', "goalStatus", diff --git a/src/queries/api/dashboards/qa/no-tta.sql b/src/queries/api/dashboards/qa/no-tta.sql index 8e4b146cf1..f19347a5be 100644 --- a/src/queries/api/dashboards/qa/no-tta.sql +++ b/src/queries/api/dashboards/qa/no-tta.sql @@ -76,6 +76,12 @@ JSON: { "nullable": true, "description": "Name of the recipient." }, + { + "columnName": "region id", + "type": "number", + "nullable": true, + "description": "Region number associated with the recipient's grant." + }, { "columnName": "last tta", "type": "date", @@ -460,7 +466,10 @@ no_tta_widget AS ( FROM no_tta ), no_tta_page AS ( - SELECT r.id, r.name, + SELECT + r.id, + r.name, + gr."regionId", (array_agg(a."endDate" ORDER BY a."endDate" DESC))[1] last_tta, now()::date - ((array_agg(a."endDate" ORDER BY a."endDate" DESC))[1])::date days_since_last_tta FROM no_tta nt @@ -471,7 +480,7 @@ no_tta_page AS ( LEFT JOIN "ActivityReports" a ON ar."activityReportId" = a.id AND a."calculatedStatus" = 'approved' WHERE gr.status = 'Active' - GROUP BY 1,2 + GROUP BY 1,2,3 ), datasets AS ( SELECT 'no_tta_widget' data_set, COUNT(*) records, @@ -486,6 +495,7 @@ datasets AS ( JSONB_AGG(JSONB_BUILD_OBJECT( 'recipient id', id, 'recipient name', name, + 'region id', "regionId", 'last tta', last_tta, 'days since last tta', days_since_last_tta )) data From 343527588c8ec901f62c6883464523656299acc2 Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Tue, 15 Oct 2024 10:55:22 -0400 Subject: [PATCH 16/28] hook up links for recipients and goals --- frontend/src/hooks/useWidgetPaging.js | 1 - frontend/src/pages/QADashboard/Components/GoalCard.js | 6 +++++- frontend/src/pages/QADashboard/Components/RecipientCard.js | 5 ++++- .../src/pages/QADashboard/Components/__tests__/GoalCard.js | 2 ++ .../pages/QADashboard/Components/__tests__/RecipientCard.js | 2 ++ .../QADashboard/RecipientsWithClassScoresAndGoals/index.js | 2 ++ frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js | 5 +++-- .../QADashboard/RecipientsWithOhsStandardFeiGoal/index.js | 5 +++-- 8 files changed, 21 insertions(+), 7 deletions(-) diff --git a/frontend/src/hooks/useWidgetPaging.js b/frontend/src/hooks/useWidgetPaging.js index 37c4001569..798352980c 100644 --- a/frontend/src/hooks/useWidgetPaging.js +++ b/frontend/src/hooks/useWidgetPaging.js @@ -2,7 +2,6 @@ import { useCallback, useEffect, useState } from 'react'; import { DECIMAL_BASE } from '@ttahub/common'; import useWidgetSorting from './useWidgetSorting'; import useWidgetExport from './useWidgetExport'; -import { set } from 'lodash'; export const parseValue = (value) => { const noCommasValue = value.replaceAll(',', ''); diff --git a/frontend/src/pages/QADashboard/Components/GoalCard.js b/frontend/src/pages/QADashboard/Components/GoalCard.js index 94dc6b1ed8..6b43b71b1f 100644 --- a/frontend/src/pages/QADashboard/Components/GoalCard.js +++ b/frontend/src/pages/QADashboard/Components/GoalCard.js @@ -6,6 +6,8 @@ import DataRow from '../../../components/DataRow'; function GoalCard({ goal, + recipientId, + regionId, expanded, }) { return ( @@ -15,7 +17,7 @@ function GoalCard({ + {goal.goalNumber} )} @@ -48,5 +50,7 @@ export const goalPropTypes = PropTypes.shape({ GoalCard.propTypes = { goal: goalPropTypes.isRequired, expanded: PropTypes.bool.isRequired, + recipientId: PropTypes.string.isRequired, + regionId: PropTypes.string.isRequired, }; export default GoalCard; diff --git a/frontend/src/pages/QADashboard/Components/RecipientCard.js b/frontend/src/pages/QADashboard/Components/RecipientCard.js index 801ca3c2ae..76dbea217a 100644 --- a/frontend/src/pages/QADashboard/Components/RecipientCard.js +++ b/frontend/src/pages/QADashboard/Components/RecipientCard.js @@ -42,7 +42,7 @@ function RecipientCard({

Recipient

- + {recipient.name}

@@ -97,6 +97,8 @@ function RecipientCard({ goal={goal} zIndex={zIndex - 1} expanded={goalsExpanded} + recipientId={recipient.id} + regionId={recipient.regionId} /> ))} @@ -108,6 +110,7 @@ function RecipientCard({ RecipientCard.propTypes = { recipient: PropTypes.shape({ id: PropTypes.number.isRequired, + regionId: PropTypes.number.isRequired, name: PropTypes.string.isRequired, lastARStartDate: PropTypes.string.isRequired, emotionalSupport: PropTypes.number.isRequired, diff --git a/frontend/src/pages/QADashboard/Components/__tests__/GoalCard.js b/frontend/src/pages/QADashboard/Components/__tests__/GoalCard.js index 0bc34ff6c3..9404ef5ee7 100644 --- a/frontend/src/pages/QADashboard/Components/__tests__/GoalCard.js +++ b/frontend/src/pages/QADashboard/Components/__tests__/GoalCard.js @@ -22,6 +22,8 @@ describe('GoalCard', () => { status: 'In progress', creator: 'Jon Doe', collaborator: 'Jane Doe', + recipientId: 1, + regionId: 2, }; renderGoalCard(goal); diff --git a/frontend/src/pages/QADashboard/Components/__tests__/RecipientCard.js b/frontend/src/pages/QADashboard/Components/__tests__/RecipientCard.js index af40bf8981..f3e45086b3 100644 --- a/frontend/src/pages/QADashboard/Components/__tests__/RecipientCard.js +++ b/frontend/src/pages/QADashboard/Components/__tests__/RecipientCard.js @@ -11,6 +11,8 @@ const recipientData = { classroomOrganization: 2.3, instructionalSupport: 3.4, reportDeliveryDate: '09/15/2021', + id: 1, + regionId: 2, goals: [ { goalNumber: 'G-54826', diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js index f51ca11737..1c80bec1f8 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js @@ -91,6 +91,7 @@ export default function RecipientsWithClassScoresAndGoals() { lastARStartDate, reportDeliveryDate, collaborators, + regionId, creator, } = item; @@ -119,6 +120,7 @@ export default function RecipientsWithClassScoresAndGoals() { grantNumber, lastARStartDate: lastARStartDate === null ? '01/01/2000' : moment(lastARStartDate).format('MM/DD/YYYY'), reportDeliveryDate: reportDeliveryDate === null ? '01/01/2000' : moment(reportDeliveryDate, 'YYYY-MM-DD').format('MM/DD/YYYY'), + regionId, goals: [ { id: goalId, diff --git a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js index d2548bb804..ab61147faf 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js @@ -71,6 +71,7 @@ export default function RecipientsWithNoTta() { // Format the recipient data for the generic widget. let formattedRecipientPageData = pageData[0].data.map((item) => { const recipientId = item['recipient id']; + const regionId = item['region id']; const recipientName = item['recipient name']; const dateOfLastTta = item['last tta']; const daysSinceLastTta = item['days since last tta']; @@ -78,9 +79,9 @@ export default function RecipientsWithNoTta() { id: recipientId, heading: recipientName, name: recipientName, - isURL: true, + isUrl: true, hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', // TODO: Set to correct link. + link: `/recipient-tta-records/${recipientId}/region/${regionId}/profile`, data: [ { title: 'Date_of_Last_TTA', diff --git a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js index 79cdcb9a18..1b7a389c0c 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js @@ -74,6 +74,7 @@ export default function RecipientsWithOhsStandardFeiGoal() { // Convert data to format that widget expects. let formattedRecipientPageData = pageData[0].data.map((item) => { const { recipientId } = item; + const { regionId } = item; const { recipientName } = item; const { goalId } = item; const { goalStatus } = item; @@ -84,9 +85,9 @@ export default function RecipientsWithOhsStandardFeiGoal() { id: recipientId, heading: recipientName, name: recipientName, - isURL: true, + isUrl: true, hideLinkIcon: true, - link: '/recipient-tta-records/376/region/1/profile', // TODO: Set to correct link. + link: `/recipient-tta-records/${recipientId}/region/${regionId}/profile`, data: [ { title: 'Goal_created_on', From cb8d503f5a897cd0f10e6f6c0d9bcdc572d1a4f3 Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Tue, 15 Oct 2024 11:58:10 -0400 Subject: [PATCH 17/28] fix links --- frontend/src/pages/QADashboard/Components/GoalCard.js | 2 +- frontend/src/pages/QADashboard/Components/RecipientCard.js | 2 +- .../QADashboard/RecipientsWithClassScoresAndGoals/index.js | 2 +- .../QADashboard/RecipientsWithOhsStandardFeiGoal/index.js | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/QADashboard/Components/GoalCard.js b/frontend/src/pages/QADashboard/Components/GoalCard.js index 6b43b71b1f..43dc1e6a39 100644 --- a/frontend/src/pages/QADashboard/Components/GoalCard.js +++ b/frontend/src/pages/QADashboard/Components/GoalCard.js @@ -17,7 +17,7 @@ function GoalCard({ + {goal.goalNumber} )} diff --git a/frontend/src/pages/QADashboard/Components/RecipientCard.js b/frontend/src/pages/QADashboard/Components/RecipientCard.js index 76dbea217a..60e71046db 100644 --- a/frontend/src/pages/QADashboard/Components/RecipientCard.js +++ b/frontend/src/pages/QADashboard/Components/RecipientCard.js @@ -42,7 +42,7 @@ function RecipientCard({

Recipient

- + {recipient.name}

diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js index 1c80bec1f8..0be07b7802 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js @@ -91,10 +91,10 @@ export default function RecipientsWithClassScoresAndGoals() { lastARStartDate, reportDeliveryDate, collaborators, - regionId, creator, } = item; + const regionId = item['region id']; // Check if recipientId is already in the accumulator. const existingRecipient = acc.find((recipient) => recipient.id === recipientId); if (existingRecipient) { diff --git a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js index 1b7a389c0c..2caccac299 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js @@ -70,17 +70,17 @@ export default function RecipientsWithOhsStandardFeiGoal() { // Get summary and row data. const pageData = data.filter((d) => d.data_set === 'with_fei_page'); const widgetData = data.filter((d) => d.data_set === 'with_fei_widget'); - // Convert data to format that widget expects. let formattedRecipientPageData = pageData[0].data.map((item) => { const { recipientId } = item; - const { regionId } = item; + const regionId = item['region id']; const { recipientName } = item; const { goalId } = item; const { goalStatus } = item; // const { grantNumber } = item; const { createdAt } = item; const { rootCause } = item; + return { id: recipientId, heading: recipientName, From 6465d39a9440b5c22b8bc7142e2a7fc0270ab62c Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Tue, 15 Oct 2024 12:33:40 -0400 Subject: [PATCH 18/28] hook up filter applicable message --- .circleci/config.yml | 2 +- frontend/src/fetchers/__tests__/ssdi.js | 38 ++++++++++++++++++++++++- frontend/src/fetchers/ssdi.js | 9 ++++++ frontend/src/pages/QADashboard/index.js | 8 +++++- 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 827fb02ff8..d4d2d3d3a4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -560,7 +560,7 @@ parameters: type: string dev_git_branch: # change to feature branch to test deployment description: "Name of github branch that will deploy to dev" - default: "al-ttahub-3316-3317-overview-widgets" + default: "al-ttahub-3402-connect-be-overview" type: string sandbox_git_branch: # change to feature branch to test deployment default: "kw-fix-duplicate-programs" diff --git a/frontend/src/fetchers/__tests__/ssdi.js b/frontend/src/fetchers/__tests__/ssdi.js index 0a8739ccb7..7ad2af6dc3 100644 --- a/frontend/src/fetchers/__tests__/ssdi.js +++ b/frontend/src/fetchers/__tests__/ssdi.js @@ -1,6 +1,6 @@ import fetchMock from 'fetch-mock'; import join from 'url-join'; -import { getSelfServiceData } from '../ssdi'; +import { getSelfServiceData, containsFiltersThatAreNotApplicable } from '../ssdi'; const ssdiUrl = join('/', 'api', 'ssdi', 'api', 'dashboards', 'qa'); @@ -82,4 +82,40 @@ describe('SSDI fetcher', () => { }, ])).rejects.toThrow('Invalid filter name'); }); + + it('containsFiltersThatAreNotApplicable returns false if all filters are allowed', () => { + const filters = [ + { + id: '9ac8381c-2507-4b4a-a30c-6f1f87a00901', + topic: 'region', + condition: 'is', + query: '14', + }, + { + id: '9ac8381c-2507-4b4a-a30c-6f1f8723401', + topic: 'pickles', + condition: 'are', + query: 'spicy', + }, + ]; + expect(containsFiltersThatAreNotApplicable('recipients-with-no-tta', filters)).toBe(true); + }); + + it('containsFiltersThatAreNotApplicable returns true if any filter is not allowed', () => { + const filters = [ + { + id: '9ac8381c-2507-4b4a-a30c-6f1f87a00901', + topic: 'region', + condition: 'is', + query: '14', + }, + { + id: '9ac8381c-2507-4b4a-a30c-6f1f8723401', + topic: 'stateCode', + condition: 'is', + query: 'ct', + }, + ]; + expect(containsFiltersThatAreNotApplicable('recipients-with-class-scores-and-goals', filters)).toBe(false); + }); }); diff --git a/frontend/src/fetchers/ssdi.js b/frontend/src/fetchers/ssdi.js index 44db3ea050..68066bd839 100644 --- a/frontend/src/fetchers/ssdi.js +++ b/frontend/src/fetchers/ssdi.js @@ -50,6 +50,15 @@ const allowedTopicsForQuery = { ], }; +export const containsFiltersThatAreNotApplicable = (filterName, filters) => { + if (!allowedTopicsForQuery[filterName]) { + throw new Error('Invalid filter name'); + } + + const config = allowedTopicsForQuery[filterName]; + return filters.some((filter) => !config.includes(filter.topic)); +}; + export const getSelfServiceDataQueryString = (filterName, filters) => { if (!allowedTopicsForQuery[filterName]) { throw new Error('Invalid filter name'); diff --git a/frontend/src/pages/QADashboard/index.js b/frontend/src/pages/QADashboard/index.js index 653d593e57..cee6a3c4c7 100644 --- a/frontend/src/pages/QADashboard/index.js +++ b/frontend/src/pages/QADashboard/index.js @@ -21,7 +21,7 @@ import Drawer from '../../components/Drawer'; import ContentFromFeedByTag from '../../components/ContentFromFeedByTag'; import PercentageActivityReportByRole from '../../widgets/PercentageActivityReportByRole'; import RootCauseFeiGoals from '../../widgets/RootCauseFeiGoals'; -import { getSelfServiceData } from '../../fetchers/ssdi'; +import { getSelfServiceData, containsFiltersThatAreNotApplicable } from '../../fetchers/ssdi'; const DISALLOWED_FILTERS = [ 'domainClassroomOrganization', @@ -65,6 +65,7 @@ export default function QADashboard() { filters, ['no_tta_widget'], ); + const noTTAContainsFiltersThatAreNotAllowed = containsFiltersThatAreNotApplicable('recipients-with-no-tta', filters); const noTTAData = recipientsWithNoTtaData.find((item) => item.data_set === 'no_tta_widget'); // FEI data. @@ -73,6 +74,7 @@ export default function QADashboard() { filters, ['with_fei_widget', 'with_fei_graph'], ); + const feiContainsFiltersThatAreNotAllowed = containsFiltersThatAreNotApplicable('recipients-with-ohs-standard-fei-goal', filters); const feiOverviewData = feiData.find((item) => item.data_set === 'with_fei_widget'); const feiGraphData = feiData.find((item) => item.data_set === 'with_fei_graph'); @@ -88,17 +90,21 @@ export default function QADashboard() { filters, ['with_class_widget'], ); + const classContainsFiltersThatAreNotAllowed = containsFiltersThatAreNotApplicable('recipients-with-class-scores-and-goals', filters); const classOverviewData = classData.find((item) => item.data_set === 'with_class_widget'); // Build overview data. const overviewData = { recipientsWithNoTTA: { + filterApplicable: !noTTAContainsFiltersThatAreNotAllowed, pct: noTTAData.data[0]['% recipients without tta'] || '0%', }, recipientsWithOhsStandardFeiGoals: { + filterApplicable: !feiContainsFiltersThatAreNotAllowed, pct: feiOverviewData.data[0]['% recipients with fei'] || '0%', }, recipientsWithOhsStandardClass: { + filterApplicable: !classContainsFiltersThatAreNotAllowed, pct: classOverviewData.data[0]['% recipients with class'] || '0%', }, }; From d06b2e130801c50d88657a68bb88ca1f98eef94e Mon Sep 17 00:00:00 2001 From: GarrettEHill Date: Tue, 15 Oct 2024 09:37:05 -0700 Subject: [PATCH 19/28] make sure closed and merged goals are not included --- src/queries/api/dashboards/qa/class.sql | 4 ++++ src/queries/api/dashboards/qa/fei.sql | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/queries/api/dashboards/qa/class.sql b/src/queries/api/dashboards/qa/class.sql index 37e272de3c..6b170be4f5 100644 --- a/src/queries/api/dashboards/qa/class.sql +++ b/src/queries/api/dashboards/qa/class.sql @@ -742,6 +742,8 @@ WITH LEFT JOIN "MonitoringClassSummaries" mcs ON mr."reviewId" = mcs."reviewId" WHERE gr.status = 'Active' + AND g."deletedAt" IS NULL + AND g."mapsToParentGoalId" IS NULL GROUP BY 1 ), with_class_widget AS ( @@ -816,6 +818,8 @@ WITH AND (has_class OR has_scores) AND (g.id IS NOT NULL OR mcs.id IS NOT NULL) AND (mrs.id IS NULL OR mrs.name = 'Complete') + AND g."deletedAt" IS NULL + AND g."mapsToParentGoalId" IS NULL GROUP BY 1, 2, 3, 4 ORDER BY 1, 3 ), diff --git a/src/queries/api/dashboards/qa/fei.sql b/src/queries/api/dashboards/qa/fei.sql index 1e536b9e7d..4a0633182b 100644 --- a/src/queries/api/dashboards/qa/fei.sql +++ b/src/queries/api/dashboards/qa/fei.sql @@ -578,6 +578,8 @@ WITH LEFT JOIN filtered_goals fg ON g.id = fg.id WHERE gr.status = 'Active' + AND g."deletedAt" IS NULL + AND g."mapsToParentGoalId" IS NULL GROUP BY 1 ), with_fei_widget AS ( @@ -630,6 +632,9 @@ WITH ON g.id = fg.id LEFT JOIN "GoalFieldResponses" gfr ON g.id = gfr."goalId" + WHERE 1 = 1 + AND g."deletedAt" IS NULL + AND g."mapsToParentGoalId" IS NULL ), with_fei_graph AS ( SELECT From cf238022c3805322d9d98ae8aaf20bfa6924fc8c Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Tue, 15 Oct 2024 14:14:03 -0400 Subject: [PATCH 20/28] add tests fix sorting on class --- frontend/src/hooks/useWidgetPaging.js | 4 +- frontend/src/hooks/useWidgetSorting.js | 7 +- ...RecipientsWithClassScoresAndGoalsWidget.js | 107 ++++++++++++++++++ 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/frontend/src/hooks/useWidgetPaging.js b/frontend/src/hooks/useWidgetPaging.js index 798352980c..234eadd8ee 100644 --- a/frontend/src/hooks/useWidgetPaging.js +++ b/frontend/src/hooks/useWidgetPaging.js @@ -82,8 +82,8 @@ export default function useWidgetPaging( } }, [loading, perPageNumber, setSortConfig, sortConfig]); - const sort = useCallback((sortBy) => { - requestSort(sortBy); + const sort = useCallback((sortBy, direction) => { + requestSort(sortBy, direction); setOffset(0); }, [requestSort]); diff --git a/frontend/src/hooks/useWidgetSorting.js b/frontend/src/hooks/useWidgetSorting.js index b15497cbbe..457d61a1e2 100644 --- a/frontend/src/hooks/useWidgetSorting.js +++ b/frontend/src/hooks/useWidgetSorting.js @@ -22,10 +22,13 @@ export default function useWidgetSorting( ) { const [sortConfig, setSortConfig] = useSessionSort(defaultSortConfig, localStorageKey); - const requestSort = useCallback((sortBy) => { + const requestSort = useCallback((sortBy, passedDirection = null) => { // Get sort direction. let direction = 'asc'; - if ( + if (passedDirection) { + // If the direction is passed, use it. + direction = passedDirection; + } else if ( sortConfig && sortConfig.sortBy === sortBy && sortConfig.direction === 'asc' diff --git a/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js b/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js index 4475a404c2..8d19b66d87 100644 --- a/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js +++ b/frontend/src/widgets/__tests__/RecipientsWithClassScoresAndGoalsWidget.js @@ -1,6 +1,8 @@ import '@testing-library/jest-dom'; import React from 'react'; +import moment from 'moment'; import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { Router } from 'react-router'; import { createMemoryHistory } from 'history'; import RecipientsWithClassScoresAndGoalsWidget from '../RecipientsWithClassScoresAndGoalsWidget'; @@ -97,4 +99,109 @@ describe('Recipients With Class and Scores and Goals Widget', () => { expect(screen.getByText(recipientData.pageData[0].goals[1].creator)).toBeInTheDocument(); expect(screen.getByText(recipientData.pageData[0].goals[1].collaborator)).toBeInTheDocument(); }); + + it('updates the page when the per page limit is changed', async () => { + const numberOfRecipients = 15; + const multipleRecipientData = { + widgetData: { + '% recipients with class': 18.26, + 'grants with class': 346, + 'recipients with class': 283, + total: 1550, + }, + pageData: Array.from({ length: numberOfRecipients }, (_, i) => ({ + ...recipientData.pageData[0], + name: `recipient ${i + 1}`, + })), + }; + renderRecipientsWithClassScoresAndGoalsWidget(multipleRecipientData); + + expect(screen.getByText(/Recipients with CLASS® scores/i)).toBeInTheDocument(); + expect(screen.getByText(/1-10 of 15/i)).toBeInTheDocument(); + + // Make sure we see 'recipient 1' but we do NOT see 'recipient 15'. + expect(screen.getByText('recipient 1')).toBeInTheDocument(); + expect(screen.queryByText('recipient 15')).not.toBeInTheDocument(); + + // Click the perPage dropdown and select 25. + const perPageDropdown = screen.getByRole('combobox', { name: /select recipients per page/i }); + userEvent.selectOptions(perPageDropdown, '25'); + expect(screen.getByText(/1-15 of 15/i)).toBeInTheDocument(); + expect(screen.getByText('recipient 1')).toBeInTheDocument(); + expect(screen.getByText('recipient 15')).toBeInTheDocument(); + }); + + it('sorts the recipients by name', async () => { + const numberOfRecipients = 15; + const multipleRecipientData = { + widgetData: { + '% recipients with class': 18.26, + 'grants with class': 346, + 'recipients with class': 283, + total: 1550, + }, + pageData: Array.from({ length: numberOfRecipients }, (_, i) => ({ + ...recipientData.pageData[0], + name: `recipient ${i + 1}`, + })), + }; + + renderRecipientsWithClassScoresAndGoalsWidget(multipleRecipientData); + + expect(screen.getByText(/Recipients with CLASS® scores/i)).toBeInTheDocument(); + expect(screen.getByText(/1-10 of 15/i)).toBeInTheDocument(); + + // Make sure we see 'Apple' but we do NOT see 'Zebra'. + expect(screen.getByText('recipient 1')).toBeInTheDocument(); + expect(screen.queryByText('recipient 15')).not.toBeInTheDocument(); + + // Click the sort button. + const sortButton = screen.getByRole('combobox', { name: /sort by/i }); + userEvent.selectOptions(sortButton, 'name-desc'); + + // Make sure we see 'Zebra' but we do NOT see 'Apple'. + expect(screen.getByText('recipient 15')).toBeInTheDocument(); + expect(screen.queryByText('recipient 1')).not.toBeInTheDocument(); + }); + + it('sorts the recipients by date', async () => { + const numberOfRecipients = 15; + const multipleRecipientData = { + widgetData: { + '% recipients with class': 18.26, + 'grants with class': 346, + 'recipients with class': 283, + total: 1550, + }, + pageData: Array.from({ length: numberOfRecipients }, (_, i) => ({ + ...recipientData.pageData[0], + name: `recipient ${i + 1}`, + // Make the date of last TTA increment by 1 day for each recipient. + lastARStartDate: moment(recipientData.pageData[0].lastARStartDate).add(i, 'days').format('MM/DD/YYYY'), + })), + }; + + renderRecipientsWithClassScoresAndGoalsWidget(multipleRecipientData); + + expect(screen.getByText(/Recipients with CLASS® scores/i)).toBeInTheDocument(); + expect(screen.getByText(/1-10 of 15/i)).toBeInTheDocument(); + + // Make sure we see 'Apple' but we do NOT see 'Zebra'. + expect(screen.getByText('recipient 1')).toBeInTheDocument(); + expect(screen.queryByText('recipient 15')).not.toBeInTheDocument(); + + // Click the sort button. + const sortButton = screen.getByRole('combobox', { name: /sort by/i }); + userEvent.selectOptions(sortButton, 'lastARStartDate-desc'); + + // Make sure we see 'Zebra' but we do NOT see 'Apple'. + expect(screen.getByText('recipient 15')).toBeInTheDocument(); + expect(screen.queryByText('recipient 1')).not.toBeInTheDocument(); + + // Click the sort button. + userEvent.selectOptions(sortButton, 'lastARStartDate-asc'); + + expect(screen.getByText('recipient 1')).toBeInTheDocument(); + expect(screen.queryByText('recipient 15')).not.toBeInTheDocument(); + }); }); From 36785982d0dc4edc53826937e28ed5aa885ed020 Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Tue, 15 Oct 2024 15:45:29 -0400 Subject: [PATCH 21/28] hook up class to new export --- .../index.js | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js index 0be07b7802..c9acae014a 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js @@ -114,13 +114,36 @@ export default function RecipientsWithClassScoresAndGoals() { const newRecipient = { id: recipientId, name: recipientName, + heading: recipientName, emotionalSupport: !emotionalSupport ? 0 : emotionalSupport, classroomOrganization: !classroomOrganization ? 0 : classroomOrganization, instructionalSupport: !instructionalSupport ? 0 : instructionalSupport, grantNumber, - lastARStartDate: lastARStartDate === null ? '01/01/2000' : moment(lastARStartDate).format('MM/DD/YYYY'), - reportDeliveryDate: reportDeliveryDate === null ? '01/01/2000' : moment(reportDeliveryDate, 'YYYY-MM-DD').format('MM/DD/YYYY'), + lastARStartDate: lastARStartDate === null ? null : moment(lastARStartDate).format('MM/DD/YYYY'), + reportDeliveryDate: reportDeliveryDate === null ? null : moment(reportDeliveryDate, 'YYYY-MM-DD').format('MM/DD/YYYY'), regionId, + data: [ + { + title: 'Last AR Start Date', + value: lastARStartDate === null ? null : moment(lastARStartDate).format('MM/DD/YYYY'), + }, + { + title: 'Emotional Support', + value: emotionalSupport, + }, + { + title: 'Classroom Organization', + value: classroomOrganization, + }, + { + title: 'Instructional Support', + value: instructionalSupport, + }, + { + title: 'Report Delivery Date', + value: reportDeliveryDate === null ? null : moment(reportDeliveryDate, 'YYYY-MM-DD').format('MM/DD/YYYY'), + }, + ], goals: [ { id: goalId, From ac26eea2e7c29f8886658f611782ef3efff27c93 Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Tue, 15 Oct 2024 16:28:21 -0400 Subject: [PATCH 22/28] resolve weird collision between sorting and exporting --- frontend/src/hooks/useWidgetExport.js | 6 ++++-- frontend/src/hooks/useWidgetPaging.js | 2 ++ .../QADashboard/RecipientsWithClassScoresAndGoals/index.js | 2 +- .../src/widgets/RecipientsWithClassScoresAndGoalsWidget.js | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/useWidgetExport.js b/frontend/src/hooks/useWidgetExport.js index e43c25c31c..5af903a4dd 100644 --- a/frontend/src/hooks/useWidgetExport.js +++ b/frontend/src/hooks/useWidgetExport.js @@ -7,6 +7,7 @@ export default function useWidgetExport( checkboxes, exportHeading, exportName, + exportDataName = null, // Specify the data to export. ) { const exportRows = useCallback((exportType) => { let url = null; @@ -33,7 +34,8 @@ export default function useWidgetExport( // create a csv file of all the rows. const csvRows = dataToExport.map((row) => { - const rowValues = row.data.map((d) => d.value); + const dataToUse = !row.data && exportDataName ? row[exportDataName] : row.data; + const rowValues = dataToUse.map((d) => d.value); // If the heading has a comma, wrap it in quotes. const rowHeadingToUse = row.heading.includes(',') ? `"${row.heading}"` : row.heading; return `${rowHeadingToUse},${rowValues.join(',')}`; @@ -59,7 +61,7 @@ export default function useWidgetExport( } finally { window.URL.revokeObjectURL(url); } - }, [checkboxes, data, exportHeading, exportName, headers]); + }, [checkboxes, data, exportHeading, exportName, headers, exportDataName]); return { exportRows, diff --git a/frontend/src/hooks/useWidgetPaging.js b/frontend/src/hooks/useWidgetPaging.js index 234eadd8ee..59ab448222 100644 --- a/frontend/src/hooks/useWidgetPaging.js +++ b/frontend/src/hooks/useWidgetPaging.js @@ -28,6 +28,7 @@ export default function useWidgetPaging( stringColumns = [], dateColumns = [], exportName, + exportDataName = null, ) { const { sortConfig, @@ -50,6 +51,7 @@ export default function useWidgetPaging( checkBoxes, exportHeading, exportName, + exportDataName, ); const { activePage } = sortConfig; diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js index c9acae014a..0eb14ce18c 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js @@ -122,7 +122,7 @@ export default function RecipientsWithClassScoresAndGoals() { lastARStartDate: lastARStartDate === null ? null : moment(lastARStartDate).format('MM/DD/YYYY'), reportDeliveryDate: reportDeliveryDate === null ? null : moment(reportDeliveryDate, 'YYYY-MM-DD').format('MM/DD/YYYY'), regionId, - data: [ + dataForExport: [ { title: 'Last AR Start Date', value: lastARStartDate === null ? null : moment(lastARStartDate).format('MM/DD/YYYY'), diff --git a/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js b/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js index 447096a9e5..9479879724 100644 --- a/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js +++ b/frontend/src/widgets/RecipientsWithClassScoresAndGoalsWidget.js @@ -63,6 +63,7 @@ function RecipientsWithClassScoresAndGoalsWidget({ ['name'], ['lastARStartDate', 'reportDeliveryDate'], 'recipientsWithClassScoresAndGoals.csv', + 'dataForExport', ); const perPageChange = (e) => { From fe6e2a9ab0f1483cb095f864d98aa78b9a234e6f Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Thu, 17 Oct 2024 08:36:10 -0400 Subject: [PATCH 23/28] fix bad merge of routes --- frontend/src/Routes.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/frontend/src/Routes.js b/frontend/src/Routes.js index 210f3e751f..318ad90de3 100644 --- a/frontend/src/Routes.js +++ b/frontend/src/Routes.js @@ -41,6 +41,9 @@ import SessionForm from './pages/SessionForm'; import ViewTrainingReport from './pages/ViewTrainingReport'; import QADashboard from './pages/QADashboard'; import SomethingWentWrong from './components/SomethingWentWrong'; +import RecipientsWithNoTta from './pages/QADashboard/RecipientsWithNoTta'; +import RecipientsWithClassScoresAndGoals from './pages/QADashboard/RecipientsWithClassScoresAndGoals'; +import RecipientsWithOhsStandardFeiGoal from './pages/QADashboard/RecipientsWithOhsStandardFeiGoal'; export default function Routes({ alert, @@ -157,6 +160,33 @@ export default function Routes({ )} /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> Date: Thu, 17 Oct 2024 08:54:21 -0700 Subject: [PATCH 24/28] handle divide by zero case --- src/queries/api/dashboards/qa/class.sql | 4 ++-- src/queries/api/dashboards/qa/dashboard.sql | 14 +++++++------- src/queries/api/dashboards/qa/fei.sql | 8 ++++---- src/queries/api/dashboards/qa/no-tta.sql | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/queries/api/dashboards/qa/class.sql b/src/queries/api/dashboards/qa/class.sql index 6b170be4f5..6076877efa 100644 --- a/src/queries/api/dashboards/qa/class.sql +++ b/src/queries/api/dashboards/qa/class.sql @@ -748,8 +748,8 @@ WITH ), with_class_widget AS ( SELECT - (((COUNT(DISTINCT wc.id) FILTER (WHERE has_class)::decimal/ - COUNT(DISTINCT wc.id)))*100)::decimal(5,2) "% recipients with class", + (COALESCE(COUNT(DISTINCT wc.id) FILTER (WHERE has_class)::decimal/ + NULLIF(COUNT(DISTINCT wc.id), 0), 0)*100)::decimal(5,2) "% recipients with class", COUNT(DISTINCT wc.id) FILTER (WHERE wc.has_class) "recipients with class", COUNT(DISTINCT wc.id) total, SUM(grant_count) "grants with class" diff --git a/src/queries/api/dashboards/qa/dashboard.sql b/src/queries/api/dashboards/qa/dashboard.sql index c6df917bc4..5ef32e757f 100644 --- a/src/queries/api/dashboards/qa/dashboard.sql +++ b/src/queries/api/dashboards/qa/dashboard.sql @@ -1089,9 +1089,9 @@ WITH COUNT(DISTINCT a.id) FILTER (WHERE a."deliveryMethod" IN ('In person', 'in-person', 'In-person')) AS in_person_count, COUNT(DISTINCT a.id) FILTER (WHERE a."deliveryMethod" IN ('Virtual', 'virtual')) AS virtual_count, COUNT(DISTINCT a.id) FILTER (WHERE a."deliveryMethod" IN ('Hybrid', 'hybrid')) AS hybrid_count, - ((COUNT(DISTINCT a.id) FILTER (WHERE a."deliveryMethod" IN ('In person', 'in-person', 'In-person')) * 100.0) / COUNT(DISTINCT a.id))::decimal(5,2) AS in_person_percentage, - ((COUNT(DISTINCT a.id) FILTER (WHERE a."deliveryMethod" IN ('Virtual', 'virtual')) * 100.0) / COUNT(DISTINCT a.id))::decimal(5,2) AS virtual_percentage, - ((COUNT(DISTINCT a.id) FILTER (WHERE a."deliveryMethod" IN ('Hybrid', 'hybrid')) * 100.0) / COUNT(DISTINCT a.id))::decimal(5,2) AS hybrid_percentage + (COALESCE((COUNT(DISTINCT a.id) FILTER (WHERE a."deliveryMethod" IN ('In person', 'in-person', 'In-person')) * 100.0) / NULLIF(COUNT(DISTINCT a.id),0),0))::decimal(5,2) AS in_person_percentage, + (COALESCE((COUNT(DISTINCT a.id) FILTER (WHERE a."deliveryMethod" IN ('Virtual', 'virtual')) * 100.0) / NULLIF(COUNT(DISTINCT a.id),0),0))::decimal(5,2) AS virtual_percentage, + (COALESCE((COUNT(DISTINCT a.id) FILTER (WHERE a."deliveryMethod" IN ('Hybrid', 'hybrid')) * 100.0) / NULLIF(COUNT(DISTINCT a.id),0),0))::decimal(5,2) AS hybrid_percentage FROM "ActivityReports" a JOIN filtered_activity_reports far ON a.id = far.id @@ -1106,9 +1106,9 @@ WITH SUM(in_person_count) in_person_count, SUM(virtual_count) virtual_count, SUM(hybrid_count) hybrid_count, - ((SUM(in_person_count) * 100.0) / SUM(in_person_count + virtual_count + hybrid_count))::decimal(5,2) AS in_person_percentage, - ((SUM(virtual_count) * 100.0) / SUM(in_person_count + virtual_count + hybrid_count))::decimal(5,2) AS virtual_percentage, - ((SUM(hybrid_count) * 100.0) / SUM(in_person_count + virtual_count + hybrid_count))::decimal(5,2) AS hybrid_percentage + (COALESCE((SUM(in_person_count) * 100.0) / NULLIF(SUM(in_person_count + virtual_count + hybrid_count),0),0))::decimal(5,2) AS in_person_percentage, + (COALESCE((SUM(virtual_count) * 100.0) / NULLIF(SUM(in_person_count + virtual_count + hybrid_count),0),0))::decimal(5,2) AS virtual_percentage, + (COALESCE((SUM(hybrid_count) * 100.0) / NULLIF(SUM(in_person_count + virtual_count + hybrid_count),0),0))::decimal(5,2) AS hybrid_percentage FROM delivery_method_graph_values ), delivery_method_graph AS ( @@ -1122,7 +1122,7 @@ WITH SELECT COALESCE(r.name, a."creatorRole"::text) AS role_name, COUNT(*) AS role_count, - ((COUNT(*) * 100.0) / SUM(COUNT(*)) OVER ())::decimal(5,2) AS percentage + (COALESCE((COUNT(*) * 100.0) / NULLIF(SUM(COUNT(*),0)) OVER (),0))::decimal(5,2) AS percentage FROM "ActivityReports" a JOIN filtered_activity_reports far ON a.id = far.id diff --git a/src/queries/api/dashboards/qa/fei.sql b/src/queries/api/dashboards/qa/fei.sql index 4a0633182b..35c58302c5 100644 --- a/src/queries/api/dashboards/qa/fei.sql +++ b/src/queries/api/dashboards/qa/fei.sql @@ -584,11 +584,11 @@ WITH ), with_fei_widget AS ( SELECT - (((COUNT(DISTINCT wf.id) FILTER (WHERE has_fei)::decimal/ - COUNT(DISTINCT wf.id)))*100)::decimal(5,2) "% recipients with fei", + (COALESCE((COUNT(DISTINCT wf.id) FILTER (WHERE has_fei)::decimal/ + NULLIF(COUNT(DISTINCT wf.id),0)),0)*100)::decimal(5,2) "% recipients with fei", COUNT(DISTINCT wf.id) FILTER (WHERE wf.has_fei) "recipients with fei", COUNT(DISTINCT wf.id) total, - SUM(grant_count) "grants with fei" + COALESCE(SUM(grant_count),0) "grants with fei" FROM with_fei wf ), @@ -640,7 +640,7 @@ WITH SELECT wfpr.response, COUNT(*) AS response_count, - ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 0)::decimal(5,2) AS percentage + ROUND(COALESCE(COUNT(*) * 100.0 / NULLIF(SUM(COUNT(*)) OVER (),0),0), 0)::decimal(5,2) AS percentage FROM with_fei_page wfp CROSS JOIN UNNEST(wfp."rootCause") wfpr(response) GROUP BY 1 diff --git a/src/queries/api/dashboards/qa/no-tta.sql b/src/queries/api/dashboards/qa/no-tta.sql index f19347a5be..b870cb9064 100644 --- a/src/queries/api/dashboards/qa/no-tta.sql +++ b/src/queries/api/dashboards/qa/no-tta.sql @@ -460,7 +460,7 @@ no_tta AS ( ), no_tta_widget AS ( SELECT - (((COUNT(*) FILTER (WHERE NOT has_tta))::decimal/COUNT(*))*100)::decimal(5,2) "% recipients without tta", + (COALESCE((COUNT(*) FILTER (WHERE NOT has_tta))::decimal/NULLIF(COUNT(*),0),0)*100)::decimal(5,2) "% recipients without tta", COUNT(*) FILTER (WHERE not has_tta ) "recipients without tta", COUNT(*) total FROM no_tta From faf7c44984e2aa6a62ce652597f46e1d4b8ea8cd Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Thu, 17 Oct 2024 14:57:40 -0400 Subject: [PATCH 25/28] code updates per Matt --- frontend/src/fetchers/ssdi.js | 5 ++--- frontend/src/hooks/useWidgetSorting.js | 1 + .../src/pages/QADashboard/Components/GoalCard.js | 2 +- .../__tests__/index.js | 11 +++++++++++ .../RecipientsWithClassScoresAndGoals/index.js | 4 ++-- .../RecipientsWithNoTta/__tests__/index.js | 13 +++++++++++++ .../pages/QADashboard/RecipientsWithNoTta/index.js | 4 ++-- .../__tests__/index.js | 13 +++++++++++++ .../RecipientsWithOhsStandardFeiGoal/index.js | 4 ++-- frontend/src/pages/QADashboard/index.js | 8 ++++---- frontend/src/widgets/DeliveryMethodGraph.js | 6 ++---- .../src/widgets/PercentageActivityReportByRole.js | 9 ++------- frontend/src/widgets/RootCauseFeiGoals.js | 9 ++------- 13 files changed, 57 insertions(+), 32 deletions(-) diff --git a/frontend/src/fetchers/ssdi.js b/frontend/src/fetchers/ssdi.js index 68066bd839..a50402d529 100644 --- a/frontend/src/fetchers/ssdi.js +++ b/frontend/src/fetchers/ssdi.js @@ -46,7 +46,6 @@ const allowedTopicsForQuery = { ], 'qa-dashboard': [...QA_DASHBOARD_FILTER_CONFIG.map((filter) => filter), 'region', - 'regionIds', ], }; @@ -76,9 +75,9 @@ const getSelfServiceUrl = (filterName, filters) => { return `${baseUrl}?${queryString}`; }; -export const getSelfServiceData = async (filterName, filters, dataSetSelection) => { +export const getSelfServiceData = async (filterName, filters, dataSetSelection = []) => { const url = getSelfServiceUrl(filterName, filters); - const urlToUse = url + (dataSetSelection && dataSetSelection.length ? dataSetSelection.map((s) => `&dataSetSelection[]=${s}`).join('') : ''); + const urlToUse = url + dataSetSelection.map((s) => `&dataSetSelection[]=${s}`).join(''); const response = await get(urlToUse); if (!response.ok) { throw new Error('Error fetching self service data'); diff --git a/frontend/src/hooks/useWidgetSorting.js b/frontend/src/hooks/useWidgetSorting.js index 457d61a1e2..cef8df7946 100644 --- a/frontend/src/hooks/useWidgetSorting.js +++ b/frontend/src/hooks/useWidgetSorting.js @@ -25,6 +25,7 @@ export default function useWidgetSorting( const requestSort = useCallback((sortBy, passedDirection = null) => { // Get sort direction. let direction = 'asc'; + // If we have a passed direction this means that we are sorting via a dropdown and not arrow. if (passedDirection) { // If the direction is passed, use it. direction = passedDirection; diff --git a/frontend/src/pages/QADashboard/Components/GoalCard.js b/frontend/src/pages/QADashboard/Components/GoalCard.js index 43dc1e6a39..4b1f5fce81 100644 --- a/frontend/src/pages/QADashboard/Components/GoalCard.js +++ b/frontend/src/pages/QADashboard/Components/GoalCard.js @@ -17,7 +17,7 @@ function GoalCard({ + {goal.goalNumber} )} diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js index 67a7ccdf0f..bf40a403f8 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/__tests__/index.js @@ -174,4 +174,15 @@ describe('Recipients With Class and Scores and Goals', () => { pillRemoveButton.click(); expect(screen.queryAllByText(/2 selected/i).length).toBe(0); }); + + it('handles error on fetch', async () => { + fetchMock.get(dashboardApi, 500); + renderRecipientsWithClassScoresAndGoals(); + + await act(async () => { + await waitFor(() => { + expect(screen.getByText(/Unable to fetch QA data/i)).toBeInTheDocument(); + }); + }); + }); }); diff --git a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js index 0eb14ce18c..d608916b31 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithClassScoresAndGoals/index.js @@ -62,7 +62,7 @@ export default function RecipientsWithClassScoresAndGoals() { ); useDeepCompareEffect(() => { - async function fetchQaDat() { + async function fetchQaData() { setIsLoading(true); // Filters passed also contains region. try { @@ -176,7 +176,7 @@ export default function RecipientsWithClassScoresAndGoals() { } } // Call resources fetch. - fetchQaDat(); + fetchQaData(); }, [filters]); return ( diff --git a/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js index e3d2ada256..a50e2ab9ec 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithNoTta/__tests__/index.js @@ -148,4 +148,17 @@ describe('Recipients With Ohs Standard Fei Goal', () => { const option = select.querySelector('option[value="region"]'); expect(option).toBeNull(); }); + + it('correctly handles an error on fetch', async () => { + fetchMock.get('/api/ssdi/api/dashboards/qa/no-tta.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=no_tta_widget&dataSetSelection[]=no_tta_page', 500); + renderRecipientsWithNoTta(); + expect(screen.queryAllByText(/recipients with no tta/i).length).toBe(2); + expect(screen.getByText(/Recipients without Activity Reports or Training Reports for more than 90 days./i)).toBeInTheDocument(); + + await act(async () => { + await waitFor(async () => { + expect(screen.getByText(/unable to fetch qa data/i)).toBeInTheDocument(); + }); + }); + }); }); diff --git a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js index ab61147faf..de03544a4a 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithNoTta/index.js @@ -54,7 +54,7 @@ export default function RecipientsWithNoTta() { ); useDeepCompareEffect(() => { - async function fetchQaDat() { + async function fetchQaData() { setIsLoading(true); // Filters passed also contains region. try { @@ -113,7 +113,7 @@ export default function RecipientsWithNoTta() { } } // Call resources fetch. - fetchQaDat(); + fetchQaData(); }, [filters]); return ( diff --git a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js index f07e88f85c..0fd3d50ba6 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/__tests__/index.js @@ -167,4 +167,17 @@ describe('Recipients With Ohs Standard Fei Goal', () => { const option = select.querySelector('option[value="region"]'); expect(option).toBeNull(); }); + + it('handles error on fetch', async () => { + fetchMock.get('/api/ssdi/api/dashboards/qa/fei.sql?region.in[]=1®ion.in[]=2&dataSetSelection[]=with_fei_widget&dataSetSelection[]=with_fei_page', 500); + renderRecipientsWithOhsStandardFeiGoal(); + + expect(screen.queryAllByRole('heading', { name: /recipients with ohs standard fei goal/i }).length).toBe(1); + expect(screen.getByText(/root causes were identified through self-reported data\./i)).toBeInTheDocument(); + await act(async () => { + await waitFor(() => { + expect(screen.getByText(/unable to fetch qa data/i)).toBeInTheDocument(); + }); + }); + }); }); diff --git a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js index 2caccac299..18847c51ce 100644 --- a/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js +++ b/frontend/src/pages/QADashboard/RecipientsWithOhsStandardFeiGoal/index.js @@ -57,7 +57,7 @@ export default function RecipientsWithOhsStandardFeiGoal() { ); useDeepCompareEffect(() => { - async function fetchQaDat() { + async function fetchQaData() { setIsLoading(true); // Filters passed also contains region. try { @@ -127,7 +127,7 @@ export default function RecipientsWithOhsStandardFeiGoal() { } } // Call resources fetch. - fetchQaDat(); + fetchQaData(); }, [filters]); return ( diff --git a/frontend/src/pages/QADashboard/index.js b/frontend/src/pages/QADashboard/index.js index cee6a3c4c7..ceae28ab6e 100644 --- a/frontend/src/pages/QADashboard/index.js +++ b/frontend/src/pages/QADashboard/index.js @@ -22,6 +22,7 @@ import ContentFromFeedByTag from '../../components/ContentFromFeedByTag'; import PercentageActivityReportByRole from '../../widgets/PercentageActivityReportByRole'; import RootCauseFeiGoals from '../../widgets/RootCauseFeiGoals'; import { getSelfServiceData, containsFiltersThatAreNotApplicable } from '../../fetchers/ssdi'; +import Loader from '../../components/Loader'; const DISALLOWED_FILTERS = [ 'domainClassroomOrganization', @@ -165,6 +166,7 @@ export default function QADashboard() { {error} )} +
- + diff --git a/frontend/src/widgets/DeliveryMethodGraph.js b/frontend/src/widgets/DeliveryMethodGraph.js index e2ea5ebbe1..ec3134d04a 100644 --- a/frontend/src/widgets/DeliveryMethodGraph.js +++ b/frontend/src/widgets/DeliveryMethodGraph.js @@ -34,7 +34,7 @@ const DEFAULT_SORT_CONFIG = { const KEY_COLUMNS = ['Months']; -export default function DeliveryMethodGraph({ data, loading }) { +export default function DeliveryMethodGraph({ data }) { const widgetRef = useRef(null); const capture = useMediaCapture(widgetRef, 'Total TTA hours'); const [showTabularData, setShowTabularData] = useState(false); @@ -236,7 +236,7 @@ export default function DeliveryMethodGraph({ data, loading }) { return ( Date: Thu, 17 Oct 2024 13:27:32 -0700 Subject: [PATCH 26/28] fix bugs and add more filters --- src/queries/api/dashboards/qa/dashboard.sql | 47 ++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/src/queries/api/dashboards/qa/dashboard.sql b/src/queries/api/dashboards/qa/dashboard.sql index 5ef32e757f..22b5ad0370 100644 --- a/src/queries/api/dashboards/qa/dashboard.sql +++ b/src/queries/api/dashboards/qa/dashboard.sql @@ -220,6 +220,28 @@ JSON: { "display": "Activity Report Goal Response", "description": "Filter based on goal field responses in activity reports.", "supportsExclusion": true + }, + { + "name": "startDate", + "type": "date[]", + "display": "Start Date", + "description": "Filter based on the start date of the activity reports.", + "supportsExclusion": true + }, + { + "name": "endDate", + "type": "date[]", + "display": "End Date", + "description": "Filter based on the end date of the activity reports.", + "supportsExclusion": true + }, + { + "name": "reportId", + "type": "string[]", + "display": "Report Ids", + "description": "Filter based on the report ids.", + "supportsExclusion": true, + "supportsFuzzyMatch": true } ] } @@ -237,6 +259,7 @@ DECLARE goal_name_filter TEXT := NULLIF(current_setting('ssdi.goalName', true), ''); create_date_filter TEXT := NULLIF(current_setting('ssdi.createDate', true), ''); activity_report_goal_response_filter TEXT := NULLIF(current_setting('ssdi.activityReportGoalResponse', true), ''); + report_id_filter TEXT := NULLIF(current_setting('ssdi.reportId', true), ''); start_date_filter TEXT := NULLIF(current_setting('ssdi.startDate', true), ''); end_date_filter TEXT := NULLIF(current_setting('ssdi.endDate', true), ''); reason_filter TEXT := NULLIF(current_setting('ssdi.reason', true), ''); @@ -258,6 +281,7 @@ DECLARE goal_name_not_filter BOOLEAN := COALESCE(current_setting('ssdi.goalName.not', true), 'false') = 'true'; create_date_not_filter BOOLEAN := COALESCE(current_setting('ssdi.createDate.not', true), 'false') = 'true'; activity_report_goal_response_not_filter BOOLEAN := COALESCE(current_setting('ssdi.activityReportGoalResponse.not', true), 'false') = 'true'; + report_id_not_filter BOOLEAN := COALESCE(current_setting('ssdi.reportId.not', true), 'false') = 'true'; start_date_not_filter BOOLEAN := COALESCE(current_setting('ssdi.startDate.not', true), 'false') = 'true'; end_date_not_filter BOOLEAN := COALESCE(current_setting('ssdi.endDate.not', true), 'false') = 'true'; reason_not_filter BOOLEAN := COALESCE(current_setting('ssdi.reason.not', true), 'false') = 'true'; @@ -612,6 +636,7 @@ BEGIN --------------------------------------------------------------------------------------------------- -- Step 3.2: If activity reports filters (set 1), delete from filtered_activity_reports for any activity reports filtered, delete from filtered_goals using filterd_activity_reports, delete from filtered_grants using filtered_goals IF + report_id_filter IS NOT NULL OR start_date_filter IS NOT NULL OR end_date_filter IS NOT NULL OR reason_filter IS NOT NULL OR @@ -626,6 +651,18 @@ BEGIN JOIN "ActivityReports" a ON fa.id = a.id WHERE a."calculatedStatus" = 'approved' + -- Filter for reportId if ssdi.reportId is defined + AND ( + report_id_filter IS NULL + OR ( + EXISTS ( + SELECT 1 + FROM json_array_elements_text(COALESCE(report_text_filter, '[]')::json) AS value + WHERE CONCAT('R', LPAD(a."regionId"::text, 2, '0'), '-AR-', a.id) ~* value::text + OR COALESCE(a."legacyId",'') ~* value::text + ) != report_text_not_filter + ) + ) -- Filter for startDate dates between two values if ssdi.startDate is defined AND ( start_date_filter IS NULL @@ -658,7 +695,7 @@ BEGIN AND ( reason_filter IS NULL OR ( - (a."reason"::string[] && ARRAY( + (a."reason"::TEXT[] && ARRAY( SELECT value::text FROM json_array_elements_text(COALESCE(reason_filter, '[]')::json) )) != reason_not_filter @@ -668,7 +705,7 @@ BEGIN AND ( target_populations_filter IS NULL OR ( - (a."targetPopulations"::string[] && ARRAY( + (a."targetPopulations"::TEXT[] && ARRAY( SELECT value::text FROM json_array_elements_text(COALESCE(target_populations_filter, '[]')::json) )) != target_populations_not_filter @@ -679,11 +716,11 @@ BEGIN tta_type_filter IS NULL OR ( ( - a."ttaType"::string[] @> ARRAY( + a."ttaType"::TEXT[] @> ARRAY( SELECT value::text FROM json_array_elements_text(COALESCE(tta_type_filter, '[]')::json) ) - AND a."ttaType"::string[] <@ ARRAY( + AND a."ttaType"::TEXT[] <@ ARRAY( SELECT value::text FROM json_array_elements_text(COALESCE(tta_type_filter, '[]')::json) ) @@ -1122,7 +1159,7 @@ WITH SELECT COALESCE(r.name, a."creatorRole"::text) AS role_name, COUNT(*) AS role_count, - (COALESCE((COUNT(*) * 100.0) / NULLIF(SUM(COUNT(*),0)) OVER (),0))::decimal(5,2) AS percentage + (COALESCE((COUNT(*) * 100.0) / NULLIF(SUM(COUNT(*)) OVER (), 0), 0))::decimal(5,2) AS percentage FROM "ActivityReports" a JOIN filtered_activity_reports far ON a.id = far.id From f2e6a87b5f3a4aad828a0472330f60cd40f692e8 Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Thu, 17 Oct 2024 17:28:52 -0400 Subject: [PATCH 27/28] add report id to dashboard async and handle for no data returned --- frontend/src/fetchers/ssdi.js | 1 + frontend/src/widgets/DeliveryMethodGraph.js | 8 ++++---- frontend/src/widgets/PercentageActivityReportByRole.js | 2 +- frontend/src/widgets/RootCauseFeiGoals.js | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/fetchers/ssdi.js b/frontend/src/fetchers/ssdi.js index a50402d529..29b2fcdc30 100644 --- a/frontend/src/fetchers/ssdi.js +++ b/frontend/src/fetchers/ssdi.js @@ -46,6 +46,7 @@ const allowedTopicsForQuery = { ], 'qa-dashboard': [...QA_DASHBOARD_FILTER_CONFIG.map((filter) => filter), 'region', + 'reportId', ], }; diff --git a/frontend/src/widgets/DeliveryMethodGraph.js b/frontend/src/widgets/DeliveryMethodGraph.js index ec3134d04a..be3b7553f4 100644 --- a/frontend/src/widgets/DeliveryMethodGraph.js +++ b/frontend/src/widgets/DeliveryMethodGraph.js @@ -125,7 +125,7 @@ export default function DeliveryMethodGraph({ data }) { x: [], y: [], name: 'Hybrid', traceOrder: 3, }); - records.forEach((dataset, index) => { + (records || []).forEach((dataset, index) => { tableData.push({ heading: moment(dataset.month, 'YYYY-MM-DD').format('MMM YYYY'), sortKey: index + 1, @@ -177,11 +177,11 @@ export default function DeliveryMethodGraph({ data }) { setTraces(Array.from(traceMap.values())); setTabularData(tableData); setTotals({ - totalInPerson: totalInPerson.toLocaleString('en-us'), + totalInPerson: totalInPerson ? totalInPerson.toLocaleString('en-us') : 0, averageInPersonPercentage: `${averageInPersonPercentage}%`, - totalVirtualCount: totalVirtualCount.toLocaleString('en-us'), + totalVirtualCount: totalVirtualCount ? totalVirtualCount.toLocaleString('en-us') : 0, averageVirtualPercentage: `${averageVirtualPercentage}%`, - totalHybridCount: totalHybridCount.toLocaleString('en-us'), + totalHybridCount: totalHybridCount ? totalHybridCount.toLocaleString('en-us') : 0, averageHybridPercentage: `${averageHybridPercentage}%`, }); }, [data]); diff --git a/frontend/src/widgets/PercentageActivityReportByRole.js b/frontend/src/widgets/PercentageActivityReportByRole.js index 493333d8d1..37cdf20247 100644 --- a/frontend/src/widgets/PercentageActivityReportByRole.js +++ b/frontend/src/widgets/PercentageActivityReportByRole.js @@ -88,7 +88,7 @@ export default function PercentageActivityReportByRole({ data }) { const tableData = []; const traceData = []; - records.forEach((dataset, index) => { + (records || []).forEach((dataset, index) => { traceData.push({ name: dataset.role_name, count: dataset.percentage, diff --git a/frontend/src/widgets/RootCauseFeiGoals.js b/frontend/src/widgets/RootCauseFeiGoals.js index ad5a4dc809..a08bfa031b 100644 --- a/frontend/src/widgets/RootCauseFeiGoals.js +++ b/frontend/src/widgets/RootCauseFeiGoals.js @@ -90,7 +90,7 @@ export default function RootCauseFeiGoals({ data }) { const tableData = []; const traceData = []; - records.forEach((dataset, index) => { + (records || []).forEach((dataset, index) => { traceData.push({ category: dataset.rootCause, count: dataset.percentage, From 4021036b78bda66e020ce127d221376118cf9629 Mon Sep 17 00:00:00 2001 From: Adam Levin Date: Fri, 18 Oct 2024 12:06:33 -0400 Subject: [PATCH 28/28] add coverage for null checks --- .../src/pages/QADashboard/__tests__/index.js | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/frontend/src/pages/QADashboard/__tests__/index.js b/frontend/src/pages/QADashboard/__tests__/index.js index c2d133e9cb..cacf2c0678 100644 --- a/frontend/src/pages/QADashboard/__tests__/index.js +++ b/frontend/src/pages/QADashboard/__tests__/index.js @@ -258,4 +258,141 @@ describe('Resource Dashboard page', () => { const option = select.querySelector('option[value="region"]'); expect(option).toBeNull(); }); + + it('renders the graphs correctly if the records are null', async () => { + fetchMock.restore(); + // Mock Recipients with no TTA data. + fetchMock.get(noTtaApi, [ + { + data_set: 'no_tta_widget', + records: '1', + data: [ + { + total: 1460, + 'recipients without tta': 941, + '% recipients without tta': 64.45, + }, + ], + }, + ]); + + // Mock Recipients with OHS standard FEI goal data. + fetchMock.get(feiApi, [ + { + data_set: 'with_fei_graph', + records: '6', + data: [ + { + rootCause: 'Facilities', + percentage: 9, + response_count: 335, + }, + { + rootCause: 'Workforce', + percentage: 46, + response_count: 1656, + }, + { + rootCause: 'Community Partnerships', + percentage: 8, + response_count: 275, + }, + { + rootCause: 'Family Circumstances', + percentage: 11, + response_count: 393, + }, + { + rootCause: 'Other ECE Care Options', + percentage: 18, + response_count: 639, + }, + { + rootCause: 'Unavailable', + percentage: 8, + response_count: 295, + }, + ], + active_filters: [ + 'currentUserId', + ], + }, + { + data_set: 'with_fei_widget', + records: '1', + data: [ + { + total: 1550, + 'grants with fei': 1042, + 'recipients with fei': 858, + '% recipients with fei': 55.35, + }, + ], + active_filters: [ + 'currentUserId', + ], + }, + ]); + + // Mock Recipients with OHS standard CLASS data. + fetchMock.get(classApi, [ + { + data_set: 'with_class_widget', + records: '1', + data: [ + { + total: 1550, + 'grants with class': 327, + 'recipients with class': 267, + '% recipients with class': 17.23, + }, + ], + active_filters: [ + 'currentUserId', + ], + }, + ]); + + // Mock Dashboard data. + fetchMock.get(dashboardApi, [ + { + data_set: 'role_graph', + records: '0', + data: null, + }, + { + data_set: 'delivery_method_graph', + records: '1', + data: [ + { + month: 'Total', + hybrid_count: null, + virtual_count: null, + in_person_count: null, + hybrid_percentage: 0, + virtual_percentage: 0, + in_person_percentage: 0, + }, + ], + }, + ]); + renderQADashboard(); + + // Header + expect(await screen.findByText('Quality assurance dashboard')).toBeVisible(); + + // Overview + expect(await screen.findByText('Recipients with no TTA')).toBeVisible(); + expect(await screen.findByText('Recipients with OHS standard FEI goal')).toBeVisible(); + expect(await screen.findByText('Recipients with OHS standard CLASS goal')).toBeVisible(); + + // Assert test data. + await act(async () => { + await waitFor(() => { + expect(screen.getByText('Delivery method')).toBeVisible(); + expect(screen.getByText('Percentage of activity reports by role')).toBeVisible(); + expect(screen.getByText('Root cause on FEI goals')).toBeVisible(); + }); + }); + }); });