Files
pedwfrontend/components/admin/tabs/appealsDashboardLPA.js
T
Robert Bond 18a05d4be8 Merged PR 2357: dashboards in admin for appeals and lpa
dashboards in admin for appeals and lpa

Related work items: #23527
2026-05-29 16:35:27 +00:00

572 lines
27 KiB
JavaScript

import { useMemo, useState } from "react";
import Link from "next/link";
export default function AppealLpaStatusDashboard({ statusObj = [] }) {
const [openAppeals, setOpenAppeals] = useState({});
const [openLpaSections, setOpenLpaSections] = useState({});
const [openAppealLpas, setOpenAppealLpas] = useState({});
const [openLpaAppeals, setOpenLpaAppeals] = useState({});
const percentageOf = (value, total) =>
total ? `${(((value || 0) / total) * 100).toFixed(1)}%` : "0.0%";
const totalCases = useMemo(
() =>
statusObj.reduce((total, item) => total + (item.caseCount || 0), 0),
[statusObj]
);
const groupedAppeals = useMemo(() => {
const grouped = statusObj.reduce((acc, item) => {
const appealKey = item.appealType;
if (!acc[appealKey]) {
acc[appealKey] = {
appealType: item.appealType,
appealTypeName: item.appealTypeName,
caseCount: 0,
lpas: {}
};
}
acc[appealKey].caseCount += item.caseCount || 0;
const lpaKey = item.lpaId || "unknown-lpa";
if (!acc[appealKey].lpas[lpaKey]) {
acc[appealKey].lpas[lpaKey] = {
lpaId: item.lpaId,
lpaName: item.lpaName || "Unknown LPA",
caseCount: 0,
statuses: []
};
}
acc[appealKey].lpas[lpaKey].caseCount += item.caseCount || 0;
acc[appealKey].lpas[lpaKey].statuses.push({
statuscode: item.statuscode,
status: item.status,
caseCount: item.caseCount || 0
});
return acc;
}, {});
return Object.values(grouped)
.map((appeal) => ({
...appeal,
lpas: Object.values(appeal.lpas)
.map((lpa) => ({
...lpa,
statuses: lpa.statuses.sort(
(a, b) => (b.caseCount || 0) - (a.caseCount || 0)
)
}))
.sort((a, b) => (b.caseCount || 0) - (a.caseCount || 0))
}))
.sort((a, b) => (b.caseCount || 0) - (a.caseCount || 0));
}, [statusObj]);
const groupedLpas = useMemo(() => {
const grouped = statusObj.reduce((acc, item) => {
const lpaKey = item.lpaId || "unknown-lpa";
if (!acc[lpaKey]) {
acc[lpaKey] = {
lpaId: item.lpaId,
lpaName: item.lpaName || "Unknown LPA",
caseCount: 0,
appeals: {}
};
}
acc[lpaKey].caseCount += item.caseCount || 0;
const appealKey = item.appealType;
if (!acc[lpaKey].appeals[appealKey]) {
acc[lpaKey].appeals[appealKey] = {
appealType: item.appealType,
appealTypeName: item.appealTypeName,
caseCount: 0,
statuses: []
};
}
acc[lpaKey].appeals[appealKey].caseCount += item.caseCount || 0;
acc[lpaKey].appeals[appealKey].statuses.push({
statuscode: item.statuscode,
status: item.status,
caseCount: item.caseCount || 0
});
return acc;
}, {});
return Object.values(grouped)
.map((lpa) => ({
...lpa,
appeals: Object.values(lpa.appeals)
.map((appeal) => ({
...appeal,
statuses: appeal.statuses.sort(
(a, b) => (b.caseCount || 0) - (a.caseCount || 0)
)
}))
.sort((a, b) => (b.caseCount || 0) - (a.caseCount || 0))
}))
.sort((a, b) => (b.caseCount || 0) - (a.caseCount || 0));
}, [statusObj]);
const toggleAppeal = (appealType) => {
setOpenAppeals((current) => ({
...current,
[appealType]: !current[appealType]
}));
};
const toggleLpaSection = (lpaId) => {
setOpenLpaSections((current) => ({
...current,
[lpaId]: !current[lpaId]
}));
};
const toggleAppealLpa = (key) => {
setOpenAppealLpas((current) => ({
...current,
[key]: !current[key]
}));
};
const toggleLpaAppeal = (key) => {
setOpenLpaAppeals((current) => ({
...current,
[key]: !current[key]
}));
};
const sortedAppeals = useMemo(
() => [...statusObj].sort((a, b) => (b.lpaId || 0) - (a.lpaId || 0)),
[statusObj]
);
const allStatuses = sortedAppeals.flatMap((appeal) =>
(appeal.statuses || []).map((status) => ({
...status,
lpaId: appeal.lpaId,
lpaName: appeal.lpaName
}))
);
const statusTotals = allStatuses.reduce((acc, status) => {
if (!acc[status.statuscode]) {
acc[status.statuscode] = {
value: status.value,
statuscode: status.statuscode,
caseCount: 0
};
}
acc[status.statuscode].caseCount += status.caseCount || 0;
return acc;
}, {});
const largestAppeal = groupedLpas[0];
const mostCommonStatus = Object.values(groupedAppeals).sort(
(a, b) => b.caseCount - a.caseCount
)[0];
const maxAppealCount = largestAppeal?.caseCount || 1;
const maxGroupedAppealCount = groupedAppeals[0]?.caseCount || 1;
const allAppealsExpanded =
groupedAppeals.length > 0 &&
groupedAppeals.every((appeal) => openAppeals[appeal.appealType]);
const allLpasExpanded =
groupedLpas.length > 0 &&
groupedLpas.every((lpa) => openLpaSections[lpa.lpaId]);
const showAllAppeals = () => {
const allOpen = {};
groupedAppeals.forEach((appeal) => {
allOpen[appeal.appealType] = true;
});
setOpenAppeals(allOpen);
};
const showAllLpas = () => {
const allOpen = {};
groupedLpas.forEach((lpa) => {
allOpen[lpa.lpaId] = true;
});
setOpenLpaSections(allOpen);
};
const hideAllAppeals = () => {
setOpenAppeals({});
setOpenAppealLpas({});
};
const hideAllLpas = () => {
setOpenLpaSections({});
setOpenLpaAppeals({});
};
return (
<div className="govuk-grid-row">
<div className="govuk-grid-column-full">
<h1 className="">LPA Appeal case status dashboard</h1>
<div className="govuk-grid-row">
<DashboardCard title="Total cases" value={totalCases} />
<DashboardCard title="LPAs" value={groupedLpas.length} />
<DashboardCard
title="Largest number of appeals"
value={largestAppeal?.caseCount || 0}
caption={largestAppeal?.lpaName}
/>
<DashboardCard
title="Most common appeal"
value={mostCommonStatus?.caseCount || 0}
caption={mostCommonStatus?.appealTypeName}
/>
</div>
</div>
<section className="govuk-!-margin-top-8">
<h2 className="govuk-heading-m">
Appeal types and case stages by LPA
</h2>
<div className="govuk-accordion__controls">
<button
type="button"
className="govuk-accordion__show-all"
onClick={allLpasExpanded ? hideAllLpas : showAllLpas}
>
<span className="govuk-accordion__show-all-text">
{allLpasExpanded
? "Hide all sections"
: "Show all sections"}
</span>
</button>
</div>
<div
className="govuk-accordion"
id="lpa-appeal-status-accordion"
>
{groupedLpas.map((lpa) => {
const lpaOpen = !!openLpaSections[lpa.lpaId];
const width =
((lpa.caseCount || 0) / maxAppealCount) * 100;
return (
<div
className={
lpaOpen
? "govuk-accordion__section govuk-accordion__section--expanded"
: "govuk-accordion__section"
}
key={lpa.lpaId}
>
<div className="govuk-accordion__section-header">
<h3 className="govuk-accordion__section-heading">
<button
type="button"
className="govuk-accordion__section-button"
aria-expanded={lpaOpen}
onClick={() =>
toggleLpaSection(lpa.lpaId)
}
>
<div className="appeal-count-chart__row">
<div className="appeal-count-chart__label">
{lpa.lpaName} (
{percentageOf(
lpa.caseCount,
totalCases
)}
)
</div>
<div className="appeal-count-chart__bar-wrapper">
<span
className="appeal-count-chart__bar"
style={{
width: `${width}%`
}}
/>
<span className="appeal-count-chart__count">
{lpa.caseCount}
</span>
</div>
</div>
</button>
</h3>
</div>
<div
className="govuk-accordion__section-content"
hidden={!lpaOpen}
>
{lpa.appeals.map((appeal) => {
const key = `${lpa.lpaId}-${appeal.appealType}`;
const appealOpen =
!!openLpaAppeals[key];
return (
<div
className="lpa-status-block"
key={key}
>
<button
type="button"
className="lpa-status-block__button"
aria-expanded={appealOpen}
onClick={() =>
toggleLpaAppeal(key)
}
>
<span className="govuk-body-m">
{appeal.appealTypeName}
</span>
<span className="govuk-body-m">
{appeal.caseCount} cases
(
{percentageOf(
appeal.caseCount,
lpa.caseCount
)}
)
</span>
</button>
<div hidden={!appealOpen}>
<dl className="govuk-summary-list govuk-!-margin-bottom-4">
{appeal.statuses.map(
(status) => (
<div
className="govuk-summary-list__row"
key={
status.statuscode
}
>
<dt className="govuk-summary-list__key">
<Link
target="_blank"
href={`/advancedsearchresults?apt=${appeal.appealType}&lpa=${lpa.lpaId}&statuscode=${status.statuscode}`}
>
{
status.status
}
</Link>
</dt>
<dd className="govuk-summary-list__value">
{
status.caseCount
}{" "}
(
{percentageOf(
status.caseCount,
appeal.caseCount
)}
)
</dd>
</div>
)
)}
</dl>
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
</section>
<section className="govuk-!-margin-top-8">
<h2 className="govuk-heading-m">
Case stage by LPA and Appeal Type
</h2>
<p className="govuk-body-s">
Select an appeal type row to show the LPA and then case
stage breakdown.
</p>
<div className="govuk-accordion__controls">
<button
type="button"
className="govuk-accordion__show-all"
onClick={
allAppealsExpanded ? hideAllAppeals : showAllAppeals
}
>
<span className="govuk-accordion__show-all-text">
{allAppealsExpanded
? "Hide all sections"
: "Show all sections"}
</span>
</button>
</div>
<div
className="govuk-accordion"
id="appeal-lpa-status-accordion"
>
{groupedAppeals.map((appeal) => {
const appealOpen = !!openAppeals[appeal.appealType];
const width =
((appeal.caseCount || 0) / maxGroupedAppealCount) *
100;
return (
<div
className={
appealOpen
? "govuk-accordion__section govuk-accordion__section--expanded"
: "govuk-accordion__section"
}
key={appeal.appealType}
>
<div className="govuk-accordion__section-header">
<h2 className="govuk-accordion__section-heading">
<button
type="button"
className="govuk-accordion__section-button"
aria-expanded={appealOpen}
onClick={() =>
toggleAppeal(appeal.appealType)
}
>
<div className="appeal-count-chart__row">
<div className="appeal-count-chart__label">
{appeal.appealTypeName}{" "}
{/* {appeal.caseCount} cases */}
(
{percentageOf(
appeal.caseCount,
totalCases
)}
)
</div>
<div className="appeal-count-chart__bar-wrapper">
<span
className="appeal-count-chart__bar"
style={{
width: `${width}%`
}}
/>
<span className="appeal-count-chart__count">
{appeal.caseCount}
</span>
</div>
</div>
</button>
</h2>
</div>
<div
className="govuk-accordion__section-content"
hidden={!appealOpen}
>
{appeal.lpas.map((lpa) => {
const lpaKey = `${appeal.appealType}-${lpa.lpaId}`;
const lpaOpen =
!!openAppealLpas[lpaKey];
return (
<div
className="lpa-status-block"
key={lpaKey}
>
<button
type="button"
className="lpa-status-block__button"
aria-expanded={lpaOpen}
onClick={() =>
toggleAppealLpa(lpaKey)
}
>
<span className="govuk-body-m">
{lpa.lpaName}
</span>
<span className="govuk-body-m">
{lpa.caseCount} cases (
{percentageOf(
lpa.caseCount,
appeal.caseCount
)}
)
</span>
</button>
<div hidden={!lpaOpen}>
<dl className="govuk-summary-list govuk-!-margin-bottom-4">
{lpa.statuses.map(
(status) => (
<div
className="govuk-summary-list__row"
key={
status.statuscode
}
>
<dt className="govuk-summary-list__key">
<Link
target="_blank"
href={`/advancedsearchresults?apt=${appeal.appealType}&lpa=${lpa.lpaId}&statuscode=${status.statuscode}`}
>
{
status.status
}
</Link>
</dt>
<dd className="govuk-summary-list__value">
{
status.caseCount
}{" "}
(
{percentageOf(
status.caseCount,
lpa.caseCount
)}
)
</dd>
</div>
)
)}
</dl>
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
</section>
</div>
);
}
function DashboardCard({ title, value, caption }) {
return (
<div className="govuk-grid-column-one-quarter">
<div className="dashboard-card">
<p className="govuk-body-s dashboard-card__title">{title}</p>
<p className="govuk-heading-l dashboard-card__value">{value}</p>
{caption && (
<p className="govuk-body-s dashboard-card__caption">
{caption}
</p>
)}
</div>
</div>
);
}