Merged PR 2357: dashboards in admin for appeals and lpa

dashboards in admin for appeals and lpa

Related work items: #23527
This commit is contained in:
Robert Bond
2026-05-29 16:35:27 +00:00
parent b71a93076b
commit 18a05d4be8
12 changed files with 1658 additions and 9 deletions
+20
View File
@@ -65,3 +65,23 @@ export const getNewDocumentsPaged = async (
return logAndReturnResponse(error);
}
};
export const getStatusByAppeal = async () => {
try {
return await getJson(
BASE_URL + "/api/admin/getStatusCountsByAppeal_api"
);
} catch (error) {
return logAndReturnResponse(error);
}
};
export const getStatusByAppealLPA = async () => {
try {
return await getJson(
BASE_URL + "/api/admin/getStatusCountsByAppealAndLPA_api"
);
} catch (error) {
return logAndReturnResponse(error);
}
};
+10 -2
View File
@@ -1,7 +1,15 @@
import {
getNewAppeals,
getNewAppealsPage,
getNewDocumentsPaged
getNewDocumentsPaged,
getStatusByAppeal,
getStatusByAppealLPA
} from "./adminDirectService";
export { getNewAppeals, getNewAppealsPage, getNewDocumentsPaged };
export {
getNewAppeals,
getNewAppealsPage,
getNewDocumentsPaged,
getStatusByAppeal,
getStatusByAppealLPA
};
+273
View File
@@ -0,0 +1,273 @@
import { useMemo, useState } from "react";
import Link from "next/link";
export default function AppealStatusDashboard({ statusObj = [] }) {
const [openSections, setOpenSections] = useState({});
const sortedAppeals = useMemo(
() =>
[...statusObj].sort(
(a, b) => (b.caseCount || 0) - (a.caseCount || 0)
),
[statusObj]
);
const percentageOf = (value, total) =>
total ? `${(((value || 0) / total) * 100).toFixed(1)}%` : "0.0%";
const totalCases = sortedAppeals.reduce(
(sum, appeal) => sum + (appeal.caseCount || 0),
0
);
const largestAppeal = sortedAppeals[0];
const allStatuses = sortedAppeals.flatMap((appeal) =>
(appeal.statuses || []).map((status) => ({
...status,
appealType: appeal.value
}))
);
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 mostCommonStatus = Object.values(statusTotals).sort(
(a, b) => b.caseCount - a.caseCount
)[0];
const maxAppealCount = largestAppeal?.caseCount || 1;
const allExpanded =
sortedAppeals.length > 0 &&
sortedAppeals.every((_, index) => openSections[index]);
const toggleSection = (index) => {
setOpenSections((current) => ({
...current,
[index]: !current[index]
}));
};
const showAll = () => {
const allOpen = {};
sortedAppeals.forEach((_, index) => {
allOpen[index] = true;
});
setOpenSections(allOpen);
};
const hideAll = () => {
setOpenSections({});
};
return (
<div className="govuk-grid-row">
<div className="govuk-grid-column-full">
<h1>Appeal case status dashboard</h1>
<div className="govuk-grid-row">
<DashboardCard title="Total cases" value={totalCases} />
<DashboardCard
title="Appeal types"
value={sortedAppeals.length}
/>
<DashboardCard
title="Largest appeal type"
value={largestAppeal?.caseCount || 0}
caption={largestAppeal?.value}
/>
<DashboardCard
title="Most common status"
value={mostCommonStatus?.caseCount || 0}
caption={mostCommonStatus?.value}
/>
</div>
<section className="govuk-!-margin-top-8">
<h2 className="govuk-heading-m">
Case counts and stage breakdown by appeal type
</h2>
<p className="govuk-body-s">
Select an appeal type row to show the case stage
breakdown.
</p>
<div className="govuk-accordion__controls">
<button
type="button"
className="govuk-accordion__show-all"
onClick={allExpanded ? hideAll : showAll}
>
<span className="govuk-accordion__show-all-text">
{allExpanded
? "Hide all sections"
: "Show all sections"}
</span>
</button>
</div>
<div
className="govuk-accordion appeal-stage-chart"
id="appeals-with-status-accordion"
>
{sortedAppeals.map((appeal, index) => {
const isOpen = !!openSections[index];
const barWidth =
((appeal.caseCount || 0) / maxAppealCount) *
100;
const sortedStatuses = [...(appeal.statuses || [])]
.filter((status) => (status.caseCount || 0) > 0)
.sort(
(a, b) =>
(b.caseCount || 0) - (a.caseCount || 0)
);
return (
<div
className={
isOpen
? "govuk-accordion__section govuk-accordion__section--expanded appeal-stage-chart__accordion-section"
: "govuk-accordion__section appeal-stage-chart__accordion-section"
}
key={appeal.appealType || index}
>
<div className="govuk-accordion__section-header">
<h2 className="govuk-accordion__section-heading">
<button
type="button"
className="govuk-accordion__section-button appeal-stage-chart__button"
id={`appeal-status-heading-${index}`}
aria-expanded={isOpen}
aria-controls={`appeal-status-content-${index}`}
onClick={() =>
toggleSection(index)
}
>
<span className="appeal-stage-chart__row">
<span className="appeal-stage-chart__label">
{appeal.value} (
{percentageOf(
appeal.caseCount,
totalCases
)}
)
</span>
<span className="appeal-stage-chart__bar-wrapper">
<span
className="appeal-stage-chart__bar"
style={{
width: `${barWidth}%`
}}
aria-hidden="true"
>
{sortedStatuses.map(
(
status,
statusIndex
) => (
<span
key={`${status.statuscode}-${statusIndex}`}
className={`appeal-stage-chart__segment appeal-stage-chart__segment--${
statusIndex %
6
}`}
style={{
width: `${
((status.caseCount ||
0) /
(appeal.caseCount ||
1)) *
100
}%`
}}
title={`${status.value}: ${status.caseCount}`}
/>
)
)}
</span>
<span className="appeal-stage-chart__count">
{appeal.caseCount ||
0}{" "}
cases
</span>
</span>
</span>
</button>
</h2>
</div>
<div
id={`appeal-status-content-${index}`}
className="govuk-accordion__section-content"
aria-labelledby={`appeal-status-heading-${index}`}
hidden={!isOpen}
>
<dl className="govuk-summary-list">
{sortedStatuses.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}&statuscode=${status.statuscode}`}
>
{status.value}
</Link>
</dt>
<dd className="govuk-summary-list__value">
{status.caseCount || 0}{" "}
cases (
{percentageOf(
status.caseCount,
appeal.caseCount
)}{" "}
of this appeal type)
</dd>
</div>
))}
</dl>
</div>
</div>
);
})}
</div>
</section>
</div>
</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>
);
}
@@ -0,0 +1,571 @@
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>
);
}
+294
View File
@@ -0,0 +1,294 @@
// // components/admin/tabs/AccountsTab.js
// import Link from "next/link";
// export default function AppealsWithStatusTab({ statusObj }) {
// return (
// <div>
// <h1>User Accounts</h1>
// <dl className="govuk-summary-list govuk-!-margin-bottom-9 results">
// <div className="govuk-summary-list__row results-headings">
// <dd className="govuk-summary-list__key govuk-!-font-size-16">
// Name
// </dd>
// <dd className="govuk-summary-list__key govuk-!-font-size-16">
// Last accessed
// </dd>
// </div>
// {statusObj.map((appeal) => (
// <div key={appeal.id} className="govuk-summary-list__row">
// <dd className="govuk-summary-list__value govuk-!-font-size-16 ">
// <Link
// target="_blank"
// href={`/auth/signin2?email=${appeal.value}`}
// >
// {appeal.value}
// </Link>
// <br />
// <dl className="govuk-summary-list govuk-!-margin-bottom-9 govuk-!-margin-left-9 results">
// <div className="govuk-summary-list__row results-headings">
// <dd className="govuk-summary-list__key govuk-!-font-size-16">
// Name
// </dd>
// <dd className="govuk-summary-list__key govuk-!-font-size-16">
// Last accessed
// </dd>
// </div>
// {appeal.statuses.map((statuses) => (
// <div
// key={appeal.id}
// className="govuk-summary-list__row"
// >
// <dd className="govuk-summary-list__value govuk-!-font-size-16 ">
// {statuses.value}
// </dd>
// <dd className="govuk-summary-list__value govuk-!-font-size-16 ">
// Total: {statuses.caseCount}
// </dd>
// </div>
// ))}
// </dl>
// </dd>
// <dd className="govuk-summary-list__value govuk-!-font-size-16 ">
// Total: {appeal.caseCount}
// </dd>
// </div>
// ))}
// </dl>
// </div>
// );
// }
import { useState } from "react";
import Link from "next/link";
export default function AppealsWithStatusTab({ statusObj = [] }) {
const [openSections, setOpenSections] = useState({});
const toggleSection = (index) => {
setOpenSections((current) => ({
...current,
[index]: !current[index]
}));
};
const showAll = () => {
const allOpen = {};
statusObj.forEach((_, index) => {
allOpen[index] = true;
});
setOpenSections(allOpen);
};
const hideAll = () => {
setOpenSections({});
};
const allExpanded =
statusObj.length > 0 &&
statusObj.every((_, index) => openSections[index]);
const sortedAppeals = [...statusObj].sort(
(a, b) => (b.caseCount || 0) - (a.caseCount || 0)
);
const maxCount = sortedAppeals[0]?.caseCount || 1;
return (
<div>
<h1 className="govuk-heading-l">Case status by appeal type</h1>
<div className="govuk-!-margin-bottom-6">
<h2 className="govuk-heading-m">Case counts by appeal type</h2>
<dl className="appeal-count-chart">
{sortedAppeals.map((appeal) => {
const maxCount = sortedAppeals[0]?.caseCount || 1;
const width =
((appeal.caseCount || 0) / maxCount) * 100;
return (
<div
className="appeal-count-chart__row"
key={appeal.appealType}
>
<dt className="appeal-count-chart__label">
{appeal.value}
</dt>
<dd className="appeal-count-chart__bar-wrapper">
<span
className="appeal-count-chart__bar"
style={{ width: `${width}%` }}
/>
<span className="appeal-count-chart__count">
{appeal.caseCount || 0}
</span>
</dd>
</div>
);
})}
</dl>
</div>
<div className="govuk-!-margin-bottom-6">
<h2 className="govuk-heading-m">Case stages by appeal type</h2>
<dl className="appeal-stage-chart">
{sortedAppeals.map((appeal) => (
<div
className="appeal-stage-chart__row"
key={appeal.appealType}
>
<dt className="appeal-stage-chart__label">
{appeal.value}
</dt>
<dd className="appeal-stage-chart__bar-wrapper">
<div
className="appeal-stage-chart__bar"
style={{
width: `${((appeal.caseCount || 0) / maxCount) * 100}%`
}}
aria-label={`${appeal.value}, ${appeal.caseCount || 0} cases`}
>
{(appeal.statuses || [])
.filter(
(status) => status.caseCount > 0
)
.sort(
(a, b) => b.caseCount - a.caseCount
)
.map((status, index) => (
<span
key={status.statuscode}
className={`appeal-stage-chart__segment appeal-stage-chart__segment--${index % 6}`}
style={{
width: `${((status.caseCount || 0) / (appeal.caseCount || 1)) * 100}%`
}}
title={`${status.value}: ${status.caseCount}`}
/>
))}
</div>
<span className="appeal-stage-chart__count">
{appeal.caseCount || 0}
</span>
</dd>
</div>
))}
</dl>
</div>
<div className="govuk-accordion__controls">
<button
type="button"
className="govuk-accordion__show-all"
onClick={allExpanded ? hideAll : showAll}
>
<span className="govuk-accordion__show-all-text">
{allExpanded
? "Hide all sections"
: "Show all sections"}
</span>
</button>
</div>
<div className="govuk-accordion" id="appeals-with-status-accordion">
{[...statusObj]
.sort((a, b) => (b.caseCount || 0) - (a.caseCount || 0))
.map((appeal, index) => {
const isOpen = !!openSections[index];
return (
<div
className={
isOpen
? "govuk-accordion__section govuk-accordion__section--expanded"
: "govuk-accordion__section"
}
key={appeal.appealType || index}
>
<div className="govuk-accordion__section-header ">
<h2 className="govuk-accordion__section-heading">
<button
type="button"
className="govuk-accordion__section-button "
id={`appeal-status-heading-${index}`}
aria-expanded={isOpen}
aria-controls={`appeal-status-content-${index}`}
onClick={() => toggleSection(index)}
>
<span
style={{
display: "flex",
justifyContent:
"space-between",
alignItems: "center",
width: "100%",
paddingRight: "20px" // leaves room for accordion icon
}}
>
<span className="govuk-heading-s ">
{appeal.value}
</span>
<span
className="govuk-body-s"
style={{
fontWeight: "normal"
}}
>
{appeal.caseCount || 0}{" "}
cases
</span>
</span>
</button>
</h2>
</div>
<div
id={`appeal-status-content-${index}`}
className="govuk-accordion__section-content"
aria-labelledby={`appeal-status-heading-${index}`}
hidden={!isOpen}
>
<dl className="govuk-summary-list govuk-!-margin-left-9">
{(appeal.statuses || [])
.sort(
(a, b) =>
(b.caseCount || 0) -
(a.caseCount || 0)
)
.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}&statuscode=${status.statuscode}`}
>
{status.value}
</Link>
</dt>
<dd className="govuk-summary-list__value">
{status.caseCount || 0}
</dd>
</div>
))}
</dl>
</div>
</div>
);
})}
</div>
</div>
);
}
+1 -3
View File
@@ -1104,9 +1104,7 @@ const DocumentsTab = (props) => {
return (
<div className="govuk-grid-row">
<div className="govuk-grid-column-full">
<h2 className="govuk-heading-m ">
{t("case:documents-heading-label")}
</h2>
<h1 className="">{t("case:documents-heading-label")}</h1>
{/* <div className="">
MDU :{" "}
{countFieldValue(
+10
View File
@@ -283,5 +283,15 @@ export const statusValues = [
"value": "Case Submitted",
"value_cy": "Achos wedii gyflwyno",
"statuscode": 846040061
},
{
"value": "Problem Solved",
"value_cy": "Problem wedi'i Datrys",
"statuscode": 5
},
{
"value": "Information Provided",
"value_cy": "Gwybodaeth a Ddarperir",
"statuscode": 1000
}
];
+51 -4
View File
@@ -11,9 +11,16 @@ import StorageTab from "../../components/admin/tabs/storage";
import AccountsTab from "../../components/admin/tabs/accounts";
import AppealsTab from "../../components/admin/tabs/appeals";
import DocumentsTab from "../../components/admin/tabs/documents";
import AppealsWithStatusTab from "../../components/admin/tabs/appealsWithStatus";
import AppealStatusDashboard from "../../components/admin/tabs/appealsDashboard";
import AppealLpaStatusDashboard from "../../components/admin/tabs/appealsDashboardLPA";
import { fetchAdminStorageData } from "../../components/admin/utils/serverside";
import { getNewAppeals } from "../../actions/services/adminService";
import {
getNewAppeals,
getStatusByAppeal,
getStatusByAppealLPA
} from "../../actions/services/adminService";
import { getSearchDetails } from "../../components/utils";
@@ -30,9 +37,11 @@ const StoragePage = (props) => {
repCompleteCount,
searchResultsObj,
docsOffline,
showFilteredDocs
showFilteredDocs,
statusObj,
statusLPAObj
} = props;
const [whichTab, setWhichTab] = useState("documents");
const [whichTab, setWhichTab] = useState("appealDashboard");
return (
<div>
@@ -54,6 +63,8 @@ const StoragePage = (props) => {
{/* Tabs */}
<ul className="govuk-tabs__list">
{[
"appealDashboard",
"appealDashboardLPA",
"documents",
"appeals",
"storage",
@@ -85,6 +96,10 @@ const StoragePage = (props) => {
"Storage Account"}
{tab === "accounts" &&
"User Accounts"}
{tab === "appealDashboard" &&
"Appeals Dashboard"}
{tab === "appealDashboardLPA" &&
"Appeals LPA Dashboard"}
</a>
</li>
))}
@@ -92,6 +107,26 @@ const StoragePage = (props) => {
{/* Panels */}
<div
className={
whichTab === "appealDashboard"
? "govuk-tabs__panel"
: "govuk-tabs__panel govuk-tabs__panel--hidden"
}
>
<AppealStatusDashboard statusObj={statusObj} />
</div>
<div
className={
whichTab === "appealDashboardLPA"
? "govuk-tabs__panel"
: "govuk-tabs__panel govuk-tabs__panel--hidden"
}
>
<AppealLpaStatusDashboard
statusObj={statusLPAObj}
/>
</div>
<div
className={
whichTab === "documents"
@@ -119,6 +154,15 @@ const StoragePage = (props) => {
searchResultsObj={searchResultsObj}
/>
</div>
<div
className={
whichTab === "appealsWithStatus"
? "govuk-tabs__panel"
: "govuk-tabs__panel govuk-tabs__panel--hidden"
}
>
<AppealsWithStatusTab statusObj={statusObj} />
</div>
<div
className={
@@ -177,7 +221,8 @@ export const getServerSideProps = wrapper.getServerSideProps(
await fetchAdminStorageData();
const newAppeals = await getNewAppeals();
const statusObj = await getStatusByAppeal();
const statusLPAObj = await getStatusByAppealLPA();
const searchDetailsObj = await getSearchDetails(newAppeals);
store.dispatch(setSearchResults(newAppeals));
@@ -188,6 +233,8 @@ export const getServerSideProps = wrapper.getServerSideProps(
return {
props: {
statusObj,
statusLPAObj,
data,
userData,
repCompleteCount,
@@ -0,0 +1,57 @@
import { azureHeaders } from "../../../actions/core/headers";
import { relayGet } from "../middleware/relayForwarding";
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
import { statusValues } from "../../../components/utils/caseStagesObj";
export default async function ApiProxy(req, res) {
const queryUrl =
"incidents?$apply=groupby((statuscode,pinswg_appealcasetype),aggregate($count as caseCount))&$filter=pinswg_publishtoweb eq true";
return relayGet({
queryUrl,
res,
requestOptionsBuilder: (accessToken) => azureHeaders(accessToken),
relayPolicy: RELAY_POLICY_BOUNDED_READ,
transformData: (data) => {
const countsByStatusCode = (data.value || []).reduce(
(acc, item) => {
const statuscode = item.statuscode;
if (!acc[statuscode]) {
acc[statuscode] = {
caseCount: 0,
appealTypes: []
};
}
acc[statuscode].caseCount += item.caseCount || 0;
acc[statuscode].appealTypes.push({
value:
item[
"pinswg_appealcasetype@OData.Community.Display.V1.FormattedValue"
] || "",
appealType: item.pinswg_appealcasetype,
caseCount: item.caseCount || 0
});
return acc;
},
{}
);
return statusValues.map((status) => ({
...status,
caseCount: countsByStatusCode[status.statuscode] || 0,
appealTypes:
countsByStatusCode[status.statuscode]?.appealTypes || []
}));
},
errorResponse: {
status: 400,
code: "STATUS_COUNTS_FETCH_FAILED",
message: "Failed to fetch status counts"
}
});
}
@@ -0,0 +1,59 @@
import { azureHeaders } from "../../../actions/core/headers";
import { relayGet } from "../middleware/relayForwarding";
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
import { statusValues } from "../../../components/utils/caseStagesObj";
export default async function ApiProxy(req, res) {
// const queryUrl =
// "incidents?$apply=groupby((statuscode,pinswg_appealcasetype),aggregate($count as caseCount))&$filter=pinswg_publishtoweb eq true";
const queryUrl =
"incidents?$select=statuscode,pinswg_appealcasetype,_pinswg_associatedlpa_value&$filter=pinswg_publishtoweb eq true";
return relayGet({
queryUrl,
res,
requestOptionsBuilder: (accessToken) => azureHeaders(accessToken),
relayPolicy: RELAY_POLICY_BOUNDED_READ,
transformData: (data) => {
const grouped = (data.value || []).reduce((acc, item) => {
const key = [
item.statuscode,
item.pinswg_appealcasetype,
item._pinswg_associatedlpa_value || "no-lpa"
].join("|");
if (!acc[key]) {
acc[key] = {
statuscode: item.statuscode,
status: item[
"statuscode@OData.Community.Display.V1.FormattedValue"
],
appealType: item.pinswg_appealcasetype,
appealTypeName:
item[
"pinswg_appealcasetype@OData.Community.Display.V1.FormattedValue"
],
lpaId: item._pinswg_associatedlpa_value || null,
lpaName:
item[
"_pinswg_associatedlpa_value@OData.Community.Display.V1.FormattedValue"
] || null,
caseCount: 0
};
}
acc[key].caseCount += 1;
return acc;
}, {});
return Object.values(grouped);
},
errorResponse: {
status: 400,
code: "STATUS_COUNTS_FETCH_FAILED",
message: "Failed to fetch status counts"
}
});
}
@@ -0,0 +1,57 @@
import { azureHeaders } from "../../../actions/core/headers";
import { relayGet } from "../middleware/relayForwarding";
import { RELAY_POLICY_BOUNDED_READ } from "../middleware/relayPolicyPresets";
import { statusValues } from "../../../components/utils/caseStagesObj";
export default async function ApiProxy(req, res) {
const queryUrl =
"incidents?$apply=groupby((statuscode,pinswg_appealcasetype),aggregate($count as caseCount))&$filter=pinswg_publishtoweb eq true";
return relayGet({
queryUrl,
res,
requestOptionsBuilder: (accessToken) => azureHeaders(accessToken),
relayPolicy: RELAY_POLICY_BOUNDED_READ,
transformData: (data) => {
const groupedByAppealType = (data.value || []).reduce(
(acc, item) => {
const appealType = item.pinswg_appealcasetype;
if (!acc[appealType]) {
acc[appealType] = {
value:
item[
"pinswg_appealcasetype@OData.Community.Display.V1.FormattedValue"
] || "",
appealType,
caseCount: 0,
statuses: []
};
}
acc[appealType].caseCount += item.caseCount || 0;
acc[appealType].statuses.push({
value:
item[
"statuscode@OData.Community.Display.V1.FormattedValue"
] || "",
statuscode: item.statuscode,
caseCount: item.caseCount || 0
});
return acc;
},
{}
);
return Object.values(groupedByAppealType);
},
errorResponse: {
status: 400,
code: "STATUS_COUNTS_FETCH_FAILED",
message: "Failed to fetch status counts"
}
});
}
+255
View File
@@ -3288,4 +3288,259 @@ ul.subFieldList {
.govuk-task-list__name-and-hint {
width: 85%;
}
}
.appeal-count-chart {
margin: 0;
}
.appeal-count-chart__row {
display: grid;
grid-template-columns: minmax(220px, 45%) 1fr;
gap: 15px;
align-items: center;
margin-bottom: 10px;
}
.appeal-count-chart__label {
font-weight: 700;
font-size: 1.2rem
}
.appeal-count-chart__bar-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin: 0;
}
.appeal-count-chart__bar {
display: inline-block;
height: 16px;
background: #1d70b8;
}
.appeal-count-chart__count {
white-space: nowrap;
}
.appeal-stage-chart {
margin: 0;
}
.appeal-stage-chart__row {
display: grid;
grid-template-columns: minmax(260px, 42%) 1fr;
gap: 15px;
align-items: center;
margin-bottom: 12px;
}
.appeal-stage-chart__label {
font-weight: 700;
font-weight: 1.2rem;
}
.appeal-stage-chart__bar-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin: 0;
}
.appeal-stage-chart__bar {
display: flex;
height: 18px;
min-width: 2px;
background: #f3f2f1;
}
.appeal-stage-chart__segment {
display: block;
height: 100%;
}
.appeal-stage-chart__segment--0 {
background: #1d70b8;
}
.appeal-stage-chart__segment--1 {
background: #00703c;
}
.appeal-stage-chart__segment--2 {
background: #f47738;
}
.appeal-stage-chart__segment--3 {
background: #b58840;
}
.appeal-stage-chart__segment--4 {
background: #4c2c92;
}
.appeal-stage-chart__segment--5 {
background: #d4351c;
}
.appeal-stage-chart__count {
white-space: nowrap;
}
.dashboard-card {
border: 1px solid #b1b4b6;
padding: 15px;
min-height: 130px;
margin-bottom: 20px;
}
.dashboard-card__title {
margin-bottom: 5px;
}
.dashboard-card__value {
margin-bottom: 5px;
}
.dashboard-card__caption {
margin-bottom: 0;
}
/* Simple bar chart */
.appeal-count-chart,
.appeal-stage-chart {
margin: 0;
}
.appeal-count-chart__row,
.appeal-stage-chart__row {
display: grid;
grid-template-columns: minmax(260px, 42%) 1fr;
gap: 15px;
align-items: center;
margin-bottom: 12px;
}
.appeal-count-chart__label,
.appeal-stage-chart__label {
font-weight: 700;
font-size: 1.2rem;
}
.appeal-count-chart__bar-wrapper,
.appeal-stage-chart__bar-wrapper {
display: flex;
align-items: center;
gap: 10px;
margin: 0;
}
.appeal-count-chart__bar {
display: inline-block;
height: 18px;
background: #1d70b8;
}
.appeal-count-chart__count,
.appeal-stage-chart__count {
white-space: nowrap;
}
.appeal-stage-chart__bar {
display: flex;
height: 20px;
min-width: 2px;
background: #f3f2f1;
}
.appeal-stage-chart__segment {
display: block;
height: 100%;
}
.appeal-stage-chart__segment--0 {
background: #1d70b8;
}
.appeal-stage-chart__segment--1 {
background: #00703c;
}
.appeal-stage-chart__segment--2 {
background: #f47738;
}
.appeal-stage-chart__segment--3 {
background: #b58840;
}
.appeal-stage-chart__segment--4 {
background: #4c2c92;
}
.appeal-stage-chart__segment--5 {
background: #d4351c;
}
/* Accordion heading total */
.appeal-accordion-heading {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding-right: 20px;
}
.appeal-accordion-total {
font-weight: normal;
white-space: nowrap;
}
.govuk-accordion__show-all {
background: none;
border: 0;
color: #1d70b8;
cursor: pointer;
padding: 0;
text-decoration: underline;
}
.appeal-accordion-heading,
.lpa-status-block__button {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.appeal-accordion-total {
font-weight: normal;
white-space: nowrap;
}
.lpa-status-block {
border-bottom: 1px solid #b1b4b6;
padding: 10px 0;
}
.lpa-status-block__button {
background: none;
border: 0;
color: #1d70b8;
cursor: pointer;
font-weight: 700;
padding: 0;
text-align: left;
text-decoration: underline;
}
.lpa-status-block__button span:last-child {
color: #0b0c0c;
font-weight: normal;
text-decoration: none;
white-space: nowrap;
}