Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
994f818
Compliant Follow-up Reviews with TTA Support widget
Andrew565 Jun 5, 2026
cf7e2b6
fixes and updates to get graph to show and with proper formatting
AdamAdHocTeam Jun 9, 2026
e5b7c73
fix table export and total table row
AdamAdHocTeam Jun 9, 2026
9a56d96
Dynamic label spacing to prevent squishing of small values
AdamAdHocTeam Jun 9, 2026
2ec26d9
suggested change to how the widget scopes are implemented, plus tests
hardwarehuman Jun 9, 2026
1b08cd3
Apply suggestions from code review
Andrew565 Jun 11, 2026
5fc7bc7
streamline sql filtering
hardwarehuman Jun 16, 2026
1e94c4b
Merge remote-tracking branch 'origin/main' into 5344-5348-backend-sug…
Andrew565 Jun 16, 2026
0e7bb07
Merge pull request #3686 from HHS/5344-5348-backend-suggestion
Andrew565 Jun 16, 2026
fdce45d
Apply suggestions from code review
Andrew565 Jun 17, 2026
a802a68
Apply scopes.activityReport to TTA support check in compliantFollowUp…
Copilot Jun 17, 2026
2670bf7
Revert "Apply scopes.activityReport to TTA support check in compliant…
Andrew565 Jun 17, 2026
532636f
Adding frontend tests
Andrew565 Jun 18, 2026
0204549
Putting widget behind feature flag and changing DeliveredReviews to "…
Andrew565 Jun 18, 2026
2a4da3d
Merge remote-tracking branch 'origin/main' into 5344-5348-compliant-r…
Andrew565 Jun 18, 2026
54a7ada
Updates for tests
Andrew565 Jun 19, 2026
bf505f6
Fix title casing to be sentence case
Andrew565 Jun 23, 2026
c6dab3e
Hiding annotations from graph
Andrew565 Jun 23, 2026
0c08da9
Renaming migration file again
Andrew565 Jun 23, 2026
3d2a866
Merge remote-tracking branch 'origin/main' into 5344-5348-compliant-r…
Andrew565 Jun 23, 2026
9b5a111
Removing inner text numbers from graph
Andrew565 Jun 23, 2026
abb944c
Fixing help drawer and graph order
Andrew565 Jun 23, 2026
df4f228
Moving CompliantReviewsGrid to separate file
Andrew565 Jun 23, 2026
d9078fa
Adding grid tests
Andrew565 Jun 23, 2026
730fe79
Adding more frontend tests
Andrew565 Jun 24, 2026
c385016
Merge remote-tracking branch 'origin/main' into 5344-5348-compliant-r…
Andrew565 Jun 24, 2026
0ccb9f2
Fix to sticky last cell
Andrew565 Jun 24, 2026
07cc746
Removing no-longer-needed label and annotation code from reviews grid…
Andrew565 Jun 24, 2026
8aaa920
More frontend tests
Andrew565 Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/logical_data_model.encoded

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/logical_data_model.puml
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,7 @@ class Users{

enum enum_Users_flags {
actionable_notifications
compliant_follow_up_reviews_tta_support
monitoring-regional-dashboard
quality_assurance_dashboard
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import { MemoryRouter } from 'react-router';
import AppLoadingContext from '../../../AppLoadingContext';
import UserContext from '../../../UserContext';
import ActiveDeficientCitationsWithTtaSupport from '../../../widgets/ActiveDeficientCitationsWithTtaSupport';
import CompliantFollowUpReviewsWithTtaSupport from '../../../widgets/CompliantFollowUpReviewsWithTtaSupport';
import MonitoringReportDashboardOverview from '../../../widgets/MonitoringReportDashboardOverview';
import MonitoringReportDashboard from '../components/MonitoringReportDashboard';

jest.mock('../../../widgets/MonitoringReportDashboardOverview');
jest.mock('../../../widgets/ActiveDeficientCitationsWithTtaSupport');
jest.mock('../../../widgets/CompliantFollowUpReviewsWithTtaSupport');
jest.mock('../../../widgets/ActiveNoncompliantCitationsWithTtaSupport', () => () => (
<div data-testid="noncompliant-citations-widget" />
));
jest.mock('../../../widgets/MonitoringRelatedTta', () => () => (
<div data-testid="related-tta-widget" />
));
Expand All @@ -25,11 +30,14 @@ describe('MonitoringReportDashboard', () => {
ActiveDeficientCitationsWithTtaSupport.mockImplementation(({ filters }) => (
<div data-testid="citations-widget">{JSON.stringify(filters)}</div>
));
CompliantFollowUpReviewsWithTtaSupport.mockImplementation(({ filters }) => (
<div data-testid="compliant-follow-up-widget">{JSON.stringify(filters)}</div>
));
});

const renderDashboard = (filtersToApply = []) =>
const renderDashboard = (filtersToApply = [], user = { id: 1, flags: [] }) =>
render(
<UserContext.Provider value={{ user: { id: 1, flags: [] } }}>
<UserContext.Provider value={{ user }}>
<MemoryRouter>
<AppLoadingContext.Provider value={{ setIsAppLoading: jest.fn() }}>
<MonitoringReportDashboard filtersToApply={filtersToApply} />
Expand All @@ -43,6 +51,7 @@ describe('MonitoringReportDashboard', () => {

expect(screen.getByTestId('overview-widget')).toBeInTheDocument();
expect(screen.getByTestId('citations-widget')).toBeInTheDocument();
expect(screen.queryByTestId('compliant-follow-up-widget')).not.toBeInTheDocument();
});

it('passes merged filters including default startDate filter to both widgets', () => {
Expand All @@ -68,4 +77,26 @@ describe('MonitoringReportDashboard', () => {
expect(overviewFilters[0]).toEqual(incomingFilters[0]);
expect(citationsFilters[0]).toEqual(incomingFilters[0]);
});

it('renders the compliant follow-up widget only when the feature flag is enabled', () => {
const incomingFilters = [
{
id: 'f1',
topic: 'region',
condition: 'is',
query: '1',
},
];

renderDashboard(incomingFilters, {
id: 1,
flags: ['compliant_follow_up_reviews_tta_support'],
});

expect(screen.getByTestId('compliant-follow-up-widget')).toBeInTheDocument();
expect(CompliantFollowUpReviewsWithTtaSupport).toHaveBeenCalledWith(
expect.objectContaining({ filters: incomingFilters }),
expect.anything()
);
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Grid } from '@trussworks/react-uswds';
import PropTypes from 'prop-types';
import React from 'react';
import FeatureFlag from '../../../components/FeatureFlag';
import ActiveDeficientCitationsWithTtaSupport from '../../../widgets/ActiveDeficientCitationsWithTtaSupport';
import ActiveNoncompliantCitationsWithTtaSupport from '../../../widgets/ActiveNoncompliantCitationsWithTtaSupport';
import CompliantFollowUpReviewsWithTtaSupport from '../../../widgets/CompliantFollowUpReviewsWithTtaSupport';
import FindingCategoryHotspot from '../../../widgets/FindingCategoryHotspot';
import MonitoringRelatedTta from '../../../widgets/MonitoringRelatedTta';
import MonitoringReportDashboardOverview from '../../../widgets/MonitoringReportDashboardOverview';
Expand All @@ -13,6 +15,11 @@ export default function MonitoringReportDashboard({ filtersToApply }) {
<Grid row gap>
<MonitoringReportDashboardOverview filters={filtersToApply} loading={false} />
</Grid>
<Grid row>
<FeatureFlag flag="compliant_follow_up_reviews_tta_support">
<CompliantFollowUpReviewsWithTtaSupport filters={filtersToApply} />
</FeatureFlag>
</Grid>
<Grid row>
<ActiveDeficientCitationsWithTtaSupport filters={filtersToApply} />
</Grid>
Expand Down
Empty file.
177 changes: 177 additions & 0 deletions frontend/src/widgets/CompliantFollowUpReviewsWithTtaSupport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import PropTypes from 'prop-types';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a large file can anything be broken out into a helper file?

import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import AppLoadingContext from '../AppLoadingContext';
import ContentFromFeedByTag from '../components/ContentFromFeedByTag';
import Drawer from '../components/Drawer';
import DrawerTriggerButton from '../components/DrawerTriggerButton';
import WidgetContainer from '../components/WidgetContainer';
import WidgetContainerSubtitle from '../components/WidgetContainer/WidgetContainerSubtitle';
import useMediaCapture from '../hooks/useMediaCapture';
import useWidgetExport from '../hooks/useWidgetExport';
import useWidgetMenuItems from '../hooks/useWidgetMenuItems';
import CompliantReviewsGrid from './CompliantReviewsGrid';
import HorizontalTableWidget from './HorizontalTableWidget';
import withWidgetData from './withWidgetData';
import './CompliantFollowUpReviewsWithTtaSupport.css';
import NoResultsFound from '../components/NoResultsFound';

const EXPORT_NAME = 'Compliant follow-up reviews with TTA support';

export function CompliantFollowUpReviewsWithTtaSupport({ loading, data }) {
const { setIsAppLoading } = useContext(AppLoadingContext);
const drawerTriggerRef = useRef(null);
const widgetRef = useRef(null);
const capture = useMediaCapture(widgetRef, EXPORT_NAME);
const [showTabularData, setShowTabularData] = useState(false);

Comment thread
Andrew565 marked this conversation as resolved.
useEffect(() => {
setIsAppLoading(loading);
}, [loading, setIsAppLoading]);

const months = useMemo(() => {
if (!data?.months?.length) return [];
return data.months;
}, [data]);
Comment thread
Andrew565 marked this conversation as resolved.

// Build rows for HorizontalTableWidget (table view / export)
// Separate non-Total rows from the Total row so it can go in tfoot (bold)
const { tableData, footerData } = useMemo(() => {
const reviews = data?.reviews || [];
const nonTotalRows = reviews.filter((row) => !/total/i.test(row.name));
const totalRow = reviews.find((row) => /total/i.test(row.name));

const rows = nonTotalRows.map((row) => ({
heading: row.name,
id: row.name,
tooltip: true,
hideSortingIndicator: true,
data: [
...row.values.map((value) => ({ value: value.toString() })),
{ value: row.values.reduce((sum, v) => sum + Number(v), 0).toString() },
],
}));

const footer = totalRow
? [
'Total',
...totalRow.values.map(String),
totalRow.values.reduce((sum, v) => sum + Number(v), 0).toString(),
]
: false;

return { tableData: rows, footerData: footer };
}, [data]);

const { exportRows } = useWidgetExport(
tableData,
[...(months || []), 'Total'],
{},
'Follow-up reviews',
EXPORT_NAME
);

const menuItems = useWidgetMenuItems(
Comment thread
Andrew565 marked this conversation as resolved.
showTabularData,
setShowTabularData,
capture,
{},
exportRows
);

const subtitle = (
<div className="margin-bottom-3">
<WidgetContainerSubtitle>
Compliant follow-up reviews, broken out by those with and without citations addressed by
approved activity reports during the correction period.
</WidgetContainerSubtitle>
<div className="margin-top-1">
<DrawerTriggerButton drawerTriggerRef={drawerTriggerRef}>
About this data
</DrawerTriggerButton>
</div>
Comment thread
Andrew565 marked this conversation as resolved.
</div>
);

const showEmptyState = !loading && !data?.months?.length;
if (showEmptyState) {
return (
<>
<Drawer triggerRef={drawerTriggerRef} title="Compliant follow-up reviews with TTA support">
<ContentFromFeedByTag tagName="ttahub-compliant-follow-up-reviews" />
</Drawer>
<WidgetContainer
title="Compliant follow-up reviews with TTA support"
subtitle={subtitle}
menuItems={[]}
loading={loading}
titleMargin={{ bottom: 1 }}
>
<NoResultsFound
drawerConfig={{
tagName: 'ttahub-regional-dash-monitoring-filters',
title: 'Monitoring dashboard filters',
}}
/>
</WidgetContainer>
</>
);
}

return (
<>
<Drawer triggerRef={drawerTriggerRef} title="Compliant follow-up reviews with TTA support">
<ContentFromFeedByTag tagName="ttahub-compliant-follow-up-reviews" />
</Drawer>
<WidgetContainer
title="Compliant follow-up reviews with TTA support"
subtitle={subtitle}
menuItems={menuItems}
loading={loading}
titleMargin={{ bottom: 1 }}
>
{showTabularData ? (
<HorizontalTableWidget
headers={months}
data={tableData}
caption="Compliant follow-up reviews with TTA support"
firstHeading="Follow-up reviews"
lastHeading="Totals"
showTotalColumn
stickyFirstColumn
stickyLastColumn
enableCheckboxes={false}
selectAllIdPrefix="compliant-follow-up-reviews"
hideFirstColumnBorder
footerData={footerData}
/>
) : (
<CompliantReviewsGrid data={data} widgetRef={widgetRef} />
)}
Comment thread
Andrew565 marked this conversation as resolved.
</WidgetContainer>
</>
);
}

CompliantFollowUpReviewsWithTtaSupport.propTypes = {
data: PropTypes.shape({
months: PropTypes.arrayOf(PropTypes.string),
reviews: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
values: PropTypes.arrayOf(PropTypes.number),
})
),
}),
loading: PropTypes.bool.isRequired,
filters: PropTypes.arrayOf(PropTypes.shape({})),
};

CompliantFollowUpReviewsWithTtaSupport.defaultProps = {
data: null,
filters: [],
};
Comment thread
Andrew565 marked this conversation as resolved.

export default withWidgetData(
CompliantFollowUpReviewsWithTtaSupport,
'compliantFollowUpReviewsWithTtaSupport'
);
121 changes: 121 additions & 0 deletions frontend/src/widgets/CompliantReviewsGrid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import createPlotlyComponent from 'react-plotly.js/factory';
import colors from '../colors';
import useSize from '../hooks/useSize';

let Plot = null;
import('plotly.js-basic-dist').then((Plotly) => {
Plot = createPlotlyComponent(Plotly);
});

const SERIES_COLORS = [colors.ttahubMediumBlue, colors.ttahubOrange, colors.ttahubMediumDeepTeal];

export default function CompliantReviewsGrid({ data, widgetRef }) {
const [plotData, setPlotData] = useState(null);
const size = useSize(widgetRef);

useEffect(() => {
if (!data || !size) return;
const { months, reviews } = data;
// Exclude Total; put "with TTA" first so "without TTA" renders on top in stacked mode
const filtered = (reviews || []).filter((s) => !/total/i.test(s.name));
const withTta = filtered.filter((s) => /with tta/i.test(s.name));
const withoutTta = filtered.filter((s) => !/with tta/i.test(s.name));
const ordered = [...withTta, ...withoutTta];

const traces = ordered.map((series, i) => ({
type: 'bar',
name: series.name,
x: months,
y: series.values,
text: [],
textposition: 'inside',
insidetextanchor: 'middle',
insidetextfont: { color: i === 0 ? '#fff' : colors.baseDarkest, size: 10 },
marker: { color: SERIES_COLORS[i % SERIES_COLORS.length] },
hovertemplate: '%{y}<extra></extra>',
hoverlabel: {
bgcolor: colors.baseDarkest,
bordercolor: colors.baseDarkest,
font: { color: '#fff', size: 16 },
},
}));

setPlotData({
traces,
layout: {
barmode: 'stack',
height: 350,
width: size.width,
margin: { l: 90, r: 20, t: 28, b: 80 },
font: { color: colors.baseDarkest },
xaxis: {
automargin: true,
title: {
text: 'Follow-up review received date',
font: { family: 'Source Sans Pro, sans-serif', size: 16 },
},
},
yaxis: {
tickformat: ',.0d',
autorange: true,
title: {
text: 'Compliant follow-up reviews',
font: { family: 'Source Sans Pro, sans-serif', size: 16 },
},
},
showlegend: false,
},
config: { responsive: true, displayModeBar: false },
});
}, [data, size]);

return (
<div ref={widgetRef} className="padding-3">
{plotData && (
<>
<div
style={{
display: 'flex',
justifyContent: 'center',
gap: '24px',
marginBottom: '8px',
fontFamily: 'Source Sans Pro, sans-serif',
fontSize: '16px',
}}
>
{plotData.traces.map((trace) => (
<div key={trace.name} style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div
style={{
width: '26px',
height: '26px',
borderRadius: '4px',
backgroundColor: trace.marker.color,
flexShrink: 0,
}}
/>
<span>{trace.name}</span>
</div>
))}
</div>
<Plot data={plotData.traces} layout={plotData.layout} config={plotData.config} />
</>
)}
</div>
);
}

CompliantReviewsGrid.propTypes = {
data: PropTypes.shape({
months: PropTypes.arrayOf(PropTypes.string),
reviews: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
values: PropTypes.arrayOf(PropTypes.number),
})
),
}).isRequired,
widgetRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }).isRequired,
};
Loading
Loading