diff --git a/backend/dev-dockerfile b/backend/dev-dockerfile index 0fbd123e..451218df 100644 --- a/backend/dev-dockerfile +++ b/backend/dev-dockerfile @@ -2,3 +2,4 @@ FROM amazoncorretto:17 ENV HOME=/app RUN mkdir -p $HOME WORKDIR $HOME +ENTRYPOINT [ "./gradlew", "bootRun", "--continuous", "--args=--server.port=8081" ] \ No newline at end of file diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index bf39617d..d1245d58 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -1,9 +1,8 @@ spring: datasource: - url: ${POSTGRES_HOST:postgres_fqdn}?sslmode=require - username: ${DB_USERNAME:postgres_user} - password: ${POSTGRES_USER:postgres_password} - name: ${POSTGRES_DB:postgres_db_name} + url: jdbc:postgresql://${POSTGRES_HOST:db}:${DB_PORT:5432}/${POSTGRES_DB:reportvision}?sslmode=${SSL_MODE:disable} + username: ${POSTGRES_USER:postgres} + password: ${POSTGRES_PASSWORD:super_secret_password} devtools: restart: enabled: true diff --git a/dev-env.yaml b/dev-env.yaml index 38acb615..3fcf9d04 100644 --- a/dev-env.yaml +++ b/dev-env.yaml @@ -31,11 +31,12 @@ services: ports: - "5432:5432" environment: - POSTGRES_DB: ${postgres_db_name} - POSTGRES_HOST: ${postgres_fqdn} - POSTGRES_USER: ${postgres_user} - POSTGRES_PASSWORD: ${postgres_password} - sslmode: require + POSTGRES_DB: ${postgres_db_name:-reportvision} + POSTGRES_HOST: ${postgres_fqdn:-localhost} + POSTGRES_USER: ${postgres_user:-postgres} + POSTGRES_PASSWORD: ${postgres_password:-super_secret_password} + DB_PORT: ${postgres_port:-5432} + SSL_MODE: ${postgres_sslmode:-disable} api: build: context: ./backend diff --git a/frontend/e2e/App.spec.ts b/frontend/e2e/App.spec.ts index cb577c88..9effb67c 100644 --- a/frontend/e2e/App.spec.ts +++ b/frontend/e2e/App.spec.ts @@ -22,31 +22,27 @@ test.describe("when templates exist", async () => { const templates = [ { name: "MumpsQuestV1", - lab: "Quest", - createdBy: "J.Smith", - status: "Completed", - lastUpdated: new Date(Date.parse("2025-03-24T12:00:00.000-05:00")), + facility: "Quest", + condition: 'Mumps', + createdOn: new Date(Date.parse("2025-03-24T12:00:00.000-05:00")), }, { name: "LBTIRadar", - lab: "Radar", - createdBy: "C.Alex", - status: "Completed", - lastUpdated: new Date(Date.parse("2025-05-30T12:00:00.000-05:00")), + condition: 'Covid', + facility: "LBTI", + createdOn: new Date(Date.parse("2025-05-30T12:00:00.000-05:00")), }, { name: "COVIDBaylor1", - lab: "Emory", - createdBy: "A.Bryant", - status: "Completed", - lastUpdated: new Date(Date.parse("2025-06-21T12:00:00.000-05:00")), + condition: 'Covid', + facility: "Baylor", + createdOn: new Date(Date.parse("2025-06-21T12:00:00.000-05:00")), }, { name: "COVIDEMory", - lab: "Baylor", - createdBy: "D.Smith", - status: "Completed", - lastUpdated: new Date(Date.parse("2024-06-21T12:00:00.000-05:00")), + condition: 'Covid', + facility: "Emory", + createdOn: new Date(Date.parse("2024-06-21T12:00:00.000-05:00")), }, ]; localStorage.setItem("templates", JSON.stringify(templates)); @@ -60,14 +56,14 @@ test.describe("when templates exist", async () => { page.getByRole("heading", { name: "Saved Templates" }), ).toBeVisible(); await expect(page.locator("tbody").getByRole("row")).toHaveCount(4); - await page.getByText("Updated On").click(); + await page.getByText("Created On").click(); await expect( page.locator("tbody").locator("tr").nth(0).getByRole("cell").nth(1), - ).toHaveText("6/21/2024"); - await page.getByText("Updated On").click(); + ).toHaveText("Mumps"); + await page.getByText("Created On").click(); await expect( page.locator("tbody").locator("tr").nth(0).getByRole("cell").nth(1), - ).toHaveText("6/21/2025"); + ).toHaveText("Covid"); await page.close(); }); test("has links to extraction", async ({ page, baseURL }) => { diff --git a/frontend/src/components/TemplatesIndex/TemplatesIndex.scss b/frontend/src/components/TemplatesIndex/TemplatesIndex.scss new file mode 100644 index 00000000..caca7b7a --- /dev/null +++ b/frontend/src/components/TemplatesIndex/TemplatesIndex.scss @@ -0,0 +1,16 @@ +.pagination-text { + color: #71767A; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + margin-left: 40px; +} + +.pagination-container { + justify-content: space-between; +} + +.pagination-button-group { + margin-right: 40px; +} \ No newline at end of file diff --git a/frontend/src/components/TemplatesIndex/TemplatesIndex.tsx b/frontend/src/components/TemplatesIndex/TemplatesIndex.tsx index 6c6842d9..21155a96 100644 --- a/frontend/src/components/TemplatesIndex/TemplatesIndex.tsx +++ b/frontend/src/components/TemplatesIndex/TemplatesIndex.tsx @@ -5,11 +5,24 @@ import { useNavigate } from "react-router-dom"; import extractImage from "../../assets/extract_image.svg"; import { useQuery } from "@tanstack/react-query"; import { TemplateAPI } from "../../types/templates.ts"; +import usePagination from "../../hooks/use-pagination/index.ts"; + +import './TemplatesIndex.scss' type TemplateIndexProps = unknown; export const TemplatesIndex: FC = () => { const [templates, setTemplates] = useState([]); + const { + currentItems, + currentPage, + nextPage, + previousPage, + goToPage, + getPageNumbers, + hasNextPage, + hasPreviousPage + } = usePagination(templates, 10, 1); const navigate = useNavigate(); // TODO: Pagination and sorting will be added later const templateQuery = useQuery({ @@ -51,22 +64,13 @@ export const TemplatesIndex: FC = () => { const templateColumnNames = { name: "Name", - labName: "Lab", - lab: "Lab", - createdBy: "Creator", - status: "Status", - updatedAt: "Updated On", + created: "Created On", + facility: "Facility", + condition: "Condition", }; const templateColumnFormatters = { - updatedAt: (d) => { - const date = Date.parse(d); - if (isNaN(date)) { - return new Date().toLocaleDateString(); - } - return new Date(date).toLocaleDateString(); - }, - lastUpdated: (d) => { + created: (d) => { const date = Date.parse(d); if (isNaN(date)) { return new Date().toLocaleDateString(); @@ -77,11 +81,9 @@ export const TemplatesIndex: FC = () => { const templateColumns = [ "name", - "updatedAt", - "createdBy", - "lab", - "status", - "labName", + "condition", + "facility", + "created", ]; useEffect(() => { @@ -148,7 +150,7 @@ export const TemplatesIndex: FC = () => { ); } - + return ( <>
@@ -181,10 +183,44 @@ export const TemplatesIndex: FC = () => {

Saved Templates

+
+

+ Showing {Math.min(currentPage * 10, templates.length)} of {templates.length} templates +

+
+ + + {getPageNumbers().map(pageNum => ( + + ))} + + +
+
+
diff --git a/frontend/src/hooks/use-pagination/index.ts b/frontend/src/hooks/use-pagination/index.ts new file mode 100644 index 00000000..d118ba89 --- /dev/null +++ b/frontend/src/hooks/use-pagination/index.ts @@ -0,0 +1,87 @@ +import { useState, useMemo } from "react"; + + +const usePagination = (items: T[] = [], itemsPerPage = 10, initialPage = 1) => { + const [currentPage, setCurrentPage] = useState(initialPage); + + // Calculate total number of pages + const totalPages = useMemo(() => + Math.ceil(items.length / itemsPerPage), + [items.length, itemsPerPage] + ); + + // Ensure current page stays within bounds + useMemo(() => { + if (currentPage > totalPages) { + setCurrentPage(totalPages || 1); + } + }, [currentPage, totalPages]); + + // Get current page items + const currentItems = useMemo(() => { + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return items.slice(startIndex, endIndex); + }, [items, currentPage, itemsPerPage]); + + // Navigation functions + const goToPage = (pageNumber: number) => { + const page = Math.max(1, Math.min(pageNumber, totalPages)); + setCurrentPage(page); + }; + + const nextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(prev => prev + 1); + } + }; + + const previousPage = () => { + if (currentPage > 1) { + setCurrentPage(prev => prev - 1); + } + }; + + const firstPage = () => { + setCurrentPage(1); + }; + + const lastPage = () => { + setCurrentPage(totalPages); + }; + + // Generate page numbers for pagination display + const getPageNumbers = (maxVisible = 5) => { + const pages = []; + let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2)); + const endPage = Math.min(totalPages, startPage + maxVisible - 1); + + // Adjust start page if end page is maxed out + if (endPage - startPage + 1 < maxVisible) { + startPage = Math.max(1, endPage - maxVisible + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + return pages; + }; + + return { + currentPage, + currentItems, + totalPages, + itemsPerPage, + goToPage, + nextPage, + previousPage, + firstPage, + lastPage, + getPageNumbers, + hasNextPage: currentPage < totalPages, + hasPreviousPage: currentPage > 1 + }; + }; + +export default usePagination; \ No newline at end of file diff --git a/frontend/src/hooks/use-pagination/use-pagination.test.ts b/frontend/src/hooks/use-pagination/use-pagination.test.ts new file mode 100644 index 00000000..d2073894 --- /dev/null +++ b/frontend/src/hooks/use-pagination/use-pagination.test.ts @@ -0,0 +1,98 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import usePagination from './index'; + +describe('usePagination hook', () => { + const items: number[] = Array.from({ length: 50 }, (_, i) => i + 1); // Sample items [1, 2, ..., 50] + + it('should initialize with the correct state', () => { + const { result } = renderHook(() => usePagination(items, 10, 1)); + + expect(result.current.currentPage).toBe(1); + expect(result.current.totalPages).toBe(5); + expect(result.current.currentItems).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it('should navigate to the next page', () => { + const { result } = renderHook(() => usePagination(items, 10, 1)); + + act(() => { + result.current.nextPage(); + }); + + expect(result.current.currentPage).toBe(2); + expect(result.current.currentItems).toEqual([11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); + }); + + it('should navigate to the previous page', () => { + const { result } = renderHook(() => usePagination(items, 10, 2)); + + act(() => { + result.current.previousPage(); + }); + + expect(result.current.currentPage).toBe(1); + expect(result.current.currentItems).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it('should navigate to the first page', () => { + const { result } = renderHook(() => usePagination(items, 10, 3)); + + act(() => { + result.current.firstPage(); + }); + + expect(result.current.currentPage).toBe(1); + expect(result.current.currentItems).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it('should navigate to the last page', () => { + const { result } = renderHook(() => usePagination(items, 10, 1)); + + act(() => { + result.current.lastPage(); + }); + + expect(result.current.currentPage).toBe(5); + expect(result.current.currentItems).toEqual([41, 42, 43, 44, 45, 46, 47, 48, 49, 50]); + }); + + it('should navigate to a specific page', () => { + const { result } = renderHook(() => usePagination(items, 10, 1)); + + act(() => { + result.current.goToPage(3); + }); + + expect(result.current.currentPage).toBe(3); + expect(result.current.currentItems).toEqual([21, 22, 23, 24, 25, 26, 27, 28, 29, 30]); + }); + + it('should generate correct page numbers', () => { + const { result } = renderHook(() => usePagination(items, 10, 3)); + + const pageNumbers = result.current.getPageNumbers(5); + expect(pageNumbers).toEqual([1, 2, 3, 4, 5]); + }); + + it('should handle edge cases for page numbers', () => { + const { result } = renderHook(() => usePagination(items, 10, 5)); + + const pageNumbers = result.current.getPageNumbers(5); + expect(pageNumbers).toEqual([1, 2, 3, 4, 5]); + }); + + it('should handle hasNextPage and hasPreviousPage correctly', () => { + const { result } = renderHook(() => usePagination(items, 10, 1)); + + expect(result.current.hasNextPage).toBe(true); + expect(result.current.hasPreviousPage).toBe(false); + + act(() => { + result.current.goToPage(5); + }); + + expect(result.current.hasNextPage).toBe(false); + expect(result.current.hasPreviousPage).toBe(true); + }); +}); diff --git a/frontend/src/pages/SaveTemplate.tsx b/frontend/src/pages/SaveTemplate.tsx index ece2ee22..49247fa5 100644 --- a/frontend/src/pages/SaveTemplate.tsx +++ b/frontend/src/pages/SaveTemplate.tsx @@ -78,6 +78,7 @@ export const SaveTemplate = () => { name, condition, facility, + created: new Date().toLocaleDateString(), pages: pages, };