diff --git a/application/CohortManager/src/Functions/Shared/Common/Pagination/IPaginationService.cs b/application/CohortManager/src/Functions/Shared/Common/Pagination/IPaginationService.cs index b6e0f759dc..6e698143b3 100644 --- a/application/CohortManager/src/Functions/Shared/Common/Pagination/IPaginationService.cs +++ b/application/CohortManager/src/Functions/Shared/Common/Pagination/IPaginationService.cs @@ -4,6 +4,6 @@ namespace Common; public interface IPaginationService { - PaginationResult GetPaginatedResult(IQueryable source, int page = 1); + PaginationResult GetPaginatedResult(IQueryable source, int page, int pageSize); Dictionary AddNavigationHeaders(HttpRequestData request, PaginationResult paginationResult); } diff --git a/application/CohortManager/src/Functions/Shared/Common/Pagination/PaginationService.cs b/application/CohortManager/src/Functions/Shared/Common/Pagination/PaginationService.cs index 8a25d6d85e..b72f3331b2 100644 --- a/application/CohortManager/src/Functions/Shared/Common/Pagination/PaginationService.cs +++ b/application/CohortManager/src/Functions/Shared/Common/Pagination/PaginationService.cs @@ -4,16 +4,18 @@ namespace Common; public class PaginationService : IPaginationService { - private const int pageSize = 10; - /// /// Gets paginated results using page-based pagination /// /// The queryable source /// The page number (1-based) + /// The number of items per page (default: 10) /// Paginated result - public PaginationResult GetPaginatedResult(IQueryable source, int page = 1) + public PaginationResult GetPaginatedResult(IQueryable 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); diff --git a/application/CohortManager/src/Functions/screeningDataServices/GetValidationExceptions/GetValidationExceptions.cs b/application/CohortManager/src/Functions/screeningDataServices/GetValidationExceptions/GetValidationExceptions.cs index c9ec3e9f33..e90f849288 100644 --- a/application/CohortManager/src/Functions/screeningDataServices/GetValidationExceptions/GetValidationExceptions.cs +++ b/application/CohortManager/src/Functions/screeningDataServices/GetValidationExceptions/GetValidationExceptions.cs @@ -47,7 +47,7 @@ public async Task 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); @@ -72,11 +72,11 @@ public async Task 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) { @@ -85,9 +85,9 @@ public async Task Run([HttpTrigger(AuthorizationLevel.Anonymou } } - private HttpResponseData CreatePaginatedResponse(HttpRequestData request, IQueryable source, int page) + private HttpResponseData CreatePaginatedResponse(HttpRequestData request, IQueryable 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); diff --git a/application/CohortManager/src/Web/app/components/pagination.tsx b/application/CohortManager/src/Web/app/components/pagination.tsx index ff9b479910..9ca365a8b9 100644 --- a/application/CohortManager/src/Web/app/components/pagination.tsx +++ b/application/CohortManager/src/Web/app/components/pagination.tsx @@ -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 (