Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import React from 'react';
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 @@ -22,6 +23,9 @@ export default function MonitoringReportDashboard({ filtersToApply }) {
<Grid row>
<FindingCategoryHotspot filters={filtersToApply} />
</Grid>
<Grid row>
<CompliantFollowUpReviewsWithTtaSupport filters={filtersToApply} />
</Grid>
<Grid row>
<MonitoringRelatedTta filters={filtersToApply} />
</Grid>
Expand Down
Empty file.
351 changes: 351 additions & 0 deletions frontend/src/widgets/CompliantFollowUpReviewsWithTtaSupport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
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 createPlotlyComponent from 'react-plotly.js/factory';
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 colors from '../colors';
import useMediaCapture from '../hooks/useMediaCapture';
import useSize from '../hooks/useSize';
import useWidgetExport from '../hooks/useWidgetExport';
import useWidgetMenuItems from '../hooks/useWidgetMenuItems';
import HorizontalTableWidget from './HorizontalTableWidget';
import withWidgetData from './withWidgetData';
import './CompliantFollowUpReviewsWithTtaSupport.css';
import NoResultsFound from '../components/NoResultsFound';

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

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

const SMALL_VALUE_LABEL_THRESHOLD = 2;
const MIN_INSIDE_LABEL_HEIGHT_PX = 14;

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 monthlyTotals = months.map((_, monthIndex) => ordered
.reduce((sum, series) => sum + Number(series.values?.[monthIndex] || 0), 0));
const maxMonthlyTotal = monthlyTotals.length ? Math.max(...monthlyTotals) : 0;
const chartAreaHeight = 350 - 28 - 80;
const dynamicSmallValueThreshold = maxMonthlyTotal > 0
? (MIN_INSIDE_LABEL_HEIGHT_PX / chartAreaHeight) * maxMonthlyTotal
: SMALL_VALUE_LABEL_THRESHOLD;
const annotationBaseOffset = Math.max(1, Math.ceil(maxMonthlyTotal * 0.02));
const annotationStep = Math.max(1, Math.ceil(maxMonthlyTotal * 0.05));

const outsideAnnotations = [];
const outsideSeriesByMonth = [];

months.forEach((month, monthIndex) => {
const monthSeries = ordered
.map((series, seriesIndex) => ({
seriesIndex,
name: series.name,
value: Number(series.values?.[monthIndex] || 0),
}));

const smallValueSeries = monthSeries.filter(
({ value }) => value === 0
|| value <= SMALL_VALUE_LABEL_THRESHOLD
|| value <= dynamicSmallValueThreshold,
);

// If any segment is too small for an inside label, move all labels for that month above the stack.
const renderAllOutsideForMonth = smallValueSeries.length > 0;
const monthOutsideSeries = renderAllOutsideForMonth ? monthSeries : smallValueSeries;
outsideSeriesByMonth[monthIndex] = new Set(monthOutsideSeries.map(({ seriesIndex }) => seriesIndex));

const labelsToRender =
monthOutsideSeries.length > 1 && monthOutsideSeries.every(({ value }) => value === 0)
? [{ value: 0 }]
: monthOutsideSeries;

// When multiple labels are above a stack, start higher so the first label does not crowd the bar top.
const startStep = labelsToRender.length > 1 ? 1 : 0;
const stepSize = labelsToRender.length > 1
? Math.max(1, Math.ceil(annotationStep * 1.3))
: annotationStep;

labelsToRender.forEach(({ value }, labelIndex) => {
outsideAnnotations.push({
x: month,
y: monthlyTotals[monthIndex] + annotationBaseOffset + ((startStep + labelIndex) * stepSize),
text: value.toString(),
showarrow: false,
font: { color: colors.baseDarkest, size: 10 },
xanchor: 'center',
yanchor: 'bottom',
});
});
});

const traces = ordered.map((series, i) => ({
type: 'bar',
name: series.name,
x: months,
y: series.values,
text: series.values.map((v, monthIndex) => {
const isOutside = outsideSeriesByMonth[monthIndex]?.has(i);
return isOutside ? '' : v.toString();
}),
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 } },
},
annotations: outsideAnnotations,
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,
};

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-with-tta-support" />
</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-with-tta-support" />
</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"
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'
);
Loading