572 lines
27 KiB
JavaScript
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>
|
|
);
|
|
}
|