Skip to content

Commit db5f347

Browse files
authored
feat: reports page functionality to display reports from API (#1550)
* feat: reports page functionality to display reports from API * fix: add some more mock data and fix GitHub review * test: add tests for fetch reports and fix linting * fix: fix sonarqube issues
1 parent 7fe784f commit db5f347

15 files changed

Lines changed: 903 additions & 58 deletions

File tree

application/CohortManager/src/Web/app/api/GetValidationExceptions/route.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,25 @@ function createExceptionListResponse<T extends { ExceptionId: number }>(
3434
};
3535
}
3636

37+
function addExceptionDetails<T extends { ExceptionId: number }>(items: T[]) {
38+
const exceptions = mockDataStore.getExceptions();
39+
return items.map((item) => ({
40+
...item,
41+
ExceptionDetails: exceptions[item.ExceptionId]?.ExceptionDetails ?? null,
42+
}));
43+
}
44+
3745
export async function GET(request: Request) {
3846
const { searchParams } = new URL(request.url);
3947
const exceptionId = searchParams.get("exceptionId");
4048
const raisedOnly = searchParams.get("raisedOnly");
4149
const notRaisedOnly = searchParams.get("notRaisedOnly");
4250
const sortBy = searchParams.get("sortBy");
51+
const sortOrder = searchParams.get("sortOrder");
52+
const exceptionStatus = searchParams.get("exceptionStatus");
53+
const isReport = searchParams.get("isReport");
54+
const exceptionCategory = searchParams.get("exceptionCategory");
55+
const reportDate = searchParams.get("reportDate");
4356

4457
// Handle single exception requests - get fresh data from store
4558
if (exceptionId !== null) {
@@ -57,10 +70,55 @@ export async function GET(request: Request) {
5770
}
5871

5972
// Handle list requests - get fresh data from store
73+
if (exceptionStatus !== null) {
74+
const usingSort = sortBy ?? sortOrder; // accept either param name
75+
const isRaised = exceptionStatus === "1";
76+
const allItems = isRaised
77+
? mockDataStore.getRaisedExceptions()
78+
: mockDataStore.getNotRaisedExceptions();
79+
80+
// Optional category filter (e.g., 12 or 13)
81+
const categoryFiltered = exceptionCategory
82+
? allItems.filter((i) => i.Category === Number(exceptionCategory))
83+
: allItems;
84+
85+
// Optional date filter for report mode
86+
// If isReport = 1 and a reportDate (YYYY-MM-DD) is provided, try to match on
87+
// ServiceNowCreatedDate for raised items, otherwise DateCreated for not raised.
88+
let dateFiltered = categoryFiltered;
89+
if (isReport === "1" && reportDate) {
90+
const datePrefix = `${reportDate}`; // already YYYY-MM-DD
91+
type DateField = "ServiceNowCreatedDate" | "DateCreated";
92+
const dateField: DateField = isRaised
93+
? "ServiceNowCreatedDate"
94+
: "DateCreated";
95+
dateFiltered = categoryFiltered.filter((i) => {
96+
const value = i[dateField as keyof typeof i] as unknown as
97+
| string
98+
| undefined;
99+
return value ? value.startsWith(datePrefix) : false;
100+
});
101+
}
102+
103+
// Sort: for raised items prefer ServiceNowCreatedDate, otherwise DateCreated
104+
const sortedItems = sortExceptions(
105+
[...dateFiltered],
106+
usingSort,
107+
isRaised ? "ServiceNowCreatedDate" : "DateCreated"
108+
);
109+
110+
const response = createExceptionListResponse(
111+
addExceptionDetails(sortedItems)
112+
);
113+
return NextResponse.json(response, { status: 200 });
114+
}
115+
60116
if (notRaisedOnly) {
61117
const notRaisedExceptions = mockDataStore.getNotRaisedExceptions();
62118
const sortedItems = sortExceptions([...notRaisedExceptions], sortBy);
63-
const response = createExceptionListResponse(sortedItems);
119+
const response = createExceptionListResponse(
120+
addExceptionDetails(sortedItems)
121+
);
64122
return NextResponse.json(response, { status: 200 });
65123
}
66124

@@ -71,7 +129,9 @@ export async function GET(request: Request) {
71129
sortBy,
72130
"ServiceNowCreatedDate"
73131
);
74-
const response = createExceptionListResponse(sortedItems);
132+
const response = createExceptionListResponse(
133+
addExceptionDetails(sortedItems)
134+
);
75135
return NextResponse.json(response, { status: 200 });
76136
}
77137

application/CohortManager/src/Web/app/components/dataError.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import Link from "next/link";
22

3-
export default async function DataError() {
3+
type DataErrorProps = Readonly<{
4+
entity?: "exceptions" | "reports";
5+
}>;
6+
7+
export default async function DataError({
8+
entity = "exceptions",
9+
}: DataErrorProps) {
410
return (
511
<main className="nhsuk-main-wrapper" id="maincontent" role="main">
612
<div className="nhsuk-grid-row">
713
<div className="nhsuk-grid-column-two-thirds">
8-
<h1>The exceptions could not be loaded</h1>
14+
<h1>The {entity} could not be loaded</h1>
915
<p>Please try again later.</p>
1016
<p>
1117
<Link href="/contact-us">Contact us</Link> if the problem persists

application/CohortManager/src/Web/app/components/overview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default async function Overview() {
2727

2828
const reportItems = [
2929
{
30-
value: 0,
30+
value: 28, // Showing reports for last 2 weeks
3131
label: "Reports",
3232
description: "To manage investigations into demographic changes",
3333
url: "/reports",

application/CohortManager/src/Web/app/components/participantInformationPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExceptionDetails } from "@/app//types";
1+
import { ExceptionDetails } from "@/app/types";
22
import {
33
formatDate,
44
formatCompactDate,
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { ExceptionAPIDetails } from "@/app/types/exceptionsApi";
2+
import { formatCompactDate, formatNhsNumber } from "@/app/lib/utils";
3+
4+
interface ReportsInformationTableProps {
5+
readonly category: number;
6+
readonly items: readonly ExceptionAPIDetails[];
7+
}
8+
9+
export default function ReportsInformationTable({
10+
category,
11+
items,
12+
}: Readonly<ReportsInformationTableProps>) {
13+
const isConfusion = category === 12; // Possible confusion
14+
return (
15+
<table role="table" className="nhsuk-table-responsive">
16+
<thead className="nhsuk-table__head">
17+
{isConfusion ? (
18+
<tr role="row">
19+
<th role="columnheader" scope="col">
20+
Patient name
21+
</th>
22+
<th role="columnheader" scope="col">
23+
Date of Birth
24+
</th>
25+
<th role="columnheader" scope="col">
26+
NHS number
27+
</th>
28+
</tr>
29+
) : (
30+
<tr role="row">
31+
<th role="columnheader" scope="col">
32+
Patient name
33+
</th>
34+
<th role="columnheader" scope="col">
35+
Date of Birth
36+
</th>
37+
<th role="columnheader" scope="col">
38+
NHS number
39+
</th>
40+
<th role="columnheader" scope="col">
41+
Superseded by NHS number
42+
</th>
43+
</tr>
44+
)}
45+
</thead>
46+
<tbody className="nhsuk-table__body">
47+
{items.map((item) => {
48+
const d = item.ExceptionDetails;
49+
const name = `${d.GivenName} ${d.FamilyName}`.trim();
50+
return (
51+
<tr role="row" className="nhsuk-table__row" key={item.ExceptionId}>
52+
<td className="nhsuk-table__cell">
53+
<span
54+
className="nhsuk-table-responsive__heading"
55+
aria-hidden="true"
56+
>
57+
Patient name{" "}
58+
</span>
59+
{name}
60+
</td>
61+
<td className="nhsuk-table__cell app-u-no-wrap">
62+
<span
63+
className="nhsuk-table-responsive__heading"
64+
aria-hidden="true"
65+
>
66+
Date of Birth{" "}
67+
</span>
68+
{formatCompactDate(d.DateOfBirth)}
69+
</td>
70+
{isConfusion ? (
71+
<td className="nhsuk-table__cell app-u-no-wrap">
72+
<span
73+
className="nhsuk-table-responsive__heading"
74+
aria-hidden="true"
75+
>
76+
NHS number{" "}
77+
</span>
78+
{formatNhsNumber(item.NhsNumber)}
79+
</td>
80+
) : (
81+
<>
82+
<td className="nhsuk-table__cell app-u-no-wrap">
83+
<span
84+
className="nhsuk-table-responsive__heading"
85+
aria-hidden="true"
86+
>
87+
NHS number{" "}
88+
</span>
89+
{formatNhsNumber(item.NhsNumber)}
90+
</td>
91+
<td className="nhsuk-table__cell app-u-no-wrap">
92+
<span
93+
className="nhsuk-table-responsive__heading"
94+
aria-hidden="true"
95+
>
96+
Superseded by NHS number{" "}
97+
</span>
98+
{formatNhsNumber(d.SupersededByNhsNumber ?? "")}
99+
</td>
100+
</>
101+
)}
102+
</tr>
103+
);
104+
})}
105+
</tbody>
106+
</table>
107+
);
108+
}

application/CohortManager/src/Web/app/components/reportsTable.tsx

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,39 +34,49 @@ export default function ExceptionsTable({
3434
</tr>
3535
</thead>
3636
<tbody className="nhsuk-table__body">
37-
{reports.map((exception) => (
38-
<tr role="row" className="nhsuk-table__row" key={exception.reportId}>
39-
<td className="nhsuk-table__cell app-u-no-wrap">
40-
<span
41-
className="nhsuk-table-responsive__heading"
42-
aria-hidden="true"
43-
>
44-
Date{" "}
45-
</span>
46-
{formatDate(exception.dateCreated)}
47-
</td>
48-
<td className="nhsuk-table__cell">
49-
<span
50-
className="nhsuk-table-responsive__heading"
51-
aria-hidden="true"
52-
>
53-
Demographic change{" "}
54-
</span>
55-
{exception.description}
56-
</td>
57-
<td className="nhsuk-table__cell">
58-
<span
59-
className="nhsuk-table-responsive__heading"
60-
aria-hidden="true"
61-
>
62-
Action{" "}
63-
</span>
64-
<Link href={`/reports/${exception.reportId}`}>
65-
View report {exception.reportId}
66-
</Link>
67-
</td>
68-
</tr>
69-
))}
37+
{reports.map((exception) => {
38+
const categoryTitle =
39+
exception.category === 12
40+
? "Possible confusion"
41+
: exception.category === 13
42+
? "NHS number change"
43+
: String(exception.category);
44+
return (
45+
<tr
46+
role="row"
47+
className="nhsuk-table__row"
48+
key={exception.reportId}
49+
>
50+
<td className="nhsuk-table__cell app-u-no-wrap">
51+
<span
52+
className="nhsuk-table-responsive__heading"
53+
aria-hidden="true"
54+
>
55+
Date{" "}
56+
</span>
57+
{formatDate(exception.dateCreated)}
58+
</td>
59+
<td className="nhsuk-table__cell">
60+
<span
61+
className="nhsuk-table-responsive__heading"
62+
aria-hidden="true"
63+
>
64+
Demographic change{" "}
65+
</span>
66+
{categoryTitle}
67+
</td>
68+
<td className="nhsuk-table__cell">
69+
<span
70+
className="nhsuk-table-responsive__heading"
71+
aria-hidden="true"
72+
>
73+
Action{" "}
74+
</span>
75+
<Link href={`/reports/${exception.reportId}`}>View report</Link>
76+
</td>
77+
</tr>
78+
);
79+
})}
7080
</tbody>
7181
</table>
7282
);

application/CohortManager/src/Web/app/data/mockDataStore.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ExceptionAPIDetails } from "@/app/types/exceptionsApi";
22
import mockDataJson from "@/app/data/mockExceptions.json";
3+
import { getCurrentDate } from "@/app/lib/utils";
34

45
type ExceptionListItem = {
56
ExceptionId: number;
@@ -32,6 +33,48 @@ const initializeDataStore = () => {
3233
notRaisedExceptions: [...mockDataJson.notRaisedExceptions],
3334
raisedExceptions: [...mockDataJson.raisedExceptions],
3435
};
36+
37+
// Ensure certain mock exceptions always use today's date for reports
38+
// This keeps the sample data relevant without manual JSON edits.
39+
const store = mockDataStore;
40+
const today = getCurrentDate(); // YYYY-MM-DD
41+
42+
const applyTodayDates = (
43+
exceptionId: number,
44+
createdTime: string,
45+
serviceNowTime: string
46+
) => {
47+
const ex = store.exceptions[exceptionId];
48+
if (ex) {
49+
ex.DateCreated = `${today}T${createdTime}`;
50+
// Only set ServiceNowCreatedDate if it's a raised exception
51+
if (ex.ServiceNowId) {
52+
ex.ServiceNowCreatedDate = `${today}T${serviceNowTime}`;
53+
}
54+
ex.RecordUpdatedDate = ex.ServiceNowCreatedDate || ex.DateCreated;
55+
}
56+
57+
// Update list mirrors
58+
const updateListItem = (item: typeof store.raisedExceptions[number]) => {
59+
if (item.ExceptionId === exceptionId) {
60+
item.DateCreated = `${today}T${createdTime}`;
61+
if (item.ServiceNowId) {
62+
item.ServiceNowCreatedDate = `${today}T${serviceNowTime}`;
63+
}
64+
item.RecordUpdatedDate =
65+
item.ServiceNowCreatedDate || item.DateCreated;
66+
}
67+
};
68+
69+
store.raisedExceptions.forEach(updateListItem);
70+
store.notRaisedExceptions.forEach(updateListItem);
71+
};
72+
73+
// 3001: category 12 (raised) – keep original times but set to today
74+
applyTodayDates(3001, "09:00:00", "10:00:00");
75+
// 4001: category 13 (raised) – keep original times but set to today
76+
applyTodayDates(4001, "09:15:00", "12:00:00");
77+
3578
return mockDataStore;
3679
};
3780

0 commit comments

Comments
 (0)