Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -4,6 +4,6 @@ namespace Common;

public interface IPaginationService<T>
{
PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int page = 1);
PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int page, int pageSize);
Dictionary<string, string> AddNavigationHeaders<TEntity>(HttpRequestData request, PaginationResult<TEntity> paginationResult);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ namespace Common;

public class PaginationService<T> : IPaginationService<T>
{
private const int pageSize = 10;

/// <summary>
/// Gets paginated results using page-based pagination
/// </summary>
/// <param name="source">The queryable source</param>
/// <param name="page">The page number (1-based)</param>
/// <param name="pageSize">The number of items per page (default: 10)</param>
/// <returns>Paginated result</returns>
public PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int page = 1)
public PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int page, int pageSize)
{
if (pageSize <= 0) pageSize = 10;
if (page <= 0) page = 1;

var totalItems = source.Count();
var totalPages = (int)Math.Ceiling((double)totalItems / pageSize);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou
{
var exceptionId = _httpParserHelper.GetQueryParameterAsInt(req, "exceptionId");
var page = _httpParserHelper.GetQueryParameterAsInt(req, "page");
if (page <= 0) page = 1;
var pageSize = _httpParserHelper.GetQueryParameterAsInt(req, "pageSize");
var exceptionStatus = HttpParserHelper.GetEnumQueryParameter(req, "exceptionStatus", ExceptionStatus.All);
var sortOrder = HttpParserHelper.GetEnumQueryParameter(req, "sortOrder", SortOrder.Descending);
var exceptionCategory = HttpParserHelper.GetEnumQueryParameter(req, "exceptionCategory", ExceptionCategory.NBO);
Expand All @@ -72,11 +72,11 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou
}

var reportExceptions = await _validationData.GetReportExceptions(reportDate, exceptionCategory);
return CreatePaginatedResponse(req, reportExceptions!.AsQueryable(), page);
return CreatePaginatedResponse(req, reportExceptions!.AsQueryable(), page, reportExceptions!.Count);
}

var filteredExceptions = await _validationData.GetFilteredExceptions(exceptionStatus, sortOrder, exceptionCategory);
return CreatePaginatedResponse(req, filteredExceptions!.AsQueryable(), page);
return CreatePaginatedResponse(req, filteredExceptions!.AsQueryable(), page, pageSize);
}
catch (Exception ex)
{
Expand All @@ -85,9 +85,9 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou
}
}

private HttpResponseData CreatePaginatedResponse(HttpRequestData request, IQueryable<ValidationException> source, int page)
private HttpResponseData CreatePaginatedResponse(HttpRequestData request, IQueryable<ValidationException> source, int page, int pageSize)
{
var paginatedResult = _paginationService.GetPaginatedResult(source, page);
var paginatedResult = _paginationService.GetPaginatedResult(source, page, pageSize);
var headers = _paginationService.AddNavigationHeaders(request, paginatedResult);

return _createResponse.CreateHttpResponseWithHeaders(HttpStatusCode.OK, request, JsonSerializer.Serialize(paginatedResult), headers);
Expand Down
131 changes: 123 additions & 8 deletions application/CohortManager/src/Web/app/components/pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,132 @@ interface PaginationItem {
readonly current?: boolean;
}

interface PaginationLink {
readonly href: string;
interface PaginationProps {
readonly linkHeader?: string | null;
readonly currentPage: number;
readonly totalPages: number;
readonly buildUrl: (page: number) => string;
}

interface PaginationProps {
readonly items: readonly PaginationItem[];
readonly previous?: PaginationLink;
readonly next?: PaginationLink;
function parseLinkHeader(linkHeader: string): {
first?: string;
previous?: string;
next?: string;
last?: string;
} {
const links: {
first?: string;
previous?: string;
next?: string;
last?: string;
} = {};

if (!linkHeader) return links;

const linkRegex = /<([^\s>]{1,2048})>;\s*rel="([^\s"]{1,100})"/g
let match;

while ((match = linkRegex.exec(linkHeader)) !== null) {
const url = match[1];
const rel = match[2];

if (rel === "first") links.first = url;
if (rel === "prev" || rel === "previous") links.previous = url;
if (rel === "next") links.next = url;
if (rel === "last") links.last = url;
}

return links;
}

export default function Pagination({ items, previous, next }: PaginationProps) {
function generatePaginationItems(
currentPage: number,
totalPages: number,
buildUrl: (page: number) => string,
maxVisiblePages: number = 10
): PaginationItem[] {
const items: PaginationItem[] = [];

if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
items.push({
number: i,
href: buildUrl(i),
current: i === currentPage,
});
}
return items;
}

let startPage = Math.max(1, currentPage - 3);
let endPage = Math.min(totalPages, currentPage + 3);

if (currentPage <= 4) {
endPage = Math.min(maxVisiblePages, totalPages);
}

if (currentPage >= totalPages - 3) {
startPage = Math.max(1, totalPages - maxVisiblePages + 1);
}

if (startPage > 1) {
items.push({
number: 1,
href: buildUrl(1),
current: false,
});

if (startPage > 2) {
items.push({
number: -1,
href: "#",
current: false,
});
}
}

for (let i = startPage; i <= endPage; i++) {
items.push({
number: i,
href: buildUrl(i),
current: i === currentPage,
});
}

if (endPage < totalPages) {
if (endPage < totalPages - 1) {
items.push({
number: -1,
href: "#",
current: false,
});
}
items.push({
number: totalPages,
href: buildUrl(totalPages),
current: false,
});
}

return items;
}

export default function Pagination({
linkHeader,
currentPage,
totalPages,
buildUrl,
}: PaginationProps) {
const paginationLinks = parseLinkHeader(linkHeader || "");
const items = generatePaginationItems(currentPage, totalPages, buildUrl);

const previous = paginationLinks.previous
? { href: buildUrl(currentPage - 1) }
: undefined;

const next = paginationLinks.next
? { href: buildUrl(currentPage + 1) }
: undefined;
return (
<nav
className="app-pagination"
Expand Down Expand Up @@ -48,7 +163,7 @@ export default function Pagination({ items, previous, next }: PaginationProps) {
<ul className="app-pagination__list">
{items.map((item, index) => (
<li
key={item.number !== -1 ? item.number : `ellipsis-${index}`}
key={item.number === -1 ? `ellipsis-${index}` : item.number}
className={`app-pagination__item${
item.current ? " app-pagination__item--current" : ""
}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ interface ReportTableProps {
readonly caption?: string;
}

const categoryTitles: Record<number, string> = {
12: "Possible Confusion",
13: "NHS Number Change",
};

export default function ExceptionsTable({
reports,
caption,
Expand Down Expand Up @@ -36,11 +41,7 @@ export default function ExceptionsTable({
<tbody className="nhsuk-table__body">
{reports.map((exception) => {
const categoryTitle =
exception.category === 12
? "Possible confusion"
: exception.category === 13
? "NHS number change"
: String(exception.category);
categoryTitles[exception.category] ?? String(exception.category);
return (
<tr
role="row"
Expand Down
63 changes: 7 additions & 56 deletions application/CohortManager/src/Web/app/exceptions/[filter]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,6 @@ import Breadcrumb from "@/app/components/breadcrumb";
import Unauthorised from "@/app/components/unauthorised";
import DataError from "@/app/components/dataError";
import Pagination from "@/app/components/pagination";
import {
parseLinkHeader,
extractPageFromUrl,
convertToLocalUrl,
generatePaginationItems,
type LinkBasedPagination,
} from "@/app/lib/pagination";

export const metadata: Metadata = {
title: `Raised breast screening exceptions - ${process.env.SERVICE_NAME} - NHS`,
Expand All @@ -42,7 +35,7 @@ export default async function Page({
const sortBy = resolvedSearchParams.sortBy === "0" ? 0 : 1;
const currentPage = Math.max(
1,
parseInt(resolvedSearchParams.page || "1", 10)
Number.parseInt(resolvedSearchParams.page || "1", 10)
);

const sortOptions = [
Expand Down Expand Up @@ -94,34 +87,7 @@ export default async function Page({
);

const linkHeader = response.headers?.get("Link") || response.linkHeader;
const paginationLinks = parseLinkHeader(linkHeader || "");

let totalPages = response.data.TotalPages;
let detectedCurrentPage = currentPage;

if (paginationLinks.last) {
totalPages = extractPageFromUrl(paginationLinks.last);
}

if (paginationLinks.next && !paginationLinks.previous) {
detectedCurrentPage = 1;
} else if (paginationLinks.previous && !paginationLinks.next) {
detectedCurrentPage = totalPages;
} else if (paginationLinks.next) {
detectedCurrentPage = extractPageFromUrl(paginationLinks.next) - 1;
}

const linkBasedPagination: LinkBasedPagination = {
links: paginationLinks,
currentPage: detectedCurrentPage,
totalPages: totalPages,
};

const paginationItems = generatePaginationItems(
linkBasedPagination,
sortBy
);

const totalPages = response.data.TotalPages || 1;
const pageSize = 10;
const totalItems = Number(response.data.TotalItems) || 0;
const startItem = totalItems > 0 ? (currentPage - 1) * pageSize + 1 : 0;
Expand Down Expand Up @@ -170,26 +136,11 @@ export default async function Page({
</div>
{totalPages > 1 && (
<Pagination
items={paginationItems}
previous={
paginationLinks.previous
? {
href: convertToLocalUrl(
paginationLinks.previous,
sortBy
)!,
}
: undefined
}
next={
paginationLinks.next
? {
href: convertToLocalUrl(
paginationLinks.next,
sortBy
)!,
}
: undefined
linkHeader={linkHeader}
currentPage={currentPage}
totalPages={totalPages}
buildUrl={(page) =>
`/exceptions/raised?sortBy=${sortBy}&page=${page}`
}
/>
)}
Expand Down
Loading
Loading