From 57c926afddd6ff8b630ee2048257a596010454ee Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 29 Oct 2024 12:01:08 -0700 Subject: [PATCH 01/43] transferred the frontend changes from the old recentOpening branch --- frontend/src/App.tsx | 6 - .../src/__test__/screens/Reports.test.tsx | 56 --- .../src/components/BCHeaderwSide/constants.ts | 15 +- .../src/components/BarChartGrouped/index.tsx | 180 +++------- .../Opening/RecentOpeningsDataTable/index.tsx | 331 ++++++++++++++++++ .../RecentOpeningsDataTable/styles.scss | 165 +++++++++ .../RecentOpeningsDataTable/testData.ts | 284 +++++++++++++++ frontend/src/components/OpeningsTab/index.tsx | 48 ++- .../Openings/AdvancedSearchDropdown/index.tsx | 2 +- .../Openings/OpeningsSearchBar/index.tsx | 10 +- .../Openings/SearchScreenDataTable/index.tsx | 67 +++- .../src/screens/DashboardRedirect/index.tsx | 2 +- frontend/src/screens/Reports/Reports.scss | 0 frontend/src/screens/Reports/index.tsx | 196 ----------- frontend/src/services/OpeningService.ts | 41 ++- .../queries/dashboard/dashboardQueries.ts | 52 +++ .../services/queries/search/openingQueries.ts | 11 +- frontend/src/services/search/openings.ts | 32 +- frontend/src/types/IOpeningPerYear.ts | 6 + frontend/src/utils/DateUtils.ts | 8 + 20 files changed, 1078 insertions(+), 434 deletions(-) delete mode 100644 frontend/src/__test__/screens/Reports.test.tsx create mode 100644 frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx create mode 100644 frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/styles.scss create mode 100644 frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts delete mode 100644 frontend/src/screens/Reports/Reports.scss delete mode 100644 frontend/src/screens/Reports/index.tsx create mode 100644 frontend/src/services/queries/dashboard/dashboardQueries.ts create mode 100644 frontend/src/types/IOpeningPerYear.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2d44cba2..ade6cec8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,7 +8,6 @@ import './custom.scss'; import Landing from "./screens/Landing"; import Help from "./screens/Help"; -import Reports from './screens/Reports'; import SideLayout from './layouts/SideLayout'; import PostLoginRoute from './routes/PostLoginRoute'; import ProtectedRoute from './routes/ProtectedRoute'; @@ -38,11 +37,6 @@ const App: React.FC = () => { } /> } /> - - } /> - - } /> } />} /> diff --git a/frontend/src/__test__/screens/Reports.test.tsx b/frontend/src/__test__/screens/Reports.test.tsx deleted file mode 100644 index 6996653a..00000000 --- a/frontend/src/__test__/screens/Reports.test.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; -import Reports from '../../screens/Reports'; - -describe('Reports', () => { - it('should render the reports page title', () => { - render(); - const titleElement = screen.getByText(/Reports Page/i); - expect(titleElement).toBeDefined(); - }); - - it('should render the form sample title', () => { - render(); - const formTitleElement = screen.getByText(/Form Sample/i); - expect(formTitleElement).toBeDefined(); - }); - - it('should render the date picker component', () => { - render(); - const datePickerElement = screen.getByLabelText(/Start date/i); - expect(datePickerElement).toBeDefined(); - }); - - it('should renders the dropdown component', () => { - render(); - const dropdownElements = screen.getAllByLabelText(/Select Fruit from Dropdown/i); - expect(dropdownElements.length).toBe(8); - }); - - it('renders the modal button', () => { - render(); - const modalButtonElement = screen.getByText(/Launch modal/i); - expect(modalButtonElement).toBeDefined(); - }); - - it('renders the table headers', () => { - render(); - const tableHeaders = ['Name', 'Rule', 'Status', 'Other', 'Example']; - tableHeaders.forEach((header) => { - const headerElement = screen.getByText(header); - expect(headerElement).toBeDefined(); - }); - }); - - it('renders the table rows and cells', () => { - render(); - const tableRows = screen.getAllByRole('row'); - // Excluding the header row - expect(tableRows.length).toBe(8); - - // Example: Check for specific cell content - const cellContent = screen.getByText('Load Balancer 1'); - expect(cellContent).toBeDefined(); - }); -}); diff --git a/frontend/src/components/BCHeaderwSide/constants.ts b/frontend/src/components/BCHeaderwSide/constants.ts index d47d2044..e4e0cc80 100644 --- a/frontend/src/components/BCHeaderwSide/constants.ts +++ b/frontend/src/components/BCHeaderwSide/constants.ts @@ -17,12 +17,6 @@ const mainActivitiesItems: LeftMenu[] = [ { name: 'Main activities', items: [ - { - name: 'Dashboard', - icon: 'Dashboard', - link: '/dashboard', - disabled: false - }, { name: 'Opening', icon: 'MapBoundaryVegetation', @@ -42,17 +36,12 @@ const mainActivitiesItems: LeftMenu[] = [ { name: 'Create an opening', link: '/opening/create', - disabled: false - }, - { - name: 'Reports', - link: '/opening/reports', - disabled: false + disabled: true }, { name: 'Upcoming activities', link: '/opening/upcoming-activities', - disabled: false + disabled: true } ] } diff --git a/frontend/src/components/BarChartGrouped/index.tsx b/frontend/src/components/BarChartGrouped/index.tsx index 2acd8ee9..d5a38a2b 100644 --- a/frontend/src/components/BarChartGrouped/index.tsx +++ b/frontend/src/components/BarChartGrouped/index.tsx @@ -1,79 +1,62 @@ +// components/BarChartGrouped.tsx import React, { useState, useEffect } from "react"; import { GroupedBarChart, ScaleTypes } from "@carbon/charts-react"; import { Dropdown, DatePicker, DatePickerInput } from "@carbon/react"; -import { fetchOpeningsPerYear } from "../../services/OpeningService"; -import { OpeningPerYearChart } from "../../types/OpeningPerYearChart"; +import { useDistrictListQuery, useFetchOpeningsPerYear } from "../../services/queries/dashboard/dashboardQueries"; +import { IOpeningPerYear } from "../../types/IOpeningPerYear"; + import "@carbon/charts/styles.css"; import "./BarChartGrouped.scss"; -interface IDropdownItem { - value: string; - text: string; -} - -/** - * Renders an Bar Chart Grouped component. - * - * @returns {JSX.Element} The rendered BarChartGrouped component. - */ function BarChartGrouped(): JSX.Element { - const [windowWidth, setWindowWidth] = useState(window.innerWidth); - const [chartData, setChartData] = useState([]); - const [isLoading, setIsLoading] = useState(true); const [orgUnitCode, setOrgUnitCode] = useState(null); const [statusCode, setStatusCode] = useState(null); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); + + const formatDateToString = (date: Date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + const formattedStartDate = startDate ? formatDateToString(startDate) : null; + const formattedEndDate = endDate ? formatDateToString(endDate) : null; - const handleResize = () => { - setWindowWidth(window.innerWidth); + const queryProps: IOpeningPerYear = { + orgUnitCode, + statusCode, + entryDateStart: formattedStartDate, + entryDateEnd: formattedEndDate, }; - useEffect(() => { - const fetchChartData = async () => { - try { - setIsLoading(true); - let formattedStartDate: string | null = null; - let formattedEndDate: string | null = null; + // Fetch the openings submission trends data + const { data: chartData = [], isLoading } = useFetchOpeningsPerYear(queryProps); + // Fetch the org units (district list) data + const { data: orgunitsData = [], isLoading: isOrgUnitsLoading } = useDistrictListQuery(); - if (startDate) { - formattedStartDate = formatDateToString(startDate); - } - if (endDate) { - formattedEndDate = formatDateToString(endDate); - } - - const data: OpeningPerYearChart[] = await fetchOpeningsPerYear({ - orgUnitCode, - statusCode, - entryDateStart: formattedStartDate, - entryDateEnd: formattedEndDate, - }); - setChartData(data); - setIsLoading(false); - } catch (error) { - console.error("Error fetching chart data:", error); - setIsLoading(false); - } - }; + // Map the orgunitsData to create orgUnitItems for the Dropdown + const orgUnitItems = orgunitsData?.map((item: any) => ({ + text: item.orgUnitCode, + value: item.orgUnitCode, + })) || []; - fetchChartData(); - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, [orgUnitCode, statusCode, startDate, endDate]); + const statusItems = [ + { value: "APP", text: "Approved" }, + { value: "NAN", text: "Not Approved" }, + ]; - const formatDateToString = (dateToFormat: Date) => { - if (!dateToFormat) return null; - const year = dateToFormat.getFullYear(); - const month = String(dateToFormat.getMonth() + 1).padStart(2, "0"); - const day = String(dateToFormat.getDate()).padStart(2, "0"); - return `${year}-${month}-${day}`; + const setOrgUnitCodeSelected = ({ selectedItem }: { selectedItem: { value: string } }) => { + setOrgUnitCode(selectedItem.value); }; - const colors = { - Openings: "#1192E8", + const setStatusCodeSelected = ({ selectedItem }: { selectedItem: { value: string } }) => { + setStatusCode(selectedItem.value); }; + + const options = { axes: { left: { @@ -84,62 +67,13 @@ function BarChartGrouped(): JSX.Element { mapsTo: "key", }, }, - color: { - scale: colors, - }, + color: { scale: { Openings: "#1192E8" } }, height: "18.5rem", grid: { - x: { - enabled: false, - color: "#d3d3d3", - strokeDashArray: "2,2", - }, - y: { - enabled: true, - color: "#d3d3d3", - strokeDashArray: "2,2", - }, - }, - toolbar: { - enabled: false, - numberOfIcons: 2, - controls: [ - { - type: "Make fullscreen", - }, - { - type: "Make fullscreen", - }, - ], + x: { enabled: false, color: "#d3d3d3", strokeDashArray: "2,2" }, + y: { enabled: true, color: "#d3d3d3", strokeDashArray: "2,2" }, }, - }; - - const orgUnitItems = [ - { value: "DCR", text: "DCR" }, - { value: "XYZ", text: "District 2" }, - // Add more options as needed - ]; - - const statusItems = [ - { value: "APP", text: "Approved" }, - { value: "NAN", text: "Not Approved" }, - // Add more options as needed - ]; - - const setOrgUnitCodeSelected = ({ - selectedItem, - }: { - selectedItem: IDropdownItem; - }) => { - setOrgUnitCode(selectedItem.value); - }; - - const setStatusCodeSelected = ({ - selectedItem, - }: { - selectedItem: IDropdownItem; - }) => { - setStatusCode(selectedItem.value); + toolbar: { enabled: false }, }; return ( @@ -150,7 +84,7 @@ function BarChartGrouped(): JSX.Element { id="district-dropdown" titleText="District" items={orgUnitItems} - itemToString={(item: IDropdownItem) => (item ? item.text : "")} + itemToString={(item:any) => (item ? item.text : "")} onChange={setOrgUnitCodeSelected} label="District" /> @@ -160,35 +94,19 @@ function BarChartGrouped(): JSX.Element { id="status-dropdown" titleText="Status" items={statusItems} - itemToString={(item: IDropdownItem) => (item ? item.text : "")} + itemToString={(item:any) => (item ? item.text : "")} onChange={setStatusCodeSelected} label="Status" />
- setStartDate(dates[0])} - > - + setStartDate(dates[0])}> +
- setEndDate(dates[0])} - > - + setEndDate(dates[0])}> +
@@ -201,6 +119,6 @@ function BarChartGrouped(): JSX.Element { )} ); -}; +} export default BarChartGrouped; diff --git a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx new file mode 100644 index 00000000..772a6a9c --- /dev/null +++ b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx @@ -0,0 +1,331 @@ +import React, { useContext, useEffect, useState } from "react"; +import { + TableToolbar, + TableToolbarAction, + TableToolbarContent, + TableToolbarMenu, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, + Button, + Pagination, + OverflowMenu, + OverflowMenuItem, + Popover, + PopoverContent, + Checkbox, + CheckboxGroup, + Modal, + ActionableNotification +} from "@carbon/react"; +import * as Icons from "@carbon/icons-react"; +import StatusTag from "../../../StatusTag"; +import "./styles.scss"; +import EmptySection from "../../../EmptySection"; +import PaginationContext from "../../../../contexts/PaginationContext"; +import { OpeningsSearch } from "../../../../types/OpeningsSearch"; +import { ITableHeader } from "../../../../types/TableHeader"; +import { MenuItem } from "@carbon/react"; +import { + convertToCSV, + downloadCSV, + downloadPDF, + downloadXLSX, +} from "../../../../utils/fileConversions"; +import { Tooltip } from "@carbon/react"; +import { useNavigate } from "react-router-dom"; + +interface IRecentOpeningsDataTable { + rows: OpeningsSearch[]; + headers: ITableHeader[]; + defaultColumns: ITableHeader[]; + handleCheckboxChange: Function; + setOpeningId: Function; + toggleSpatial: Function; + showSpatial: boolean; + totalItems: number; +} + +const RecentOpeningsDataTable: React.FC = ({ + rows, + headers, + defaultColumns, + showSpatial, + totalItems, +}) => { + const { + handlePageChange, + handleItemsPerPageChange, + itemsPerPage, + setInitialItemsPerPage, + currentPage, + } = useContext(PaginationContext); + const alignTwo = document?.dir === "rtl" ? "bottom-left" : "bottom-right"; + const [openDownload, setOpenDownload] = useState(false); + const [selectedRows, setSelectedRows] = useState([]); // State to store selected rows + const [toastText, setToastText] = useState(null); + const [openingDetails, setOpeningDetails] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + setInitialItemsPerPage(itemsPerPage); + }, [rows, totalItems]); + + // Function to handle row selection changes + const handleRowSelectionChanged = (rowId: string) => { + setSelectedRows((prevSelectedRows) => { + if (prevSelectedRows.includes(rowId)) { + // If the row is already selected, remove it from the selected rows + return prevSelectedRows.filter((id) => id !== rowId); + } else { + // If the row is not selected, add it to the selected rows + return [...prevSelectedRows, rowId]; + } + }); + }; + + //Function to handle the favourite feature of the opening for a user + const handleFavouriteOpening = (rowId: string) => { + console.log(rowId + " has been added as a favourite for the user") + //make a call to the api for the favourite opening when ready + setToastText(`Following "OpeningID ${rowId}"`); + } + + return ( + <> + + + +
+

+ Total Search Results: {totalItems} +

+
+ + console.log("Download Click")}> + Print + + { + console.log("Clicked print"); + }} + > + Download + + +
+
+
+ setOpenDownload(false)} + > + + + { + downloadPDF(headers, rows); + }} + /> + { + const csvData = convertToCSV(headers, rows); + downloadCSV(csvData, "openings-data.csv"); + }} + /> + downloadXLSX(headers, rows)} + /> + + +
+
+
+ + + + {headers.map((header) => + header.selected ? ( + {header.header} + ) : null + )} + + + + {rows && + rows.map((row: any, i: number) => ( + { + //add the api call to send the viewed opening + // await handleRowClick(row.openingId); + setOpeningDetails(true); + }} + > + {headers.map((header) => + header.selected ? ( + + {header.key === "statusDescription" ? ( + + ) : header.key === "actions" ? ( + <> + <> +
+
+ + {rows.length <= 0 ? ( + + ) : null} + + {rows.length > 0 && ( + { + handlePageChange(page); + handleItemsPerPageChange(page, pageSize); + }} + /> + )} + {toastText != null ? ( + setToastText(null)} + actionButtonLabel="Go to track openings" + onActionButtonClick={() => + navigate("/opening?tab=metrics&scrollTo=trackOpenings") + } + /> + ) : null} + + setOpeningDetails(false)} + passiveModal + modalHeading="We are working hard to get this feature asap, unfortunately you cannot view the opening details from SILVA atm." + modalLabel="Opening Details" + /> + + ); +}; + +export default RecentOpeningsDataTable; diff --git a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/styles.scss b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/styles.scss new file mode 100644 index 00000000..badbfad9 --- /dev/null +++ b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/styles.scss @@ -0,0 +1,165 @@ +@use '@bcgov-nr/nr-theme/design-tokens/colors.scss' as colors; +@use '@bcgov-nr/nr-theme/design-tokens/variables.scss' as vars; +@use '@carbon/type'; + + +.search-data-table{ + // nested elements for the search data table only + .table-toolbar{ + border-top: 1px solid var(--#{vars.$bcgov-prefix}-border-subtle-01); + } + .divider{ + width: 1px; + height: 48px; + background-color: var(--#{vars.$bcgov-prefix}-border-subtle-01); + } + .total-results-container{ + height: 100%; + align-items: center; + width: 100%; + display: flex; + padding: 0px 16px 0px 32px; + } + .total-search-results { + @include type.type-style('body-compact-02'); + font-weight: 400; + font-size: 14px; + line-height: 18px; + letter-spacing: 0.16px; + color: var(--bx-text-secondary); + } + .bx--btn--ghost{ + min-height: 48px; + align-items: center; + } + + +} + + +.edit-column-content{ + width: 400px; + .dropdown-label { + padding:16px; + p { + @include type.type-style('label-02'); + font-size: 12px; + } + } + .dropdown-container{ + padding: 16px; + padding-top: 0px; + } + .menu-item{ + font-size: 12px; + } + .checkbox-item .bx--checkbox-label-text{ + font-size: 14px; + line-height: 18px; + letter-spacing: 0.16px; + font-weight: 400; + } + +} +.download-column-content{ + width: 240px; + .menu-item{ + padding: 16px; + } + +} + +.checkbox-tip span{ + @include type.type-style('body-compact-02'); + max-width: 205px; + font-size: 14px; + line-height: 18px; + letter-spacing: 0.16px; +} + +.fav-toast{ + position: fixed; + top: 64px; + right: 16px; + z-index:2; +} + +//Need to find selector for specific screen +.bx--overflow-menu-options{ + width:260px !important +} +.bx--overflow-menu-options__option-content { + overflow:visible; +} + +.activity-table { + margin-bottom: 2.5rem; + + tr > th:first-child, + tr > td:first-child { + padding-left: 2.5rem; + } + + tr > th:last-child, + tr > td:last-child { + padding-right: 2.5rem; + } + + tr > th:last-child div, + tr > td:last-child { + text-align: center; + } + + .activities-table-cell svg { + position: relative; + margin-right: 0.5rem; + top: 0.1875rem; + } +} + +.#{vars.$bcgov-prefix}--data-table thead tr th#blank { + min-width:50px; +} + +.#{vars.$bcgov-prefix}--data-table thead tr th { + background-color: #F3F3F5; + border-top: 1px solid; + border-color: var(--#{vars.$bcgov-prefix}-border-subtle-01); + background-color: var(--#{vars.$bcgov-prefix}-layer-accent-01) !important; +} + +.#{vars.$bcgov-prefix}--data-table thead tr th { + min-width:158px; +} + +.#{vars.$bcgov-prefix}--data-table tr:nth-child(even) td { + background-color: var(--#{vars.$bcgov-prefix}-layer-01) !important; + height: 64px; +} +.#{vars.$bcgov-prefix}--data-table tr:nth-child(odd) td { + background-color: var(--#{vars.$bcgov-prefix}-layer-02) !important; + height: 64px; +} +.#{vars.$bcgov-prefix}--data-table tr:hover td { + background-color: var(--#{vars.$bcgov-prefix}-layer-accent-02) !important; + cursor: pointer; +} +.#{vars.$bcgov-prefix}--pagination { + background-color: var(--#{vars.$bcgov-prefix}-layer-02) !important; +} + +.table-toolbar{ + background-color: var(--#{vars.$bcgov-prefix}-layer-02); + +} + +@media only screen and (max-width: 672px) { + .#{vars.$bcgov-prefix}--data-table-content { + width: 100%; + overflow-x: scroll; + } + + .activity-table { + width: 56.25rem; + } +} diff --git a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts new file mode 100644 index 00000000..a3e5d814 --- /dev/null +++ b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts @@ -0,0 +1,284 @@ +import { ITableHeader } from "../../../../types/TableHeader"; + +export const columns: ITableHeader[] = [ + { + key: 'openingId', + header: 'Opening Id', + selected: true + }, + { + key: 'forestFileId', + header: 'File Id', + selected: true + }, + { + key: 'cuttingPermitId', + header: 'Cutting permit', + selected: true + }, + { + key: 'timberMark', + header: 'Timber mark', + selected: true + }, + { + key: 'cutBlockId', + header: 'Cut block', + selected: true + }, + { + key: 'openingGrossAreaHa', + header: 'Gross Area', + selected: true + }, + + { + key: 'statusDescription', + header: 'Status', + selected: true + }, + { + key: 'categoryDescription', + header: 'Category', + selected: true + }, + { + key: 'disturbanceStartDate', + header: 'Disturbance Date', + selected: false + }, + { + key: 'actions', + header: 'Actions', + selected: true + } +]; + + +export const rows:any = [ + { + id: '114207', + openingId: '114207', + fileId: 'TFL47', + cuttingPermit: '12S', + timberMark: '47/12S', + cutBlock: '12-69', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-10-27', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114206', + openingId: '114206', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-69', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-09-04', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114205', + openingId: '114205', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-09-04', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114204', + openingId: '114204', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-01-16', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-26' + }, + { + id: '114203', + openingId: '114203', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-12-08', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-26' + }, + { + id: '114202', + openingId: '114202', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-11-15', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-25' + }, + { + id: '114201', + openingId: '114201', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-11-15', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-25' + }, + { + id: '114200', + openingId: '114200', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-10-20', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-24' + }, + { + id: '114199', + openingId: '114199', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-10-20', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-24' + }, + { + id: '114198', + openingId: '114198', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-09-12', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-23' + }, + { + id: '114197', + openingId: '114197', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-09-12', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-23' + }, + { + id: '114196', + openingId: '114196', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-08-05', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-22' + }, + { + id: '114195', + openingId: '114195', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-08-05', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-22' + }, + { + id: '114194', + openingId: '114194', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-07-10', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-21' + }, + { + id: '114193', + openingId: '114193', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-07-10', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-21' + } +]; diff --git a/frontend/src/components/OpeningsTab/index.tsx b/frontend/src/components/OpeningsTab/index.tsx index 26f3a8f4..3aa9ea34 100644 --- a/frontend/src/components/OpeningsTab/index.tsx +++ b/frontend/src/components/OpeningsTab/index.tsx @@ -4,7 +4,7 @@ import './styles.scss' import { Location } from '@carbon/icons-react'; import OpeningsMap from '../OpeningsMap'; import OpeningScreenDataTable from '../OpeningScreenDataTable/index'; -import { headers } from '../OpeningScreenDataTable/testData'; +import { columns } from '../Dashboard/Opening/RecentOpeningsDataTable/testData'; import { fetchRecentOpenings } from '../../services/OpeningService'; import SectionTitle from '../SectionTitle'; import TableSkeleton from '../TableSkeleton'; @@ -14,6 +14,9 @@ import { useSelector } from 'react-redux'; import { RootState } from '../../store'; import { generateHtmlFile } from './layersGenerator'; import { getWmsLayersWhitelistUsers, WmsLayersWhitelistUser } from '../../services/SecretsService'; +import { useUserRecentOpeningQuery } from '../../services/queries/search/openingQueries'; +import RecentOpeningsDataTable from '../Dashboard/Opening/RecentOpeningsDataTable'; +import { ITableHeader } from '../../types/TableHeader'; interface Props { showSpatial: boolean; @@ -28,6 +31,8 @@ const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { const [openingPolygonNotFound, setOpeningPolygonNotFound] = useState(false); const [wmsUsersWhitelist, setWmsUsersWhitelist] = useState([]); const userDetails = useSelector((state: RootState) => state.userDetails); + const { data, isFetching } = useUserRecentOpeningQuery(10); + const [headers, setHeaders] = useState(columns); useEffect(() => { const fetchData = async () => { @@ -73,6 +78,31 @@ const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { } }; + const handleCheckboxChange = (columnKey: string) => { + if(columnKey === "select-default"){ + //set to the deafult + setHeaders(columns) + } + else if(columnKey === "select-all"){ + setHeaders((prevHeaders) => + prevHeaders.map((header) => ({ + ...header, + selected: true, // Select all headers + })) + ); + } + else{ + setHeaders((prevHeaders) => + prevHeaders.map((header) => + header.key === columnKey + ? { ...header, selected: !header.selected } + : header + ) + ); + } + + }; + return ( <>
@@ -116,12 +146,16 @@ const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { {loading ? ( ) : ( - + )}
diff --git a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx index 3821b73e..8822d0ee 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx @@ -340,7 +340,7 @@ const AdvancedSearchDropdown: React.FC = ({
= ({ }; const handleSearchClick = () => { + //set the Advanced Filter Dropsdown visibility to false + setIsOpen(false); onSearchClick(); }; + // this function calls handleSearchClick when the enter key is pressed + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleSearchClick(); + } + }; const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; @@ -62,7 +70,7 @@ const OpeningsSearchBar: React.FC = ({ closeButtonLabelText="Clear search input" id={`search-1`} onChange={handleInputChange} // Handle input change - onKeyDown={() => {}} + onKeyDown={handleKeyDown} // Handle enter key press value={searchTerm} /> diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx index fd3b43c3..f638870f 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx @@ -22,7 +22,7 @@ import { Row, Column, MenuItemDivider, - ToastNotification, + Modal, ActionableNotification } from "@carbon/react"; import * as Icons from "@carbon/icons-react"; @@ -42,6 +42,7 @@ import { } from "../../../../utils/fileConversions"; import { Tooltip } from "@carbon/react"; import { useNavigate } from "react-router-dom"; +import { usePostViewedOpening } from "../../../../services/queries/dashboard/dashboardQueries"; interface ISearchScreenDataTable { rows: OpeningsSearch[]; @@ -76,6 +77,8 @@ const SearchScreenDataTable: React.FC = ({ const [openDownload, setOpenDownload] = useState(false); const [selectedRows, setSelectedRows] = useState([]); // State to store selected rows const [toastText, setToastText] = useState(null); + const [openingDetails, setOpeningDetails] = useState(false); + const { mutate: markAsViewedOpening, isError, error } = usePostViewedOpening(); const navigate = useNavigate(); useEffect(() => { @@ -95,6 +98,20 @@ const SearchScreenDataTable: React.FC = ({ }); }; + const handleRowClick = (openingId: string) => { + // Call the mutation to mark as viewed + markAsViewedOpening(openingId, { + onSuccess: () => { + // setToastText(`Successfully marked opening ${openingId} as viewed.`); + console.log(`Successfully marked opening ${openingId} as viewed.`); + }, + onError: (err: any) => { + // setToastText(`Failed to mark as viewed: ${err.message}`); + console.log(`Failed to mark as viewed: ${err.message}`); + } + }); + }; + //Function to handle the favourite feature of the opening for a user const handleFavouriteOpening = (rowId: string) => { console.log(rowId + " has been added as a favourite for the user") @@ -272,7 +289,15 @@ const SearchScreenDataTable: React.FC = ({ {rows && rows.map((row: any, i: number) => ( - + { + //add the api call to send the viewed opening + await handleRowClick(row.openingId); + setOpeningDetails(true) + } + } + > {headers.map((header) => header.selected ? ( = ({
)} - + e.stopPropagation()} // Stop row onClick from triggering + > - handleFavouriteOpening(row.openingId) - } + onClick={(e: any) => { + e.stopPropagation(); // Stop row onClick from triggering + handleFavouriteOpening(row.openingId); + }} /> - downloadPDF(defaultColumns, [row]) - } + onClick={(e: any) => { + e.stopPropagation(); // Stop row onClick from triggering + downloadPDF(defaultColumns, [row]); + }} /> { + onClick={(e: any) => { + e.stopPropagation(); // Stop row onClick from triggering const csvData = convertToCSV(defaultColumns, [ row, ]); @@ -382,7 +414,7 @@ const SearchScreenDataTable: React.FC = ({ }} /> )} - {toastText!=null ? ( + {toastText != null ? ( = ({ closeOnEscape onClose={() => setToastText(null)} actionButtonLabel="Go to track openings" - onActionButtonClick = {() => navigate('/opening?tab=metrics&scrollTo=trackOpenings')} - + onActionButtonClick={() => + navigate("/opening?tab=metrics&scrollTo=trackOpenings") + } /> ) : null} + + setOpeningDetails(false)} + passiveModal + modalHeading="We are working hard to get this feature asap, unfortunately you cannot view the opening details from SILVA atm." + modalLabel="Opening Details" + /> ); }; diff --git a/frontend/src/screens/DashboardRedirect/index.tsx b/frontend/src/screens/DashboardRedirect/index.tsx index 10838afa..3625b32d 100644 --- a/frontend/src/screens/DashboardRedirect/index.tsx +++ b/frontend/src/screens/DashboardRedirect/index.tsx @@ -17,7 +17,7 @@ const DashboardRedirect: React.FC = () => { // Redirect logic based on selectedClientRoles existence useEffect(() => { if (user && selectedClientRoles) { - navigate("/dashboard"); + navigate("/opening"); } }, [user, selectedClientRoles]); diff --git a/frontend/src/screens/Reports/Reports.scss b/frontend/src/screens/Reports/Reports.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/frontend/src/screens/Reports/index.tsx b/frontend/src/screens/Reports/index.tsx deleted file mode 100644 index 38c092d2..00000000 --- a/frontend/src/screens/Reports/index.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React from 'react'; -import { - DatePicker, - DatePickerInput, - Dropdown, - Table, - TableHead, - TableHeader, - TableRow, - TableBody, - TableCell, - ModalWrapper - } from '@carbon/react'; -import './Reports.scss'; - -/** - * Reports component. - * - * This component renders a page with a sample form and a table. - * - * @returns {JSX.Element} The Reports component. - */ -function Reports(): JSX.Element { - const items: string[] = ["Apple", "Mango", "Orange", "Peach"]; - - const rows:any[] = [ - { - id: 'load-balancer-1', - name: 'Load Balancer 1', - rule: 'Round robin', - Status: 'Starting', - other: 'Test', - example: '22', - }, - { - id: 'load-balancer-2', - name: 'Load Balancer 2', - rule: 'DNS delegation', - status: 'Active', - other: 'Test', - example: '22', - }, - { - id: 'load-balancer-3', - name: 'Load Balancer 3', - rule: 'Round robin', - status: 'Disabled', - other: 'Test', - example: '22', - }, - { - id: 'load-balancer-4', - name: 'Load Balancer 4', - rule: 'Round robin', - status: 'Disabled', - other: 'Test', - example: '22', - }, - { - id: 'load-balancer-5', - name: 'Load Balancer 5', - rule: 'Round robin', - status: 'Disabled', - other: 'Test', - example: '22', - }, - { - id: 'load-balancer-6', - name: 'Load Balancer 6', - rule: 'Round robin', - status: 'Disabled', - other: 'Test', - example: '22', - }, - { - id: 'load-balancer-7', - name: 'Load Balancer 7', - rule: 'Round robin', - status: 'Disabled', - other: 'Test', - example: '22', - }, - ]; - - const headers:any[] = ['Name', 'Rule', 'Status', 'Other', 'Example']; - - return ( -
-
-
-
Reports Page
-
-
- -
Form Sample
- -
-
- - - - -
-
- (item ? item : '')} - /> -
-
- -
-
- (item ? item : '')} - /> -
-
- (item ? item : '')} - /> -
-
- (item ? item : '')} - /> -
-
- -
- {}} - > -

Modal content here

-
-
- -
-
- - - - {headers.map((header) => ( - - {header} - - ))} - - - - {rows.map((row) => ( - - {Object.keys(row) - .filter((key) => key !== 'id') - .map((key) => { - return {row[key]}; - })} - - ))} - -
-
-
-
- ); -}; - -export default Reports; diff --git a/frontend/src/services/OpeningService.ts b/frontend/src/services/OpeningService.ts index 81cda2e7..08be9011 100644 --- a/frontend/src/services/OpeningService.ts +++ b/frontend/src/services/OpeningService.ts @@ -4,6 +4,7 @@ import { env } from '../env'; import { RecentAction } from '../types/RecentAction'; import { OpeningPerYearChart } from '../types/OpeningPerYearChart'; import { RecentOpening } from '../types/RecentOpening'; +import { IOpeningPerYear } from '../types/IOpeningPerYear'; const backendUrl = env.VITE_BACKEND_URL; @@ -70,13 +71,6 @@ export async function fetchRecentOpenings(): Promise { } } -interface IOpeningPerYear { - orgUnitCode: string | null; - statusCode: string | null; - entryDateStart: string | null; - entryDateEnd: string | null; -} - /** * Fetch openings per year data from backend. * @@ -122,6 +116,39 @@ export async function fetchOpeningsPerYear(props: IOpeningPerYear): Promise => { + const authToken = getAuthIdToken(); + + try { + let url = `${backendUrl}/api/dashboard-metrics/submission-trends`; + if (props.orgUnitCode || props.statusCode || props.entryDateStart || props.entryDateEnd) { + url += "?"; + if (props.orgUnitCode) url += `orgUnitCode=${props.orgUnitCode}&`; + if (props.statusCode) url += `statusCode=${props.statusCode}&`; + if (props.entryDateStart) url += `entryDateStart=${props.entryDateStart}&`; + if (props.entryDateEnd) url += `entryDateEnd=${props.entryDateEnd}&`; + url = url.replace(/&$/, ""); + } + + const response = await axios.get(url, { + headers: { Authorization: `Bearer ${authToken}` } + }); + + if (response.data && Array.isArray(response.data)) { + return response.data.map(item => ({ + group: "Openings", + key: item.monthName, + value: item.amount + })); + } + + return []; + } catch (error) { + console.error("Error fetching openings per year:", error); + throw error; + } +}; + interface IFreeGrowingProps { orgUnitCode: string; clientNumber: string; diff --git a/frontend/src/services/queries/dashboard/dashboardQueries.ts b/frontend/src/services/queries/dashboard/dashboardQueries.ts new file mode 100644 index 00000000..622b5f88 --- /dev/null +++ b/frontend/src/services/queries/dashboard/dashboardQueries.ts @@ -0,0 +1,52 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { getAuthIdToken } from "../../AuthService"; +import { fetchOpeningsPerYearAPI } from "../../OpeningService"; +import { IOpeningPerYear } from "../../../types/IOpeningPerYear"; +import { fetchOrgUnits } from "../../search/openings"; + +const backendUrl = import.meta.env.VITE_BACKEND_URL; + +// Function to send the POST request +export const postViewedOpening = async (openingId: string): Promise => { + const authToken = getAuthIdToken(); + try { + const response = await axios.post(`${backendUrl}/viewed/${openingId}`, null, { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }); + return response.data; + } catch (error:any) { + if (error.response?.status === 403) { + throw new Error("Forbidden: You don't have permission to view this opening."); + } else { + throw new Error(error.response.data.message); + } + } + }; + + // Hook for using the mutation + export const usePostViewedOpening = () => { + return useMutation({ + mutationFn: (openingId: string) => postViewedOpening(openingId), + }); + }; + +// Custom hook to use in your component +export const useFetchOpeningsPerYear = (props: IOpeningPerYear) => { + return useQuery({ + queryKey: ['openingsPerYear', props], // Cache key including props + queryFn: () => fetchOpeningsPerYearAPI(props), // Fetch function + enabled: true, // For Conditional fetch we can use !!props.orgUnitCode || !!props.statusCode || !!props.entryDateStart || !!props.entryDateEnd + staleTime: 5 * 60 * 1000, // Cache duration (optional) + }); +}; + +export const useDistrictListQuery = () => { + return useQuery({ + queryKey: ["districtList"], + queryFn: fetchOrgUnits + }); +}; + diff --git a/frontend/src/services/queries/search/openingQueries.ts b/frontend/src/services/queries/search/openingQueries.ts index 4f5a6ebe..dd966796 100644 --- a/frontend/src/services/queries/search/openingQueries.ts +++ b/frontend/src/services/queries/search/openingQueries.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { fetchOpeningFilters, fetchOpenings, OpeningFilters } from "../../search/openings"; +import { fetchOpeningFilters, fetchOpenings, fetchUserRecentOpenings, OpeningFilters } from "../../search/openings"; export const useOpeningsQuery = (filters: OpeningFilters, enabled: boolean) => { return useQuery({ @@ -9,6 +9,15 @@ export const useOpeningsQuery = (filters: OpeningFilters, enabled: boolean) => { }); }; +export const useUserRecentOpeningQuery = (limit:number) => { + return useQuery({ + queryKey: ["userRecentOpenings"], + queryFn: () => fetchUserRecentOpenings(limit), + enabled: true, + refetchOnMount: "always" + }); +}; + export const useOpeningFiltersQuery = () => { return useQuery({ queryKey: ["openingFilters"], diff --git a/frontend/src/services/search/openings.ts b/frontend/src/services/search/openings.ts index 59c87b9f..58fe71a3 100644 --- a/frontend/src/services/search/openings.ts +++ b/frontend/src/services/search/openings.ts @@ -67,7 +67,7 @@ export const fetchOpenings = async (filters: OpeningFilters): Promise => { statusList: filters.status, // Keep it as an array entryUserId: filters.clientAcronym, cutBlockId: filters.cutBlock, - cuttinPermitId:filters.cuttingPermit, + cuttingPermitId:filters.cuttingPermit, timbermark:filters.timberMark, myOpenings: filters.openingFilters?.includes("Openings created by me") || undefined, @@ -118,6 +118,36 @@ export const fetchOpenings = async (filters: OpeningFilters): Promise => { }; }; +// Used to fetch the recent openings for a user based on a limit value +export const fetchUserRecentOpenings = async (limit: number): Promise => { + + // Retrieve the auth token + const authToken = getAuthIdToken(); + + // Make the API request with the Authorization header + const response = await axios.get(`${backendUrl}/api/user/recent-openings`, { + headers: { + Authorization: `Bearer ${authToken}` + } + }); + + // Flatten the data part of the response + const flattenedData = response.data.data.map((item: OpeningItem) => ({ + ...item, + statusCode: item.status?.code, + statusDescription: item.status?.description, + categoryCode: item.category?.code, + categoryDescription: item.category?.description, + status: undefined, // Remove the old nested status object + category: undefined // Remove the old nested category object + })); + + // Returning the modified response data with the flattened structure + return { + ...response.data, + data: flattenedData + }; +}; export const fetchCategories = async (): Promise => { // Retrieve the auth token diff --git a/frontend/src/types/IOpeningPerYear.ts b/frontend/src/types/IOpeningPerYear.ts new file mode 100644 index 00000000..4add109d --- /dev/null +++ b/frontend/src/types/IOpeningPerYear.ts @@ -0,0 +1,6 @@ +export interface IOpeningPerYear { + orgUnitCode: string | null; + statusCode: string | null; + entryDateStart: string | null; + entryDateEnd: string | null; + } \ No newline at end of file diff --git a/frontend/src/utils/DateUtils.ts b/frontend/src/utils/DateUtils.ts index a5b416ce..44b66e3d 100644 --- a/frontend/src/utils/DateUtils.ts +++ b/frontend/src/utils/DateUtils.ts @@ -12,3 +12,11 @@ export const dateStringToISO = (date: string): string => { } return ''; }; + +export const formatDateToString = (dateToFormat: Date) => { + if (!dateToFormat) return null; + const year = dateToFormat.getFullYear(); + const month = String(dateToFormat.getMonth() + 1).padStart(2, "0"); + const day = String(dateToFormat.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +}; From eb13223e5ec73854fd5024f550cc011f6d2e43a8 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 29 Oct 2024 14:04:53 -0700 Subject: [PATCH 02/43] brought the backend changes from the recentOpening branch --- .../oracle/dto/OpeningSearchResponseDto.java | 1 + .../OpeningRecentViewRepository.java | 342 ++++++++++++++++++ .../service/OpeningRecentViewService.java | 23 ++ .../postgres/dto/UserRecentOpeningDto.java | 41 +++ .../endpoint/UserRecentOpeningEndpoint.java | 49 +++ .../entity/UserRecentOpeningEntity.java | 65 ++++ .../UserRecentOpeningRepository.java | 16 + .../service/UserRecentOpeningService.java | 102 ++++++ .../V2__create_user_recent_openings_table.sql | 15 + 9 files changed, 654 insertions(+) create mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java create mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningRecentViewService.java create mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java create mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java create mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java create mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserRecentOpeningRepository.java create mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java create mode 100644 backend/src/main/resources/db/migration/V2__create_user_recent_openings_table.sql diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchResponseDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchResponseDto.java index b658b5da..394fff72 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchResponseDto.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchResponseDto.java @@ -43,4 +43,5 @@ public class OpeningSearchResponseDto { private Boolean submittedToFrpa; private String forestFileId; private Long silvaReliefAppId; + private LocalDateTime lastViewDate; } diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java new file mode 100644 index 00000000..9d433694 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java @@ -0,0 +1,342 @@ +package ca.bc.gov.restapi.results.oracle.repository; + +import ca.bc.gov.restapi.results.common.SilvaConstants; +import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; +import ca.bc.gov.restapi.results.common.pagination.PaginationParameters; +import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; +import ca.bc.gov.restapi.results.oracle.enums.OpeningCategoryEnum; +import ca.bc.gov.restapi.results.oracle.enums.OpeningStatusEnum; +import ca.bc.gov.restapi.results.oracle.util.PaginationUtil; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Query; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +/** This class represents the Openings Search repository database access. */ +@Slf4j +@Component +public class OpeningRecentViewRepository { + + private final EntityManager em; + + public OpeningRecentViewRepository(@Qualifier("oracleEntityManagerFactory") EntityManagerFactory emf) { + this.em = emf.createEntityManager(); + } + + /** + * Search Opening with filters. + * + * @param openingIds List of opening ids to search. + * @param pagination Pagination parameters with pagination settings. + * @return Paginated result with found records, if any. + */ + public PaginatedResult getUserRecentOpenings( + List openingIds, PaginationParameters pagination) { + + final String sqlQuery = createNativeSqlQuery(openingIds); + final Query query = setQueryParameters(openingIds, sqlQuery); + + // Limit to 500 records at the database + query.setMaxResults(SilvaConstants.MAX_PAGE_SIZE); + + List result = query.getResultList(); + int lastPage = PaginationUtil.getLastPage(result.size(), pagination.perPage()); + + PaginatedResult paginatedResult = new PaginatedResult<>(); + paginatedResult.setPageIndex(pagination.page()); + paginatedResult.setPerPage(pagination.perPage()); + paginatedResult.setTotalPages(lastPage); + + if (result.isEmpty() || pagination.page() > lastPage) { + log.info("No search openings result for the search given page index and size!"); + paginatedResult.setData(List.of()); + paginatedResult.setTotalPages(result.isEmpty() ? 0 : lastPage); + paginatedResult.setHasNextPage(false); + return paginatedResult; + } + + int startIndex = PaginationUtil.getStartIndex(pagination.page(), pagination.perPage()); + int endIndex = PaginationUtil.getEndIndex(startIndex, pagination.perPage(), result.size()); + + List resultList = + buildResultListDto(result.subList(startIndex, endIndex)); + + paginatedResult.setData(resultList); + paginatedResult.setPerPage(resultList.size()); + paginatedResult.setTotalPages(lastPage); + paginatedResult.setHasNextPage(pagination.page() < lastPage && pagination.page() > 0); + + return paginatedResult; + } + + private List buildResultListDto(List result) { + List resultList = new ArrayList<>(); + + for (Object obj : result) { + int index = 0; + if (obj.getClass().isArray()) { + Object[] row = (Object[]) obj; + OpeningSearchResponseDto searchOpeningDto = new OpeningSearchResponseDto(); + if (row.length > index) { + searchOpeningDto.setOpeningId(getValue(Integer.class, row[index++], "openingId")); + } + + if (row.length > index) { + String openingNumber = getValue(String.class, row[index++], "openingNumber"); + if (!Objects.isNull(openingNumber)) { + searchOpeningDto.setOpeningNumber(openingNumber.trim()); + } + } + + if (row.length > index) { + String category = getValue(String.class, row[index++], "category"); + searchOpeningDto.setCategory(OpeningCategoryEnum.of(category)); + } + + if (row.length > index) { + String status = getValue(String.class, row[index++], "status"); + searchOpeningDto.setStatus(OpeningStatusEnum.of(status)); + } + + if (row.length > index) { + String cuttingPermitId = getValue(String.class, row[index++], "cuttingPermitId"); + searchOpeningDto.setCuttingPermitId(cuttingPermitId); + } + + if (row.length > index) { + String timberMark = getValue(String.class, row[index++], "timberMark"); + searchOpeningDto.setTimberMark(timberMark); + } + + if (row.length > index) { + String cutBlockId = getValue(String.class, row[index++], "cutBlockId"); + searchOpeningDto.setCutBlockId(cutBlockId); + } + + if (row.length > index) { + BigDecimal openingGrossAreaHa = + getValue(BigDecimal.class, row[index++], "openingGrossAreaHa"); + searchOpeningDto.setOpeningGrossAreaHa(openingGrossAreaHa); + } + + if (row.length > index) { + Timestamp startDate = getValue(Timestamp.class, row[index++], "disturbanceStartDate"); + if (!Objects.isNull(startDate)) { + searchOpeningDto.setDisturbanceStartDate(startDate.toLocalDateTime()); + } + } + + if (row.length > index) { + String forestFileId = getValue(String.class, row[index++], "forestFileId"); + searchOpeningDto.setForestFileId(forestFileId); + } + + if (row.length > index) { + String orgUnitCode = getValue(String.class, row[index++], "orgUnitCode"); + searchOpeningDto.setOrgUnitCode(orgUnitCode); + } + + if (row.length > index) { + String orgUnitName = getValue(String.class, row[index++], "orgUnitName"); + searchOpeningDto.setOrgUnitName(orgUnitName); + } + + if (row.length > index) { + String clientNumber = getValue(String.class, row[index++], "clientNumber"); + searchOpeningDto.setClientNumber(clientNumber); + } + + if (row.length > index) { + String clientLocation = getValue(String.class, row[index++], "clientLocation"); + searchOpeningDto.setClientLocation(clientLocation); + } + + if (row.length > index) { + Timestamp regenDelayDate = getValue(Timestamp.class, row[index++], "regenDelayDate"); + if (!Objects.isNull(regenDelayDate)) { + searchOpeningDto.setRegenDelayDate(regenDelayDate.toLocalDateTime()); + } + } + + if (row.length > index) { + Timestamp earlyDate = getValue(Timestamp.class, row[index++], "earlyFreeGrowingDate"); + if (!Objects.isNull(earlyDate)) { + searchOpeningDto.setEarlyFreeGrowingDate(earlyDate.toLocalDateTime()); + } + } + + if (row.length > index) { + Timestamp dateDate = getValue(Timestamp.class, row[index++], "lateFreeGrowingDate"); + if (!Objects.isNull(dateDate)) { + searchOpeningDto.setLateFreeGrowingDate(dateDate.toLocalDateTime()); + } + } + + if (row.length > index) { + Timestamp updateTimestamp = getValue(Timestamp.class, row[index++], "updateTimestamp"); + searchOpeningDto.setUpdateTimestamp(updateTimestamp.toLocalDateTime()); + } + + if (row.length > index) { + String entryUserId = getValue(String.class, row[index++], "entryUserId"); + searchOpeningDto.setEntryUserId(entryUserId); + } + + if (row.length > index) { + BigDecimal silvaReliefAppId = + getValue(BigDecimal.class, row[index++], "submittedToFrpa108"); + boolean submittedApp = silvaReliefAppId.compareTo(BigDecimal.ZERO) > 0; + searchOpeningDto.setSubmittedToFrpa(submittedApp); + if (submittedApp) { + searchOpeningDto.setSilvaReliefAppId(silvaReliefAppId.longValue()); + } + } + + resultList.add(searchOpeningDto); + } + } + + return resultList; + } + + private T getValue(Class clazz, Object obj, String name) { + if (Objects.isNull(obj)) { + log.debug("{} is null", name); + return null; + } + if (clazz.equals(Integer.class) && obj instanceof Integer intVal) { + log.debug("Integer {}={}", name, intVal); + return clazz.cast(obj); + } + if (clazz.equals(String.class) && obj instanceof String strVal) { + log.debug("String {}={}", name, strVal); + return clazz.cast(obj); + } + if (clazz.equals(LocalDateTime.class) && obj instanceof LocalDateTime localDateTime) { + log.debug("LocalDateTime {}={}", name, localDateTime); + return clazz.cast(obj); + } + if (clazz.equals(BigDecimal.class) && obj instanceof BigDecimal bigDecValue) { + log.debug("BigDecimal {}={}", name, bigDecValue); + return clazz.cast(obj); + } + if (clazz.equals(Timestamp.class) && obj instanceof Timestamp timestamp) { + log.debug("Timestamp {}={}", name, timestamp); + return clazz.cast(obj); + } + log.info("Unhandled class {} for {}", obj.getClass().getName(), name); + return null; + } + + private Query setQueryParameters(List openingIds, String nativeQuery) { + Query query = em.createNativeQuery(nativeQuery); + // Binding the openingIds parameters + for (int i = 0; i < openingIds.size(); i++) { + query.setParameter(i + 1, openingIds.get(i)); // 1-based index for parameters + } + return query; + } + + private String createNativeSqlQuery(List openingIds) { + StringBuilder builder = new StringBuilder(); + builder.append("SELECT o.OPENING_ID AS openingId"); + builder.append(",o.OPENING_NUMBER AS openingNumber"); + builder.append(",o.OPEN_CATEGORY_CODE AS category"); + builder.append(",o.OPENING_STATUS_CODE AS status"); + builder.append(",cboa.CUTTING_PERMIT_ID AS cuttingPermitId"); + builder.append(",cboa.TIMBER_MARK AS timberMark"); + builder.append(",cboa.CUT_BLOCK_ID AS cutBlockId"); + builder.append(",cboa.OPENING_GROSS_AREA AS openingGrossArea"); + builder.append(",cboa.DISTURBANCE_START_DATE AS disturbanceStartDate"); + builder.append(",cboa.FOREST_FILE_ID AS forestFileId"); + builder.append(",ou.ORG_UNIT_CODE AS orgUnitCode"); + builder.append(",ou.ORG_UNIT_NAME AS orgUnitName"); + builder.append(",res.CLIENT_NUMBER AS clientNumber"); + builder.append(",res.CLIENT_LOCN_CODE AS clientLocation"); + + String sql; + sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMRG.LATE_OFFSET_YEARS,0)*12))"; + builder.append(sql).append(" AS regenDelayDate"); + + sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMFG.EARLY_OFFSET_YEARS,0)*12))"; + builder.append(sql).append(" AS earlyFreeGrowingDate"); + + sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMFG.LATE_OFFSET_YEARS,0)*12))"; + builder.append(sql).append(" AS lateFreeGrowingDate"); + + builder.append(",o.UPDATE_TIMESTAMP AS updateTimestamp"); + builder.append(",o.ENTRY_USERID AS entryUserId"); + builder.append(",COALESCE(sra.SILV_RELIEF_APPLICATION_ID, 0) AS submittedToFrpa108 "); + builder.append("FROM THE.OPENING o "); + builder.append("LEFT JOIN THE.CUT_BLOCK_OPEN_ADMIN cboa ON (cboa.OPENING_ID = o.OPENING_ID)"); + builder.append("LEFT JOIN THE.ORG_UNIT ou ON (ou.ORG_UNIT_NO = o.ADMIN_DISTRICT_NO)"); + builder.append("LEFT JOIN the.RESULTS_ELECTRONIC_SUBMISSION res ON ("); + builder.append(" res.RESULTS_SUBMISSION_ID = o.RESULTS_SUBMISSION_ID)"); + builder.append("LEFT JOIN THE.CLIENT_ACRONYM ca ON (ca.CLIENT_NUMBER = res.CLIENT_NUMBER) "); + builder.append("LEFT JOIN THE.ACTIVITY_TREATMENT_UNIT atu ON (atu.OPENING_ID = o.OPENING_ID)"); + builder.append("LEFT JOIN THE.SILV_RELIEF_APPLICATION sra ON ("); + builder.append(" sra.ACTIVITY_TREATMENT_UNIT_ID = atu.ACTIVITY_TREATMENT_UNIT_ID"); + builder.append(" AND sra.SILV_RELIEF_APPL_STATUS_CODE = 'APP') "); + builder.append("LEFT JOIN THE.STOCKING_STANDARD_UNIT ssu ON (ssu.OPENING_ID = o.OPENING_ID) "); + builder.append("LEFT JOIN THE.STOCKING_MILESTONE smrg ON ("); + builder.append(" smrg.STOCKING_STANDARD_UNIT_ID = ssu.STOCKING_STANDARD_UNIT_ID"); + builder.append(" AND SMRG.SILV_MILESTONE_TYPE_CODE = 'RG') "); + builder.append("LEFT JOIN THE.STOCKING_MILESTONE smfg ON ("); + builder.append(" smfg.STOCKING_STANDARD_UNIT_ID = ssu.STOCKING_STANDARD_UNIT_ID"); + builder.append(" AND smfg.SILV_MILESTONE_TYPE_CODE = 'FG') "); + builder.append("WHERE 1=1 "); + + if (openingIds != null && !openingIds.isEmpty()) { + builder.append("AND o.OPENING_ID IN ("); + for (int i = 0; i < openingIds.size(); i++) { + builder.append("?"); + if (i < openingIds.size() - 1) { + builder.append(","); + } + } + builder.append(") "); + } + + /* Group by - to avoid duplications */ + builder.append("GROUP BY o.OPENING_ID "); + builder.append(",o.OPENING_NUMBER "); + builder.append(",o.OPEN_CATEGORY_CODE "); + builder.append(",o.OPENING_STATUS_CODE "); + builder.append(",cboa.CUTTING_PERMIT_ID "); + builder.append(",cboa.TIMBER_MARK "); + builder.append(",cboa.CUT_BLOCK_ID "); + builder.append(",cboa.OPENING_GROSS_AREA "); + builder.append(",cboa.DISTURBANCE_START_DATE "); + builder.append(",cboa.FOREST_FILE_ID "); + builder.append(",ou.ORG_UNIT_CODE "); + builder.append(",ou.ORG_UNIT_NAME "); + builder.append(",res.CLIENT_NUMBER "); + builder.append(",res.CLIENT_LOCN_CODE "); + + sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMRG.LATE_OFFSET_YEARS, 0) * 12)) "; + builder.append(sql); + + sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMFG.EARLY_OFFSET_YEARS, 0) * 12)) "; + builder.append(sql); + + sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMFG.LATE_OFFSET_YEARS, 0) * 12)) "; + builder.append(sql); + + builder.append(",o.UPDATE_TIMESTAMP "); + builder.append(",o.ENTRY_USERID "); + builder.append(",COALESCE(sra.SILV_RELIEF_APPLICATION_ID, 0) "); + + // Order by + builder.append("ORDER BY o.OPENING_ID DESC"); + + return builder.toString(); + } +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningRecentViewService.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningRecentViewService.java new file mode 100644 index 00000000..e5910b45 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningRecentViewService.java @@ -0,0 +1,23 @@ +package ca.bc.gov.restapi.results.oracle.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; +import ca.bc.gov.restapi.results.common.pagination.PaginationParameters; +import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; +import ca.bc.gov.restapi.results.oracle.repository.OpeningRecentViewRepository; + +import java.util.List; + +@Service +public class OpeningRecentViewService { + + @Autowired + private OpeningRecentViewRepository openingRecentViewRepository; + + public PaginatedResult getOpeningsByIds(List openingIds) { + PaginationParameters pagination = new PaginationParameters(0, 10); + return openingRecentViewRepository.getUserRecentOpenings(openingIds, pagination); + } +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java new file mode 100644 index 00000000..3ae813ce --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java @@ -0,0 +1,41 @@ +package ca.bc.gov.restapi.results.postgres.dto; + +import java.time.LocalDateTime; + +public class UserRecentOpeningDto { + + private String userId; + private String openingId; + private LocalDateTime lastViewed; + + public UserRecentOpeningDto(String userId, String openingId, LocalDateTime lastViewed) { + this.userId = userId; + this.openingId = openingId; + this.lastViewed = lastViewed; + } + + // Getters and Setters + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getOpeningId() { + return openingId; + } + + public void setOpeningId(String openingId) { + this.openingId = openingId; + } + + public LocalDateTime getLastViewed() { + return lastViewed; + } + + public void setLastViewed(LocalDateTime lastViewed) { + this.lastViewed = lastViewed; + } +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java new file mode 100644 index 00000000..f0a48f73 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java @@ -0,0 +1,49 @@ +package ca.bc.gov.restapi.results.postgres.endpoint; + +import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; +import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; +import ca.bc.gov.restapi.results.postgres.dto.UserRecentOpeningDto; +import ca.bc.gov.restapi.results.postgres.service.UserRecentOpeningService; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class UserRecentOpeningEndpoint { + + private final UserRecentOpeningService userRecentOpeningService; + + /** + * Records the opening viewed by the user based on the provided opening ID. + * + * @param openingId The ID of the opening viewed by the user. + * @return A simple confirmation message or the HTTP code 204-No Content. + */ + @PostMapping("/viewed/{openingId}") + public ResponseEntity recordUserViewedOpening(@PathVariable String openingId) { + // Store the opening and return the DTO + UserRecentOpeningDto recentOpeningDto = userRecentOpeningService.storeViewedOpening(openingId); + return ResponseEntity.ok(recentOpeningDto); + } + + /** + * Retrieves a list of recent openings viewed by the user, limited by the number of results. + * + * @param limit The maximum number of results to return. + * @return A list of opening IDs viewed by the user. + */ + @GetMapping("api/user/recent-openings") + public ResponseEntity> getUserRecentOpenings(@RequestParam(defaultValue = "10") int limit) { + // Fetch recent openings for the logged-in user with the specified limit + PaginatedResult recentOpenings = userRecentOpeningService.getAllRecentOpeningsForUser(limit); + return ResponseEntity.ok(recentOpenings); + } +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java new file mode 100644 index 00000000..9d835fe4 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java @@ -0,0 +1,65 @@ +package ca.bc.gov.restapi.results.postgres.entity; + + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "user_recent_openings") +public class UserRecentOpeningEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + @Column(name = "user_id", nullable = false) + private String userId; + + @Column(name = "opening_id", nullable = false) + private String openingId; + + @Column(name = "last_viewed", nullable = false) + private LocalDateTime lastViewed; + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getOpeningId() { + return openingId; + } + + public void setOpeningId(String openingId) { + this.openingId = openingId; + } + + public LocalDateTime getLastViewed() { + return lastViewed; + } + + public void setLastViewed(LocalDateTime lastViewed) { + this.lastViewed = lastViewed; + } +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserRecentOpeningRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserRecentOpeningRepository.java new file mode 100644 index 00000000..78b2483c --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserRecentOpeningRepository.java @@ -0,0 +1,16 @@ +package ca.bc.gov.restapi.results.postgres.repository; + +import ca.bc.gov.restapi.results.postgres.entity.UserRecentOpeningEntity; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRecentOpeningRepository extends JpaRepository { + UserRecentOpeningEntity findByUserIdAndOpeningId(String userId, String openingId); + // Add a method to fetch recent openings for a user with a limit and sorting by last_viewed in descending order + Page findByUserIdOrderByLastViewedDesc(String userId, Pageable pageable); +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java new file mode 100644 index 00000000..f0e49ecd --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java @@ -0,0 +1,102 @@ +package ca.bc.gov.restapi.results.postgres.service; + +import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; +import ca.bc.gov.restapi.results.common.security.LoggedUserService; +import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; +import ca.bc.gov.restapi.results.oracle.service.OpeningRecentViewService; +import ca.bc.gov.restapi.results.postgres.dto.UserRecentOpeningDto; +import ca.bc.gov.restapi.results.postgres.entity.UserRecentOpeningEntity; +import ca.bc.gov.restapi.results.postgres.repository.UserRecentOpeningRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserRecentOpeningService { + + private final LoggedUserService loggedUserService; + private final UserRecentOpeningRepository userRecentOpeningRepository; + private final OpeningRecentViewService openingRecentViewService; + + /** + * Stores the opening viewed by the user and returns the DTO. + * + * @param openingId The ID of the opening viewed by the user. + * @return A DTO with userId, openingId, and lastViewed timestamp. + */ + public UserRecentOpeningDto storeViewedOpening(String openingId) { + String userId = loggedUserService.getLoggedUserId(); + LocalDateTime lastViewed = LocalDateTime.now(); + + // Verify that the openingId String contains numbers only and no spaces + if (!openingId.matches("^[0-9]*$")) { + throw new IllegalArgumentException("Opening ID must contain numbers only!"); + } + + // Check if the user has already viewed this opening + UserRecentOpeningEntity existingEntity = userRecentOpeningRepository.findByUserIdAndOpeningId(userId, openingId); + + if (existingEntity != null) { + // Update the last viewed timestamp for the existing record + existingEntity.setLastViewed(lastViewed); + userRecentOpeningRepository.save(existingEntity); // Save the updated entity + } else { + // Create a new entity if this openingId is being viewed for the first time + UserRecentOpeningEntity newEntity = new UserRecentOpeningEntity(null, userId, openingId, lastViewed); + userRecentOpeningRepository.save(newEntity); // Save the new entity + } + + // Return the DTO + return new UserRecentOpeningDto(userId, openingId, lastViewed); + } + + /** + * Retrieves the recent openings viewed by the logged-in user, limited by the provided limit. + * + * @param limit The maximum number of recent openings to retrieve. + * @return A list of opening IDs the user has viewed, sorted by last viewed in descending order. + */ + public PaginatedResult getAllRecentOpeningsForUser(int limit) { + String userId = loggedUserService.getLoggedUserId(); + Pageable pageable = PageRequest.of(0, limit); // PageRequest object to apply limit + + // Fetch recent openings for the user + Page recentOpenings = userRecentOpeningRepository + .findByUserIdOrderByLastViewedDesc(userId, pageable); + + // Extract opening IDs as String + Map openingIds = recentOpenings.getContent().stream() + //.map(opening -> String.valueOf(opening.getOpeningId())) // Convert Integer to String + //.collect(Collectors.toList()); + .collect(Collectors.toMap(UserRecentOpeningEntity::getOpeningId, UserRecentOpeningEntity::getLastViewed)); + log.info("User with the userId {} has the following openindIds {}", userId, openingIds); + if (openingIds.isEmpty()) { + return new PaginatedResult<>(); + } + // Call the oracle service method to fetch opening details for the given opening IDs + PaginatedResult pageResult = openingRecentViewService.getOpeningsByIds(new ArrayList<>(openingIds.keySet())); + + pageResult.setData( + pageResult + .getData() + .stream() + .peek(result -> result.setLastViewDate(openingIds.get(result.getOpeningId().toString()))) + .sorted(Comparator.comparing(OpeningSearchResponseDto::getLastViewDate).reversed()) + .collect(Collectors.toList()) + ); + return pageResult; + } + +} diff --git a/backend/src/main/resources/db/migration/V2__create_user_recent_openings_table.sql b/backend/src/main/resources/db/migration/V2__create_user_recent_openings_table.sql new file mode 100644 index 00000000..3cea5b1a --- /dev/null +++ b/backend/src/main/resources/db/migration/V2__create_user_recent_openings_table.sql @@ -0,0 +1,15 @@ +-- Create sequence if it doesn't exist +CREATE SEQUENCE IF NOT EXISTS silva.user_recent_openings_seq +START WITH 1 +INCREMENT BY 1 +MINVALUE 1 +NO MAXVALUE +CACHE 30; + +-- Use the sequence in your table creation or insert statements +CREATE TABLE IF NOT EXISTS silva.user_recent_openings ( + id BIGINT PRIMARY KEY DEFAULT nextval('silva.user_recent_openings_seq'), + opening_id VARCHAR(255) NOT NULL, + user_id VARCHAR(255) NOT NULL, + last_viewed TIMESTAMP DEFAULT NOW() +); From 793e7fc412a4d69da845c7f26cc348a68adbc3e2 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 29 Oct 2024 14:19:01 -0700 Subject: [PATCH 03/43] chore: fixing entity by adding the schemas (#430) --- .../results/oracle/entity/ClientAcronymEntity.java | 10 ++++------ .../oracle/entity/CutBlockOpenAdminEntity.java | 8 ++++---- .../oracle/entity/OpenCategoryCodeEntity.java | 8 ++++---- .../oracle/entity/OpeningAttachmentEntity.java | 10 ++++------ .../restapi/results/oracle/entity/OpeningEntity.java | 8 ++++---- .../restapi/results/oracle/entity/OrgUnitEntity.java | 10 ++++------ .../entity/ResultsElectronicSubmissionEntity.java | 12 +++++------- .../postgres/entity/OpeningsActivityEntity.java | 8 ++++---- .../postgres/entity/OpeningsLastYearEntity.java | 8 ++++---- .../postgres/entity/OracleExtractionLogsEntity.java | 9 ++++----- .../results/postgres/entity/UserOpeningEntity.java | 10 ++++------ 11 files changed, 45 insertions(+), 56 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/ClientAcronymEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/ClientAcronymEntity.java index d943fd81..bec326c8 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/ClientAcronymEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/ClientAcronymEntity.java @@ -7,21 +7,19 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; import lombok.With; -/** This class represents a Client Acronym in the database. */ +/** + * This class represents a Client Acronym in the database. + */ @Data @Builder @With @NoArgsConstructor @AllArgsConstructor @Entity -@Table(name = "CLIENT_ACRONYM") +@Table(schema = "THE", name = "CLIENT_ACRONYM") public class ClientAcronymEntity { @Id diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/CutBlockOpenAdminEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/CutBlockOpenAdminEntity.java index e7bb2724..ce68fa52 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/CutBlockOpenAdminEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/CutBlockOpenAdminEntity.java @@ -9,18 +9,18 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.With; -/** This class represents a CUT_BLOCK_OPEN_ADMIN entity in the database. */ +/** + * This class represents a CUT_BLOCK_OPEN_ADMIN entity in the database. + */ @Data @Builder @With @NoArgsConstructor @AllArgsConstructor -@Table(name = "CUT_BLOCK_OPEN_ADMIN") +@Table(schema = "THE", name = "CUT_BLOCK_OPEN_ADMIN") @Entity public class CutBlockOpenAdminEntity { diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpenCategoryCodeEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpenCategoryCodeEntity.java index 7c77ab0a..5e165d51 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpenCategoryCodeEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpenCategoryCodeEntity.java @@ -8,19 +8,19 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.With; -/** This class represents an Opening Category in the database. */ +/** + * This class represents an Opening Category in the database. + */ @Data @Builder @With @NoArgsConstructor @AllArgsConstructor @Entity -@Table(name = "OPEN_CATEGORY_CODE") +@Table(schema = "THE", name = "OPEN_CATEGORY_CODE") public class OpenCategoryCodeEntity { @Id diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningAttachmentEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningAttachmentEntity.java index b51cc95a..a1cc1825 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningAttachmentEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningAttachmentEntity.java @@ -7,21 +7,19 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; import lombok.With; -/** This class represents an Opening Attachment in the database. */ +/** + * This class represents an Opening Attachment in the database. + */ @Data @Builder @With @NoArgsConstructor @AllArgsConstructor @Entity -@Table(name = "OPENING_ATTACHMENT") +@Table(schema = "THE", name = "OPENING_ATTACHMENT") public class OpeningAttachmentEntity { @Id diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java index 2f024757..9abcfca0 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java @@ -8,19 +8,19 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.With; -/** This class represents an Opening in the database. */ +/** + * This class represents an Opening in the database. + */ @Data @Builder @With @NoArgsConstructor @AllArgsConstructor @Entity -@Table(name = "OPENING") +@Table(schema = "THE", name = "OPENING") public class OpeningEntity { @Id diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OrgUnitEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OrgUnitEntity.java index ca6b1101..424f958a 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OrgUnitEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OrgUnitEntity.java @@ -8,21 +8,19 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; import lombok.With; -/** This class represents an Organization Unity in the database. */ +/** + * This class represents an Organization Unity in the database. + */ @Data @Builder @With @NoArgsConstructor @AllArgsConstructor @Entity -@Table(name = "ORG_UNIT") +@Table(schema = "THE", name = "ORG_UNIT") public class OrgUnitEntity { @Id diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/ResultsElectronicSubmissionEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/ResultsElectronicSubmissionEntity.java index f2ba24f9..9c5780b0 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/ResultsElectronicSubmissionEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/ResultsElectronicSubmissionEntity.java @@ -7,23 +7,21 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; import lombok.With; -/** This class represents an Electronic Submission for the Openings in the database. */ +/** + * This class represents an Electronic Submission for the Openings in the database. + */ @Data @Builder @With @NoArgsConstructor @AllArgsConstructor @Entity -@Table(name = "RESULTS_ELECTRONIC_SUBMISSION") +@Table(schema = "THE", name = "RESULTS_ELECTRONIC_SUBMISSION") public class ResultsElectronicSubmissionEntity { - + @Id @Column(name = "RESULTS_SUBMISSION_ID") private Long resultsSubmissionId; diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsActivityEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsActivityEntity.java index a3087a38..257181d6 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsActivityEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsActivityEntity.java @@ -9,12 +9,12 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.With; -/** This class represents a record in the database for the openings_activity table. */ +/** + * This class represents a record in the database for the openings_activity table. + */ @Data @Builder @With @@ -22,7 +22,7 @@ @AllArgsConstructor @IdClass(OpeningsActivityEntityId.class) @Entity -@Table(name = "openings_activity") +@Table(schema = "silva", name = "openings_activity") public class OpeningsActivityEntity { @Id diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java index 5a4b0c0d..95341e97 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java @@ -8,19 +8,19 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.With; -/** This class represents a record in the database for the openings_last_year table. */ +/** + * This class represents a record in the database for the openings_last_year table. + */ @Data @Builder @With @NoArgsConstructor @AllArgsConstructor @Entity -@Table(name = "openings_last_year") +@Table(schema = "silva", name = "openings_last_year") public class OpeningsLastYearEntity { @Id diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OracleExtractionLogsEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OracleExtractionLogsEntity.java index 5f57252b..1e4940b7 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OracleExtractionLogsEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OracleExtractionLogsEntity.java @@ -11,20 +11,19 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; import lombok.With; -/** This class represents a log message in the database, for the oracle extraction flow. */ +/** + * This class represents a log message in the database, for the oracle extraction flow. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor @With @Entity -@Table(name = "oracle_extraction_logs") +@Table(schema = "silva", name = "oracle_extraction_logs") public class OracleExtractionLogsEntity { @Id diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java index 65980633..2ba819b0 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java @@ -8,21 +8,19 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; import lombok.With; -/** This class represents an Opening saved as favourite to the user. */ +/** + * This class represents an Opening saved as favourite to the user. + */ @Data @Builder @With @NoArgsConstructor @AllArgsConstructor @Entity -@Table(name = "user_openings") +@Table(schema = "silva", name = "user_openings") @IdClass(UserOpeningEntityId.class) public class UserOpeningEntity { From 2a7e76d8936129a4357956df4cb04acff09378ee Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 29 Oct 2024 15:33:35 -0700 Subject: [PATCH 04/43] properly configured wiremock --- .../results/postgres/entity/UserRecentOpeningEntity.java | 2 +- stub/__files/forestclient/findByClientNumber_00149081.json | 7 +++++++ stub/mappings/forestclient_mapping.json | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 stub/__files/forestclient/findByClientNumber_00149081.json diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java index 9d835fe4..7f382913 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java @@ -14,7 +14,7 @@ @NoArgsConstructor @AllArgsConstructor @Entity -@Table(name = "user_recent_openings") +@Table(schema = "silva", name = "user_recent_openings") public class UserRecentOpeningEntity { @Id diff --git a/stub/__files/forestclient/findByClientNumber_00149081.json b/stub/__files/forestclient/findByClientNumber_00149081.json new file mode 100644 index 00000000..cbec2a7f --- /dev/null +++ b/stub/__files/forestclient/findByClientNumber_00149081.json @@ -0,0 +1,7 @@ +{ + "clientNumber": "00149081", + "clientName": "PAULO CORPORATION OF MARS", + "clientStatusCode": "ACT", + "clientTypeCode": "F", + "acronym": "PGCJ" +} \ No newline at end of file diff --git a/stub/mappings/forestclient_mapping.json b/stub/mappings/forestclient_mapping.json index 47cfb36b..e1f20145 100644 --- a/stub/mappings/forestclient_mapping.json +++ b/stub/mappings/forestclient_mapping.json @@ -8,6 +8,9 @@ }, "response": { "status": 200, + "headers": { + "Content-Type": "application/json" + }, "transformers": [ "response-template" ], From ad23c219fdfd96e8d3dfd0cbf838b92edeea30f4 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 29 Oct 2024 16:06:03 -0700 Subject: [PATCH 05/43] changed from sequence to id temporarily --- .../results/postgres/entity/UserRecentOpeningEntity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java index 7f382913..2377812d 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java @@ -18,7 +18,7 @@ public class UserRecentOpeningEntity { @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "user_id", nullable = false) From 47f85d644f3096f959321c4434ebbad8251b7bf8 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 30 Oct 2024 00:34:16 -0700 Subject: [PATCH 06/43] fixed frontend tests --- frontend/package-lock.json | 9 ++- frontend/package.json | 2 +- .../components/BarChartGrouped.test.tsx | 50 +++++++++++------ .../src/__test__/screens/Opening.test.tsx | 56 +++++++++++-------- .../src/components/BarChartGrouped/index.tsx | 2 +- 5 files changed, 70 insertions(+), 49 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ef6bbe42..c0b6cdb3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -43,7 +43,7 @@ }, "devDependencies": { "@testing-library/dom": "^10.2.0", - "@testing-library/jest-dom": "^6.4.5", + "@testing-library/jest-dom": "^6.6.2", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", @@ -4963,11 +4963,10 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", - "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz", + "integrity": "sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==", "dev": true, - "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", diff --git a/frontend/package.json b/frontend/package.json index 689a0b6d..624e56a2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -60,7 +60,7 @@ }, "devDependencies": { "@testing-library/dom": "^10.2.0", - "@testing-library/jest-dom": "^6.4.5", + "@testing-library/jest-dom": "^6.6.2", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", diff --git a/frontend/src/__test__/components/BarChartGrouped.test.tsx b/frontend/src/__test__/components/BarChartGrouped.test.tsx index 16255636..3c3c63a6 100644 --- a/frontend/src/__test__/components/BarChartGrouped.test.tsx +++ b/frontend/src/__test__/components/BarChartGrouped.test.tsx @@ -1,26 +1,40 @@ import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import BarChartGrouped from '../../components/BarChartGrouped'; -import { fetchOpeningsPerYear } from '../../services/OpeningService'; - -vi.mock('../../services/OpeningService', () => ({ - fetchOpeningsPerYear: vi.fn(() => Promise.resolve([ - { group: '2022', key: 'Openings', value: 10 }, - { group: '2023', key: 'Openings', value: 15 }, - ])), +import { useDistrictListQuery, useFetchOpeningsPerYear } from '../../services/queries/dashboard/dashboardQueries'; +import { describe, expect, it } from 'vitest'; +import { vi } from 'vitest'; +import '@testing-library/jest-dom'; +// Mock the hook +vi.mock('../../services/queries/dashboard/dashboardQueries', () => ({ + useFetchOpeningsPerYear: vi.fn(), + useDistrictListQuery: vi.fn(), })); -describe('BarChartGrouped component tests', () => { - it('should render loading state while fetching data and clean it after', async () => { - render(); +const queryClient = new QueryClient(); - const element = await waitFor(() => screen.getByText('Loading...')); +describe('BarChartGrouped component', () => { + it('should display loading state when data is fetching', () => { + // Mock loading state for openings data + (useFetchOpeningsPerYear as any).mockReturnValue({ + data: [], + isLoading: true, + }); - expect(element).toBeDefined(); - - expect(fetchOpeningsPerYear).toHaveBeenCalled(); - expect(screen.queryByTestId('bar-chart')).toBeDefined(); - }); + // If you're using useDistrictListQuery, mock it too + (useDistrictListQuery as any).mockReturnValue({ + data: [], + isLoading: false, + }); + render( + + + + ); + + // Check if loading text is displayed + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); }); diff --git a/frontend/src/__test__/screens/Opening.test.tsx b/frontend/src/__test__/screens/Opening.test.tsx index 0535dc88..4ac7b4a3 100644 --- a/frontend/src/__test__/screens/Opening.test.tsx +++ b/frontend/src/__test__/screens/Opening.test.tsx @@ -6,21 +6,21 @@ import PaginationContext from '../../contexts/PaginationContext'; import { BrowserRouter } from 'react-router-dom'; import * as redux from 'react-redux'; import { RecentOpening } from '../../types/RecentOpening'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +// Mock data and services const data = { - "activityType": "Update", - "openingId": "1541297", - "statusCode": "APP", - "statusDescription": "Approved", - "lastUpdatedLabel": "1 minute ago", - "lastUpdated": "2024-05-16T19:59:21.635Z" + activityType: "Update", + openingId: "1541297", + statusCode: "APP", + statusDescription: "Approved", + lastUpdatedLabel: "1 minute ago", + lastUpdated: "2024-05-16T19:59:21.635Z" }; vi.mock('../../services/SecretsService', () => ({ getWmsLayersWhitelistUsers: vi.fn(() => [ - { - userName: 'TEST' - } + { userName: 'TEST' } ]) })); @@ -46,10 +46,7 @@ vi.mock('../../services/OpeningService', () => ({ { group: '2023', key: 'Openings', value: 15 }, ])), fetchFreeGrowingMilestones: vi.fn(() => Promise.resolve([ - { - group: '1-5', - value: 11 - } + { group: '1-5', value: 11 } ])), fetchRecentActions: vi.fn(() => [ { @@ -73,10 +70,11 @@ const state = { vi.spyOn(redux, 'useSelector') .mockImplementation((callback) => callback(state)); +// Pagination context mock const rows: RecentOpening[] = [{ id: '123', openingId: '123', - fileId: '1', + forestFileId: '1', cuttingPermit: '1', timberMark: '1', cutBlock: '1', @@ -87,7 +85,7 @@ const rows: RecentOpening[] = [{ entryTimestamp: '1', updateTimestamp: '1', }]; - + const paginationValueMock = { getCurrentData: () => rows, currentPage: 0, @@ -99,20 +97,30 @@ const paginationValueMock = { setInitialItemsPerPage: vi.fn(), }; +// Create a query client for testing +const createTestQueryClient = () => new QueryClient({ + defaultOptions: { + queries: { + retry: false, // Disable retries for test stability + }, + }, +}); + describe('Opening screen test cases', () => { - it('should renders Opening Page Title component', async () => { + it('should render Opening Page Title component', async () => { + const queryClient = createTestQueryClient(); + const { getByTestId } = render( - - - - - + + + + + + + ); const pageTitleComp = await waitFor(() => getByTestId('opening-pagetitle')); expect(pageTitleComp).toBeDefined(); - - //const subtitle = 'Create, manage or check opening information'; - //expect(screen.getByText(subtitle)).toBeDefined(); }); }); diff --git a/frontend/src/components/BarChartGrouped/index.tsx b/frontend/src/components/BarChartGrouped/index.tsx index d5a38a2b..c395c2f8 100644 --- a/frontend/src/components/BarChartGrouped/index.tsx +++ b/frontend/src/components/BarChartGrouped/index.tsx @@ -1,5 +1,5 @@ // components/BarChartGrouped.tsx -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { GroupedBarChart, ScaleTypes } from "@carbon/charts-react"; import { Dropdown, DatePicker, DatePickerInput } from "@carbon/react"; import { useDistrictListQuery, useFetchOpeningsPerYear } from "../../services/queries/dashboard/dashboardQueries"; From 94eb7262c924660b387e3d81dd9eeb3b1bf988aa Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 29 Oct 2024 16:11:05 -0700 Subject: [PATCH 07/43] fix: removing code not required for the soft launch (#429) --- frontend/src/components/BCHeaderwSide/constants.ts | 12 +----------- frontend/src/screens/DashboardRedirect/index.tsx | 4 ++-- frontend/src/screens/Opening/index.tsx | 2 +- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/BCHeaderwSide/constants.ts b/frontend/src/components/BCHeaderwSide/constants.ts index e4e0cc80..074342bc 100644 --- a/frontend/src/components/BCHeaderwSide/constants.ts +++ b/frontend/src/components/BCHeaderwSide/constants.ts @@ -16,7 +16,7 @@ export type LeftMenu = { const mainActivitiesItems: LeftMenu[] = [ { name: 'Main activities', - items: [ + items: [ { name: 'Opening', icon: 'MapBoundaryVegetation', @@ -32,16 +32,6 @@ const mainActivitiesItems: LeftMenu[] = [ name: 'Silviculture search', link: '/silviculture-search', disabled: false - }, - { - name: 'Create an opening', - link: '/opening/create', - disabled: true - }, - { - name: 'Upcoming activities', - link: '/opening/upcoming-activities', - disabled: true } ] } diff --git a/frontend/src/screens/DashboardRedirect/index.tsx b/frontend/src/screens/DashboardRedirect/index.tsx index 3625b32d..7a4d41dd 100644 --- a/frontend/src/screens/DashboardRedirect/index.tsx +++ b/frontend/src/screens/DashboardRedirect/index.tsx @@ -3,7 +3,7 @@ import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; import LoginOrgSelection from "../../views/LoginOrgSelection"; import SideLayout from "../../layouts/SideLayout"; -import Dashboard from "../Dashboard"; +import Opening from "../Opening"; import { RootState } from "../../store"; import { useSelector } from "react-redux"; @@ -24,7 +24,7 @@ const DashboardRedirect: React.FC = () => { return ( <> {user && selectedClientRoles ? ( - } /> + } /> ) : ( )} diff --git a/frontend/src/screens/Opening/index.tsx b/frontend/src/screens/Opening/index.tsx index e350f3e5..08bdd018 100644 --- a/frontend/src/screens/Opening/index.tsx +++ b/frontend/src/screens/Opening/index.tsx @@ -46,7 +46,7 @@ const Opening: React.FC = () => { From 3265fc0b570b364985068012423f59b059fe93e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 03:29:02 +0000 Subject: [PATCH 08/43] fix(deps): update dependency @types/node to v22 (#424) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- frontend/package-lock.json | 10 +++++----- frontend/package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c0b6cdb3..4150ff3d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,7 +15,7 @@ "@carbon/react": "^1.27.0", "@redux-devtools/extension": "^3.3.0", "@tanstack/react-query": "^5.50.1", - "@types/node": "^20.0.0", + "@types/node": "^22.0.0", "@vitejs/plugin-react": "^4.0.4", "@vitejs/plugin-react-swc": "^3.3.2", "amazon-cognito-identity-js": "^6.3.13", @@ -5464,12 +5464,12 @@ } }, "node_modules/@types/node": { - "version": "20.16.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", - "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", + "version": "22.8.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.4.tgz", + "integrity": "sha512-SpNNxkftTJOPk0oN+y2bIqurEXHTA2AOZ3EJDDKeJ5VzkvvORSvmQXGQarcOzWV1ac7DCaPBEdMDxBsM+d8jWw==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.19.8" } }, "node_modules/@types/prop-types": { diff --git a/frontend/package.json b/frontend/package.json index 624e56a2..e641aa3d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,7 @@ "@carbon/react": "^1.27.0", "@redux-devtools/extension": "^3.3.0", "@tanstack/react-query": "^5.50.1", - "@types/node": "^20.0.0", + "@types/node": "^22.0.0", "@vitejs/plugin-react": "^4.0.4", "@vitejs/plugin-react-swc": "^3.3.2", "amazon-cognito-identity-js": "^6.3.13", From ac13474c317f1cc2c346353f113bc8b108c2396c Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 30 Oct 2024 12:57:16 -0700 Subject: [PATCH 09/43] fixing the recent openings endpoints and naming convention --- .../endpoint/UserRecentOpeningEndpoint.java | 6 +++--- .../services/queries/dashboard/dashboardQueries.ts | 13 +++++++------ frontend/src/services/search/openings.ts | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java index f0a48f73..485d3e93 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java @@ -27,7 +27,7 @@ public class UserRecentOpeningEndpoint { * @param openingId The ID of the opening viewed by the user. * @return A simple confirmation message or the HTTP code 204-No Content. */ - @PostMapping("/viewed/{openingId}") + @PostMapping("api/users/recent/{openingId}") public ResponseEntity recordUserViewedOpening(@PathVariable String openingId) { // Store the opening and return the DTO UserRecentOpeningDto recentOpeningDto = userRecentOpeningService.storeViewedOpening(openingId); @@ -40,10 +40,10 @@ public ResponseEntity recordUserViewedOpening(@PathVariabl * @param limit The maximum number of results to return. * @return A list of opening IDs viewed by the user. */ - @GetMapping("api/user/recent-openings") + @GetMapping("api/users/recents") public ResponseEntity> getUserRecentOpenings(@RequestParam(defaultValue = "10") int limit) { // Fetch recent openings for the logged-in user with the specified limit PaginatedResult recentOpenings = userRecentOpeningService.getAllRecentOpeningsForUser(limit); return ResponseEntity.ok(recentOpenings); } -} +} \ No newline at end of file diff --git a/frontend/src/services/queries/dashboard/dashboardQueries.ts b/frontend/src/services/queries/dashboard/dashboardQueries.ts index 622b5f88..9a3d2dfb 100644 --- a/frontend/src/services/queries/dashboard/dashboardQueries.ts +++ b/frontend/src/services/queries/dashboard/dashboardQueries.ts @@ -4,17 +4,18 @@ import { getAuthIdToken } from "../../AuthService"; import { fetchOpeningsPerYearAPI } from "../../OpeningService"; import { IOpeningPerYear } from "../../../types/IOpeningPerYear"; import { fetchOrgUnits } from "../../search/openings"; +import { env } from "../../../env"; -const backendUrl = import.meta.env.VITE_BACKEND_URL; +const backendUrl = env.VITE_BACKEND_URL; // Function to send the POST request export const postViewedOpening = async (openingId: string): Promise => { const authToken = getAuthIdToken(); try { - const response = await axios.post(`${backendUrl}/viewed/${openingId}`, null, { + const response = await axios.post(`${backendUrl}/api/users/recent/${openingId}`, null, { headers: { - Authorization: `Bearer ${authToken}`, - }, + Authorization: `Bearer ${authToken}` + } }); return response.data; } catch (error:any) { @@ -29,7 +30,7 @@ export const postViewedOpening = async (openingId: string): Promise => { // Hook for using the mutation export const usePostViewedOpening = () => { return useMutation({ - mutationFn: (openingId: string) => postViewedOpening(openingId), + mutationFn: (openingId: string) => postViewedOpening(openingId) }); }; @@ -39,7 +40,7 @@ export const useFetchOpeningsPerYear = (props: IOpeningPerYear) => { queryKey: ['openingsPerYear', props], // Cache key including props queryFn: () => fetchOpeningsPerYearAPI(props), // Fetch function enabled: true, // For Conditional fetch we can use !!props.orgUnitCode || !!props.statusCode || !!props.entryDateStart || !!props.entryDateEnd - staleTime: 5 * 60 * 1000, // Cache duration (optional) + staleTime: 5 * 60 * 1000 // Cache duration (optional) }); }; diff --git a/frontend/src/services/search/openings.ts b/frontend/src/services/search/openings.ts index 58fe71a3..07e965c9 100644 --- a/frontend/src/services/search/openings.ts +++ b/frontend/src/services/search/openings.ts @@ -125,7 +125,7 @@ export const fetchUserRecentOpenings = async (limit: number): Promise => { const authToken = getAuthIdToken(); // Make the API request with the Authorization header - const response = await axios.get(`${backendUrl}/api/user/recent-openings`, { + const response = await axios.get(`${backendUrl}/api/users/recents`, { headers: { Authorization: `Bearer ${authToken}` } From 74f8ffc4017305b8a02f054e3b8381b8ed87d384 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 30 Oct 2024 15:16:51 -0700 Subject: [PATCH 10/43] reusingthe Openingsearch in a way so it can be used for rect openings as well --- .../results/oracle/SilvaOracleConstants.java | 1 + .../oracle/dto/OpeningSearchFiltersDto.java | 25 +++++++++++++++ .../OpeningRecentViewRepository.java | 1 + .../repository/OpeningSearchRepository.java | 32 +++++++++++++------ .../endpoint/UserRecentOpeningEndpoint.java | 2 +- .../service/UserRecentOpeningService.java | 13 +++++--- frontend/src/services/search/openings.ts | 2 +- 7 files changed, 61 insertions(+), 15 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/SilvaOracleConstants.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/SilvaOracleConstants.java index 68f3732c..dc6abd60 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/SilvaOracleConstants.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/SilvaOracleConstants.java @@ -9,6 +9,7 @@ public class SilvaOracleConstants { public static final String ORG_UNIT = "orgUnit"; public static final String CATEGORY = "category"; public static final String STATUS_LIST = "statusList"; + public static final String OPENING_IDS = "openingIds"; public static final String MY_OPENINGS = "myOpenings"; public static final String SUBMITTED_TO_FRPA = "submittedToFrpa"; public static final String DISTURBANCE_DATE_START = "disturbanceDateStart"; diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java index 62fc0296..487e59c8 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java @@ -35,6 +35,7 @@ public class OpeningSearchFiltersDto { @Setter private String requestUserId; + private List openingIds; /** Creates an instance of the search opening filter dto. */ public OpeningSearchFiltersDto( @@ -58,6 +59,7 @@ public OpeningSearchFiltersDto( this.orgUnit = Objects.isNull(orgUnit) ? null : orgUnit.toUpperCase().trim(); this.category = Objects.isNull(category) ? null : category.toUpperCase().trim(); this.statusList = new ArrayList<>(); + this.openingIds = new ArrayList<>(); if (!Objects.isNull(statusList)) { this.statusList.addAll(statusList.stream().map(s -> String.format("'%s'", s)).toList()); } @@ -82,6 +84,28 @@ public OpeningSearchFiltersDto( Objects.isNull(mainSearchTerm) ? null : mainSearchTerm.toUpperCase().trim(); } + // Create a constructor with only the List openingIds + public OpeningSearchFiltersDto( + List openingIds) { + this.orgUnit = null; + this.category = null; + this.statusList = new ArrayList<>(); + this.openingIds = openingIds; + this.myOpenings = null; + this.submittedToFrpa = false; + this.disturbanceDateStart = null; + this.disturbanceDateEnd = null; + this.regenDelayDateStart = null; + this.regenDelayDateEnd = null; + this.freeGrowingDateStart = null; + this.freeGrowingDateEnd = null; + this.updateDateStart = null; + this.updateDateEnd = null; + this.cuttingPermitId = null; + this.cutBlockId = null; + this.timberMark = null; + this.mainSearchTerm = null; + } /** * Define if a property has value. * @@ -93,6 +117,7 @@ public boolean hasValue(String prop) { case SilvaOracleConstants.ORG_UNIT -> !Objects.isNull(this.orgUnit); case SilvaOracleConstants.CATEGORY -> !Objects.isNull(this.category); case SilvaOracleConstants.STATUS_LIST -> !this.statusList.isEmpty(); + case SilvaOracleConstants.OPENING_IDS -> !this.openingIds.isEmpty(); case SilvaOracleConstants.MY_OPENINGS -> !Objects.isNull(this.myOpenings); case SilvaOracleConstants.SUBMITTED_TO_FRPA -> !Objects.isNull(this.submittedToFrpa); case SilvaOracleConstants.DISTURBANCE_DATE_START -> diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java index 9d433694..a48c5408 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java @@ -42,6 +42,7 @@ public PaginatedResult getUserRecentOpenings( List openingIds, PaginationParameters pagination) { final String sqlQuery = createNativeSqlQuery(openingIds); + log.info("Executing search openings query: {}", sqlQuery); final Query query = setQueryParameters(openingIds, sqlQuery); // Limit to 500 records at the database diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java index 4c79aed8..0a6feb71 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java @@ -68,8 +68,7 @@ public PaginatedResult searchOpeningQuery( int startIndex = PaginationUtil.getStartIndex(pagination.page(), pagination.perPage()); int endIndex = PaginationUtil.getEndIndex(startIndex, pagination.perPage(), result.size()); - List resultList = - buildResultListDto(result.subList(startIndex, endIndex)); + List resultList = buildResultListDto(result.subList(startIndex, endIndex)); paginatedResult.setData(resultList); paginatedResult.setPerPage(resultList.size()); @@ -124,8 +123,7 @@ private List buildResultListDto(List result) { } if (row.length > index) { - BigDecimal openingGrossAreaHa = - getValue(BigDecimal.class, row[index++], "openingGrossAreaHa"); + BigDecimal openingGrossAreaHa = getValue(BigDecimal.class, row[index++], "openingGrossAreaHa"); searchOpeningDto.setOpeningGrossAreaHa(openingGrossAreaHa); } @@ -193,8 +191,7 @@ private List buildResultListDto(List result) { } if (row.length > index) { - BigDecimal silvaReliefAppId = - getValue(BigDecimal.class, row[index++], "submittedToFrpa108"); + BigDecimal silvaReliefAppId = getValue(BigDecimal.class, row[index++], "submittedToFrpa108"); boolean submittedApp = silvaReliefAppId.compareTo(BigDecimal.ZERO) > 0; searchOpeningDto.setSubmittedToFrpa(submittedApp); if (submittedApp) { @@ -246,7 +243,7 @@ private Query setQueryParameters(OpeningSearchFiltersDto filtersDto, String nati boolean itsNumeric = filtersDto.getMainSearchTerm().replaceAll("[0-9]", "").isEmpty(); if (itsNumeric) { log.info("Setting mainSearchTerm as numeric filter value"); - // Opening id or File id + // Opening id or File id query.setParameter("openingOrFile", filtersDto.getMainSearchTerm()); } else { log.info("Setting mainSearchTerm as non-numeric filter value"); @@ -269,7 +266,14 @@ private Query setQueryParameters(OpeningSearchFiltersDto filtersDto, String nati if (filtersDto.hasValue(SilvaOracleConstants.STATUS_LIST)) { log.info("Setting statusList filter values"); - // No need to set value since the query already dit it. Didn't work set through named param + // No need to set value since the query already dit it. Didn't work set through + // named param + } + // similarly for openingIds + if (filtersDto.hasValue(SilvaOracleConstants.OPENING_IDS)) { + log.info("Setting openingIds filter values"); + // No need to set value since the query already dit it. Didn't work set through + // named param } // 4. User entry id if (filtersDto.hasValue(SilvaOracleConstants.MY_OPENINGS)) { @@ -390,8 +394,17 @@ private String createNativeSqlQuery(OpeningSearchFiltersDto filtersDto) { builder.append("WHERE 1=1 "); /* Filters */ + + // List of openings from the openingIds of the filterDto object for the recent openings + if (filtersDto.hasValue(SilvaOracleConstants.OPENING_IDS)) { + String openingIds = String.join(",", filtersDto.getOpeningIds()); + log.info("Filter for openingIds detected! openingIds={}", openingIds); + builder.append(String.format("AND o.OPENING_ID IN (%s) ", openingIds)); + } + // 0. Main number filter [opening_id, opening_number, timber_mark, file_id] - // if it's a number, filter by openingId or fileId, otherwise filter by timber mark and opening + // if it's a number, filter by openingId or fileId, otherwise filter by timber + // mark and opening // number if (filtersDto.hasValue(SilvaOracleConstants.MAIN_SEARCH_TERM)) { log.info("Filter mainSearchTerm detected! mainSearchTerm={}", filtersDto.getMainSearchTerm()); @@ -428,6 +441,7 @@ private String createNativeSqlQuery(OpeningSearchFiltersDto filtersDto) { log.info("Filter statusList detected! statusList={}", statuses); builder.append(String.format("AND o.OPENING_STATUS_CODE IN (%s) ", statuses)); } + // 4. My openings if (filtersDto.hasValue(SilvaOracleConstants.MY_OPENINGS)) { log.info("Filter myOpenings detected! entryUserId={}", filtersDto.getRequestUserId()); diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java index 485d3e93..99a7f92f 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java @@ -40,7 +40,7 @@ public ResponseEntity recordUserViewedOpening(@PathVariabl * @param limit The maximum number of results to return. * @return A list of opening IDs viewed by the user. */ - @GetMapping("api/users/recents") + @GetMapping("api/user/recent-openings") public ResponseEntity> getUserRecentOpenings(@RequestParam(defaultValue = "10") int limit) { // Fetch recent openings for the logged-in user with the specified limit PaginatedResult recentOpenings = userRecentOpeningService.getAllRecentOpeningsForUser(limit); diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java index f0e49ecd..3068401f 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningService.java @@ -1,9 +1,11 @@ package ca.bc.gov.restapi.results.postgres.service; import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; +import ca.bc.gov.restapi.results.common.pagination.PaginationParameters; import ca.bc.gov.restapi.results.common.security.LoggedUserService; +import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchFiltersDto; import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; -import ca.bc.gov.restapi.results.oracle.service.OpeningRecentViewService; +import ca.bc.gov.restapi.results.oracle.service.OpeningService; import ca.bc.gov.restapi.results.postgres.dto.UserRecentOpeningDto; import ca.bc.gov.restapi.results.postgres.entity.UserRecentOpeningEntity; import ca.bc.gov.restapi.results.postgres.repository.UserRecentOpeningRepository; @@ -28,7 +30,7 @@ public class UserRecentOpeningService { private final LoggedUserService loggedUserService; private final UserRecentOpeningRepository userRecentOpeningRepository; - private final OpeningRecentViewService openingRecentViewService; + private final OpeningService openingService; /** * Stores the opening viewed by the user and returns the DTO. @@ -86,8 +88,11 @@ public PaginatedResult getAllRecentOpeningsForUser(int return new PaginatedResult<>(); } // Call the oracle service method to fetch opening details for the given opening IDs - PaginatedResult pageResult = openingRecentViewService.getOpeningsByIds(new ArrayList<>(openingIds.keySet())); - + //convert the openingIds to a list of strings and pass it to the OpeningSearchFiltersDto constructor + OpeningSearchFiltersDto filtersDto = new OpeningSearchFiltersDto(new ArrayList<>(openingIds.keySet())); + PaginationParameters paginationParameters = new PaginationParameters(0, 10); + PaginatedResult pageResult = openingService.openingSearch(filtersDto, paginationParameters); + // perform the sorting and set the lastViewDate to the OpeningSearchResponseDto pageResult.setData( pageResult .getData() diff --git a/frontend/src/services/search/openings.ts b/frontend/src/services/search/openings.ts index 07e965c9..58fe71a3 100644 --- a/frontend/src/services/search/openings.ts +++ b/frontend/src/services/search/openings.ts @@ -125,7 +125,7 @@ export const fetchUserRecentOpenings = async (limit: number): Promise => { const authToken = getAuthIdToken(); // Make the API request with the Authorization header - const response = await axios.get(`${backendUrl}/api/users/recents`, { + const response = await axios.get(`${backendUrl}/api/user/recent-openings`, { headers: { Authorization: `Bearer ${authToken}` } From d7462cf322746c863caeb5dc335566f1403fdb44 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 30 Oct 2024 15:18:37 -0700 Subject: [PATCH 11/43] removing unwanted files for the recntOpenings search --- .../OpeningRecentViewRepository.java | 343 ------------------ .../service/OpeningRecentViewService.java | 23 -- 2 files changed, 366 deletions(-) delete mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java delete mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningRecentViewService.java diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java deleted file mode 100644 index a48c5408..00000000 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningRecentViewRepository.java +++ /dev/null @@ -1,343 +0,0 @@ -package ca.bc.gov.restapi.results.oracle.repository; - -import ca.bc.gov.restapi.results.common.SilvaConstants; -import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; -import ca.bc.gov.restapi.results.common.pagination.PaginationParameters; -import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; -import ca.bc.gov.restapi.results.oracle.enums.OpeningCategoryEnum; -import ca.bc.gov.restapi.results.oracle.enums.OpeningStatusEnum; -import ca.bc.gov.restapi.results.oracle.util.PaginationUtil; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.persistence.Query; -import java.math.BigDecimal; -import java.sql.Timestamp; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Component; - -/** This class represents the Openings Search repository database access. */ -@Slf4j -@Component -public class OpeningRecentViewRepository { - - private final EntityManager em; - - public OpeningRecentViewRepository(@Qualifier("oracleEntityManagerFactory") EntityManagerFactory emf) { - this.em = emf.createEntityManager(); - } - - /** - * Search Opening with filters. - * - * @param openingIds List of opening ids to search. - * @param pagination Pagination parameters with pagination settings. - * @return Paginated result with found records, if any. - */ - public PaginatedResult getUserRecentOpenings( - List openingIds, PaginationParameters pagination) { - - final String sqlQuery = createNativeSqlQuery(openingIds); - log.info("Executing search openings query: {}", sqlQuery); - final Query query = setQueryParameters(openingIds, sqlQuery); - - // Limit to 500 records at the database - query.setMaxResults(SilvaConstants.MAX_PAGE_SIZE); - - List result = query.getResultList(); - int lastPage = PaginationUtil.getLastPage(result.size(), pagination.perPage()); - - PaginatedResult paginatedResult = new PaginatedResult<>(); - paginatedResult.setPageIndex(pagination.page()); - paginatedResult.setPerPage(pagination.perPage()); - paginatedResult.setTotalPages(lastPage); - - if (result.isEmpty() || pagination.page() > lastPage) { - log.info("No search openings result for the search given page index and size!"); - paginatedResult.setData(List.of()); - paginatedResult.setTotalPages(result.isEmpty() ? 0 : lastPage); - paginatedResult.setHasNextPage(false); - return paginatedResult; - } - - int startIndex = PaginationUtil.getStartIndex(pagination.page(), pagination.perPage()); - int endIndex = PaginationUtil.getEndIndex(startIndex, pagination.perPage(), result.size()); - - List resultList = - buildResultListDto(result.subList(startIndex, endIndex)); - - paginatedResult.setData(resultList); - paginatedResult.setPerPage(resultList.size()); - paginatedResult.setTotalPages(lastPage); - paginatedResult.setHasNextPage(pagination.page() < lastPage && pagination.page() > 0); - - return paginatedResult; - } - - private List buildResultListDto(List result) { - List resultList = new ArrayList<>(); - - for (Object obj : result) { - int index = 0; - if (obj.getClass().isArray()) { - Object[] row = (Object[]) obj; - OpeningSearchResponseDto searchOpeningDto = new OpeningSearchResponseDto(); - if (row.length > index) { - searchOpeningDto.setOpeningId(getValue(Integer.class, row[index++], "openingId")); - } - - if (row.length > index) { - String openingNumber = getValue(String.class, row[index++], "openingNumber"); - if (!Objects.isNull(openingNumber)) { - searchOpeningDto.setOpeningNumber(openingNumber.trim()); - } - } - - if (row.length > index) { - String category = getValue(String.class, row[index++], "category"); - searchOpeningDto.setCategory(OpeningCategoryEnum.of(category)); - } - - if (row.length > index) { - String status = getValue(String.class, row[index++], "status"); - searchOpeningDto.setStatus(OpeningStatusEnum.of(status)); - } - - if (row.length > index) { - String cuttingPermitId = getValue(String.class, row[index++], "cuttingPermitId"); - searchOpeningDto.setCuttingPermitId(cuttingPermitId); - } - - if (row.length > index) { - String timberMark = getValue(String.class, row[index++], "timberMark"); - searchOpeningDto.setTimberMark(timberMark); - } - - if (row.length > index) { - String cutBlockId = getValue(String.class, row[index++], "cutBlockId"); - searchOpeningDto.setCutBlockId(cutBlockId); - } - - if (row.length > index) { - BigDecimal openingGrossAreaHa = - getValue(BigDecimal.class, row[index++], "openingGrossAreaHa"); - searchOpeningDto.setOpeningGrossAreaHa(openingGrossAreaHa); - } - - if (row.length > index) { - Timestamp startDate = getValue(Timestamp.class, row[index++], "disturbanceStartDate"); - if (!Objects.isNull(startDate)) { - searchOpeningDto.setDisturbanceStartDate(startDate.toLocalDateTime()); - } - } - - if (row.length > index) { - String forestFileId = getValue(String.class, row[index++], "forestFileId"); - searchOpeningDto.setForestFileId(forestFileId); - } - - if (row.length > index) { - String orgUnitCode = getValue(String.class, row[index++], "orgUnitCode"); - searchOpeningDto.setOrgUnitCode(orgUnitCode); - } - - if (row.length > index) { - String orgUnitName = getValue(String.class, row[index++], "orgUnitName"); - searchOpeningDto.setOrgUnitName(orgUnitName); - } - - if (row.length > index) { - String clientNumber = getValue(String.class, row[index++], "clientNumber"); - searchOpeningDto.setClientNumber(clientNumber); - } - - if (row.length > index) { - String clientLocation = getValue(String.class, row[index++], "clientLocation"); - searchOpeningDto.setClientLocation(clientLocation); - } - - if (row.length > index) { - Timestamp regenDelayDate = getValue(Timestamp.class, row[index++], "regenDelayDate"); - if (!Objects.isNull(regenDelayDate)) { - searchOpeningDto.setRegenDelayDate(regenDelayDate.toLocalDateTime()); - } - } - - if (row.length > index) { - Timestamp earlyDate = getValue(Timestamp.class, row[index++], "earlyFreeGrowingDate"); - if (!Objects.isNull(earlyDate)) { - searchOpeningDto.setEarlyFreeGrowingDate(earlyDate.toLocalDateTime()); - } - } - - if (row.length > index) { - Timestamp dateDate = getValue(Timestamp.class, row[index++], "lateFreeGrowingDate"); - if (!Objects.isNull(dateDate)) { - searchOpeningDto.setLateFreeGrowingDate(dateDate.toLocalDateTime()); - } - } - - if (row.length > index) { - Timestamp updateTimestamp = getValue(Timestamp.class, row[index++], "updateTimestamp"); - searchOpeningDto.setUpdateTimestamp(updateTimestamp.toLocalDateTime()); - } - - if (row.length > index) { - String entryUserId = getValue(String.class, row[index++], "entryUserId"); - searchOpeningDto.setEntryUserId(entryUserId); - } - - if (row.length > index) { - BigDecimal silvaReliefAppId = - getValue(BigDecimal.class, row[index++], "submittedToFrpa108"); - boolean submittedApp = silvaReliefAppId.compareTo(BigDecimal.ZERO) > 0; - searchOpeningDto.setSubmittedToFrpa(submittedApp); - if (submittedApp) { - searchOpeningDto.setSilvaReliefAppId(silvaReliefAppId.longValue()); - } - } - - resultList.add(searchOpeningDto); - } - } - - return resultList; - } - - private T getValue(Class clazz, Object obj, String name) { - if (Objects.isNull(obj)) { - log.debug("{} is null", name); - return null; - } - if (clazz.equals(Integer.class) && obj instanceof Integer intVal) { - log.debug("Integer {}={}", name, intVal); - return clazz.cast(obj); - } - if (clazz.equals(String.class) && obj instanceof String strVal) { - log.debug("String {}={}", name, strVal); - return clazz.cast(obj); - } - if (clazz.equals(LocalDateTime.class) && obj instanceof LocalDateTime localDateTime) { - log.debug("LocalDateTime {}={}", name, localDateTime); - return clazz.cast(obj); - } - if (clazz.equals(BigDecimal.class) && obj instanceof BigDecimal bigDecValue) { - log.debug("BigDecimal {}={}", name, bigDecValue); - return clazz.cast(obj); - } - if (clazz.equals(Timestamp.class) && obj instanceof Timestamp timestamp) { - log.debug("Timestamp {}={}", name, timestamp); - return clazz.cast(obj); - } - log.info("Unhandled class {} for {}", obj.getClass().getName(), name); - return null; - } - - private Query setQueryParameters(List openingIds, String nativeQuery) { - Query query = em.createNativeQuery(nativeQuery); - // Binding the openingIds parameters - for (int i = 0; i < openingIds.size(); i++) { - query.setParameter(i + 1, openingIds.get(i)); // 1-based index for parameters - } - return query; - } - - private String createNativeSqlQuery(List openingIds) { - StringBuilder builder = new StringBuilder(); - builder.append("SELECT o.OPENING_ID AS openingId"); - builder.append(",o.OPENING_NUMBER AS openingNumber"); - builder.append(",o.OPEN_CATEGORY_CODE AS category"); - builder.append(",o.OPENING_STATUS_CODE AS status"); - builder.append(",cboa.CUTTING_PERMIT_ID AS cuttingPermitId"); - builder.append(",cboa.TIMBER_MARK AS timberMark"); - builder.append(",cboa.CUT_BLOCK_ID AS cutBlockId"); - builder.append(",cboa.OPENING_GROSS_AREA AS openingGrossArea"); - builder.append(",cboa.DISTURBANCE_START_DATE AS disturbanceStartDate"); - builder.append(",cboa.FOREST_FILE_ID AS forestFileId"); - builder.append(",ou.ORG_UNIT_CODE AS orgUnitCode"); - builder.append(",ou.ORG_UNIT_NAME AS orgUnitName"); - builder.append(",res.CLIENT_NUMBER AS clientNumber"); - builder.append(",res.CLIENT_LOCN_CODE AS clientLocation"); - - String sql; - sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMRG.LATE_OFFSET_YEARS,0)*12))"; - builder.append(sql).append(" AS regenDelayDate"); - - sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMFG.EARLY_OFFSET_YEARS,0)*12))"; - builder.append(sql).append(" AS earlyFreeGrowingDate"); - - sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMFG.LATE_OFFSET_YEARS,0)*12))"; - builder.append(sql).append(" AS lateFreeGrowingDate"); - - builder.append(",o.UPDATE_TIMESTAMP AS updateTimestamp"); - builder.append(",o.ENTRY_USERID AS entryUserId"); - builder.append(",COALESCE(sra.SILV_RELIEF_APPLICATION_ID, 0) AS submittedToFrpa108 "); - builder.append("FROM THE.OPENING o "); - builder.append("LEFT JOIN THE.CUT_BLOCK_OPEN_ADMIN cboa ON (cboa.OPENING_ID = o.OPENING_ID)"); - builder.append("LEFT JOIN THE.ORG_UNIT ou ON (ou.ORG_UNIT_NO = o.ADMIN_DISTRICT_NO)"); - builder.append("LEFT JOIN the.RESULTS_ELECTRONIC_SUBMISSION res ON ("); - builder.append(" res.RESULTS_SUBMISSION_ID = o.RESULTS_SUBMISSION_ID)"); - builder.append("LEFT JOIN THE.CLIENT_ACRONYM ca ON (ca.CLIENT_NUMBER = res.CLIENT_NUMBER) "); - builder.append("LEFT JOIN THE.ACTIVITY_TREATMENT_UNIT atu ON (atu.OPENING_ID = o.OPENING_ID)"); - builder.append("LEFT JOIN THE.SILV_RELIEF_APPLICATION sra ON ("); - builder.append(" sra.ACTIVITY_TREATMENT_UNIT_ID = atu.ACTIVITY_TREATMENT_UNIT_ID"); - builder.append(" AND sra.SILV_RELIEF_APPL_STATUS_CODE = 'APP') "); - builder.append("LEFT JOIN THE.STOCKING_STANDARD_UNIT ssu ON (ssu.OPENING_ID = o.OPENING_ID) "); - builder.append("LEFT JOIN THE.STOCKING_MILESTONE smrg ON ("); - builder.append(" smrg.STOCKING_STANDARD_UNIT_ID = ssu.STOCKING_STANDARD_UNIT_ID"); - builder.append(" AND SMRG.SILV_MILESTONE_TYPE_CODE = 'RG') "); - builder.append("LEFT JOIN THE.STOCKING_MILESTONE smfg ON ("); - builder.append(" smfg.STOCKING_STANDARD_UNIT_ID = ssu.STOCKING_STANDARD_UNIT_ID"); - builder.append(" AND smfg.SILV_MILESTONE_TYPE_CODE = 'FG') "); - builder.append("WHERE 1=1 "); - - if (openingIds != null && !openingIds.isEmpty()) { - builder.append("AND o.OPENING_ID IN ("); - for (int i = 0; i < openingIds.size(); i++) { - builder.append("?"); - if (i < openingIds.size() - 1) { - builder.append(","); - } - } - builder.append(") "); - } - - /* Group by - to avoid duplications */ - builder.append("GROUP BY o.OPENING_ID "); - builder.append(",o.OPENING_NUMBER "); - builder.append(",o.OPEN_CATEGORY_CODE "); - builder.append(",o.OPENING_STATUS_CODE "); - builder.append(",cboa.CUTTING_PERMIT_ID "); - builder.append(",cboa.TIMBER_MARK "); - builder.append(",cboa.CUT_BLOCK_ID "); - builder.append(",cboa.OPENING_GROSS_AREA "); - builder.append(",cboa.DISTURBANCE_START_DATE "); - builder.append(",cboa.FOREST_FILE_ID "); - builder.append(",ou.ORG_UNIT_CODE "); - builder.append(",ou.ORG_UNIT_NAME "); - builder.append(",res.CLIENT_NUMBER "); - builder.append(",res.CLIENT_LOCN_CODE "); - - sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMRG.LATE_OFFSET_YEARS, 0) * 12)) "; - builder.append(sql); - - sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMFG.EARLY_OFFSET_YEARS, 0) * 12)) "; - builder.append(sql); - - sql = ",ADD_MONTHS(cboa.DISTURBANCE_START_DATE, (COALESCE(SMFG.LATE_OFFSET_YEARS, 0) * 12)) "; - builder.append(sql); - - builder.append(",o.UPDATE_TIMESTAMP "); - builder.append(",o.ENTRY_USERID "); - builder.append(",COALESCE(sra.SILV_RELIEF_APPLICATION_ID, 0) "); - - // Order by - builder.append("ORDER BY o.OPENING_ID DESC"); - - return builder.toString(); - } -} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningRecentViewService.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningRecentViewService.java deleted file mode 100644 index e5910b45..00000000 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningRecentViewService.java +++ /dev/null @@ -1,23 +0,0 @@ -package ca.bc.gov.restapi.results.oracle.service; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; -import ca.bc.gov.restapi.results.common.pagination.PaginationParameters; -import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; -import ca.bc.gov.restapi.results.oracle.repository.OpeningRecentViewRepository; - -import java.util.List; - -@Service -public class OpeningRecentViewService { - - @Autowired - private OpeningRecentViewRepository openingRecentViewRepository; - - public PaginatedResult getOpeningsByIds(List openingIds) { - PaginationParameters pagination = new PaginationParameters(0, 10); - return openingRecentViewRepository.getUserRecentOpenings(openingIds, pagination); - } -} From 5c4358c088bbb4fef719f049935cfb6e72623998 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 30 Oct 2024 15:21:41 -0700 Subject: [PATCH 12/43] simplified the userRecentOpeningDto --- .../postgres/dto/UserRecentOpeningDto.java | 40 +++---------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java index 3ae813ce..4872db88 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java @@ -1,41 +1,11 @@ package ca.bc.gov.restapi.results.postgres.dto; +import lombok.Data; import java.time.LocalDateTime; +@Data public class UserRecentOpeningDto { - - private String userId; - private String openingId; - private LocalDateTime lastViewed; - - public UserRecentOpeningDto(String userId, String openingId, LocalDateTime lastViewed) { - this.userId = userId; - this.openingId = openingId; - this.lastViewed = lastViewed; - } - - // Getters and Setters - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getOpeningId() { - return openingId; - } - - public void setOpeningId(String openingId) { - this.openingId = openingId; - } - - public LocalDateTime getLastViewed() { - return lastViewed; - } - - public void setLastViewed(LocalDateTime lastViewed) { - this.lastViewed = lastViewed; - } + private final String userId; + private final String openingId; + private final LocalDateTime lastViewed; } From c55831d0de3c889afcedaf53ce1b4dc5ddc76198 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 30 Oct 2024 15:24:23 -0700 Subject: [PATCH 13/43] simplified the UserOpeningEntity class --- .../entity/UserRecentOpeningEntity.java | 37 ++----------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java index 2377812d..dd361f79 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java @@ -1,6 +1,5 @@ package ca.bc.gov.restapi.results.postgres.entity; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -8,9 +7,12 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AllArgsConstructor; +import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDateTime; + +@Data @NoArgsConstructor @AllArgsConstructor @Entity @@ -29,37 +31,4 @@ public class UserRecentOpeningEntity { @Column(name = "last_viewed", nullable = false) private LocalDateTime lastViewed; - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getUserId() { - return userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getOpeningId() { - return openingId; - } - - public void setOpeningId(String openingId) { - this.openingId = openingId; - } - - public LocalDateTime getLastViewed() { - return lastViewed; - } - - public void setLastViewed(LocalDateTime lastViewed) { - this.lastViewed = lastViewed; - } } From d6a8575c394104994e35daba612cd4b7cd6cafa5 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 30 Oct 2024 15:25:50 -0700 Subject: [PATCH 14/43] removed version file for flyway migrations --- .../resources/db/migration/V1__create_schema.sql | 16 ++++++++++++++++ .../V2__create_user_recent_openings_table.sql | 15 --------------- 2 files changed, 16 insertions(+), 15 deletions(-) delete mode 100644 backend/src/main/resources/db/migration/V2__create_user_recent_openings_table.sql diff --git a/backend/src/main/resources/db/migration/V1__create_schema.sql b/backend/src/main/resources/db/migration/V1__create_schema.sql index d40f9e76..d3e22f45 100644 --- a/backend/src/main/resources/db/migration/V1__create_schema.sql +++ b/backend/src/main/resources/db/migration/V1__create_schema.sql @@ -44,3 +44,19 @@ CREATE TABLE IF NOT EXISTS silva.oracle_extraction_logs ( CONSTRAINT oracle_extraction_logs_pk PRIMARY KEY(id) ); + +-- Create sequence if it doesn't exist +CREATE SEQUENCE IF NOT EXISTS silva.user_recent_openings_seq +START WITH 1 +INCREMENT BY 1 +MINVALUE 1 +NO MAXVALUE +CACHE 30; + +-- Use the sequence in your table creation or insert statements +CREATE TABLE IF NOT EXISTS silva.user_recent_openings ( + id BIGINT PRIMARY KEY DEFAULT nextval('silva.user_recent_openings_seq'), + opening_id VARCHAR(255) NOT NULL, + user_id VARCHAR(255) NOT NULL, + last_viewed TIMESTAMP DEFAULT NOW() +); diff --git a/backend/src/main/resources/db/migration/V2__create_user_recent_openings_table.sql b/backend/src/main/resources/db/migration/V2__create_user_recent_openings_table.sql deleted file mode 100644 index 3cea5b1a..00000000 --- a/backend/src/main/resources/db/migration/V2__create_user_recent_openings_table.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Create sequence if it doesn't exist -CREATE SEQUENCE IF NOT EXISTS silva.user_recent_openings_seq -START WITH 1 -INCREMENT BY 1 -MINVALUE 1 -NO MAXVALUE -CACHE 30; - --- Use the sequence in your table creation or insert statements -CREATE TABLE IF NOT EXISTS silva.user_recent_openings ( - id BIGINT PRIMARY KEY DEFAULT nextval('silva.user_recent_openings_seq'), - opening_id VARCHAR(255) NOT NULL, - user_id VARCHAR(255) NOT NULL, - last_viewed TIMESTAMP DEFAULT NOW() -); From 22f56b759e107db3229e29005713f912770f9769 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 30 Oct 2024 15:26:55 -0700 Subject: [PATCH 15/43] removing unwanted console logs --- .../Dashboard/Opening/RecentOpeningsDataTable/index.tsx | 1 - .../SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx index 772a6a9c..7cef2986 100644 --- a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx +++ b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx @@ -90,7 +90,6 @@ const RecentOpeningsDataTable: React.FC = ({ //Function to handle the favourite feature of the opening for a user const handleFavouriteOpening = (rowId: string) => { - console.log(rowId + " has been added as a favourite for the user") //make a call to the api for the favourite opening when ready setToastText(`Following "OpeningID ${rowId}"`); } diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx index f638870f..4d927a38 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx @@ -114,7 +114,6 @@ const SearchScreenDataTable: React.FC = ({ //Function to handle the favourite feature of the opening for a user const handleFavouriteOpening = (rowId: string) => { - console.log(rowId + " has been added as a favourite for the user") //make a call to the api for the favourite opening when ready setToastText(`Following "OpeningID ${rowId}"`); } From 6d5c5cf160fe3729b42ccc1f79b656616f234e37 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 30 Oct 2024 16:21:06 -0700 Subject: [PATCH 16/43] added the builder nad with from lombok --- .../results/postgres/dto/UserRecentOpeningDto.java | 8 ++++++++ .../results/postgres/entity/UserRecentOpeningEntity.java | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java index 4872db88..3592a5cb 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java @@ -1,9 +1,17 @@ package ca.bc.gov.restapi.results.postgres.dto; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.With; + import java.time.LocalDateTime; @Data +@AllArgsConstructor +@With +@Builder public class UserRecentOpeningDto { private final String userId; private final String openingId; diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java index dd361f79..733a8619 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserRecentOpeningEntity.java @@ -7,14 +7,18 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.With; import java.time.LocalDateTime; @Data @NoArgsConstructor @AllArgsConstructor +@With +@Builder @Entity @Table(schema = "silva", name = "user_recent_openings") public class UserRecentOpeningEntity { From 0004f72a66f35af6d904786c1fc5f64e08b04e0a Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 30 Oct 2024 17:48:05 -0700 Subject: [PATCH 17/43] added test file for UserRecentOpeningService --- .../service/UserRecentOpeningServiceTest.java | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java new file mode 100644 index 00000000..250817ae --- /dev/null +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java @@ -0,0 +1,150 @@ +package ca.bc.gov.restapi.results.postgres.service; + +import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; +import ca.bc.gov.restapi.results.common.pagination.PaginationParameters; +import ca.bc.gov.restapi.results.common.security.LoggedUserService; +import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchFiltersDto; +import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; +import ca.bc.gov.restapi.results.oracle.service.OpeningService; +import ca.bc.gov.restapi.results.postgres.dto.UserRecentOpeningDto; +import ca.bc.gov.restapi.results.postgres.entity.UserRecentOpeningEntity; +import ca.bc.gov.restapi.results.postgres.repository.UserRecentOpeningRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class UserRecentOpeningServiceTest { + + @Mock + private LoggedUserService loggedUserService; + + @Mock + private UserRecentOpeningRepository userRecentOpeningRepository; + + @Mock + private OpeningService openingService; + + @InjectMocks + private UserRecentOpeningService userRecentOpeningService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void storeViewedOpening_newOpening_savesEntity() { + String userId = "user123"; + String openingId = "123"; + LocalDateTime lastViewed = LocalDateTime.now(); + + when(loggedUserService.getLoggedUserId()).thenReturn(userId); + when(userRecentOpeningRepository.findByUserIdAndOpeningId(userId, openingId)).thenReturn(null); + + UserRecentOpeningDto result = userRecentOpeningService.storeViewedOpening(openingId); + + assertNotNull(result); + assertEquals(userId, result.getUserId()); + assertEquals(openingId, result.getOpeningId()); + + verify(userRecentOpeningRepository, times(1)).save(any(UserRecentOpeningEntity.class)); + } + + @Test + void storeViewedOpening_existingOpening_updatesEntity() { + String userId = "user123"; + String openingId = "123"; + LocalDateTime lastViewed = LocalDateTime.now(); + UserRecentOpeningEntity existingEntity = new UserRecentOpeningEntity(1L, userId, openingId, lastViewed.minusDays(1)); + + when(loggedUserService.getLoggedUserId()).thenReturn(userId); + when(userRecentOpeningRepository.findByUserIdAndOpeningId(userId, openingId)).thenReturn(existingEntity); + + UserRecentOpeningDto result = userRecentOpeningService.storeViewedOpening(openingId); + + assertNotNull(result); + assertEquals(userId, result.getUserId()); + assertEquals(openingId, result.getOpeningId()); + + verify(userRecentOpeningRepository, times(1)).save(existingEntity); + } + + @Test + void storeViewedOpening_invalidOpeningId_throwsException() { + String invalidOpeningId = "abc"; + + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + userRecentOpeningService.storeViewedOpening(invalidOpeningId); + }); + + assertEquals("Opening ID must contain numbers only!", exception.getMessage()); + } + + @Test + void getAllRecentOpeningsForUser_noRecentOpenings_returnsEmptyResult() { + String userId = "idir@jasgrewa"; + int limit = 10; + + // Arrange + when(loggedUserService.getLoggedUserId()).thenReturn(userId); + when(userRecentOpeningRepository.findByUserIdOrderByLastViewedDesc(eq(userId), any(PageRequest.class))) + .thenReturn(Page.empty()); // Mocking an empty page of recent openings + + // Act + PaginatedResult result = userRecentOpeningService.getAllRecentOpeningsForUser(limit); + + // Assert + assertNotNull(result); + + // Check if data is null and assert empty + assertTrue(result.getData() == null || result.getData().isEmpty(), "Data should be empty or null"); + } + + + @Test + void getAllRecentOpeningsForUser_withRecentOpenings_returnsSortedResult() { + String userId = "user123"; + int limit = 10; + LocalDateTime now = LocalDateTime.now(); + + UserRecentOpeningEntity opening1 = new UserRecentOpeningEntity(1L, userId, "123", now.minusDays(2)); + UserRecentOpeningEntity opening2 = new UserRecentOpeningEntity(2L, userId, "456", now.minusDays(1)); + + List openings = List.of(opening1, opening2); + when(loggedUserService.getLoggedUserId()).thenReturn(userId); + when(userRecentOpeningRepository.findByUserIdOrderByLastViewedDesc(eq(userId), any(PageRequest.class))) + .thenReturn(new PageImpl<>(openings)); + + OpeningSearchResponseDto dto1 = new OpeningSearchResponseDto(); + dto1.setOpeningId(123); + + OpeningSearchResponseDto dto2 = new OpeningSearchResponseDto(); + dto2.setOpeningId(456); + + PaginatedResult pageResult = new PaginatedResult<>(); + pageResult.setData(List.of(dto1, dto2)); + + when(openingService.openingSearch(any(OpeningSearchFiltersDto.class), any(PaginationParameters.class))) + .thenReturn(pageResult); + + PaginatedResult result = userRecentOpeningService.getAllRecentOpeningsForUser(limit); + + assertNotNull(result); + assertEquals(2, result.getData().size()); + assertEquals((long) 456L, (long) result.getData().get(0).getOpeningId()); // Most recent first + assertEquals((long) 123L, (long) result.getData().get(1).getOpeningId()); // Least recent last + } +} From 5c22fabed02ed3ff1a2215b29f413acf840803ff Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Sun, 3 Nov 2024 20:17:43 -0800 Subject: [PATCH 18/43] chore: changing param to null to avoid issues When a value is set, this parameter will be consumed internally by the query. To avoid this behavior, set this to null --- .../gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java index 487e59c8..964e2553 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java @@ -92,7 +92,7 @@ public OpeningSearchFiltersDto( this.statusList = new ArrayList<>(); this.openingIds = openingIds; this.myOpenings = null; - this.submittedToFrpa = false; + this.submittedToFrpa = null; this.disturbanceDateStart = null; this.disturbanceDateEnd = null; this.regenDelayDateStart = null; From fcdf23235d9548703eab43d9d2918b939d97c081 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Sun, 3 Nov 2024 20:17:59 -0800 Subject: [PATCH 19/43] chore: converting class to record --- .../postgres/dto/UserRecentOpeningDto.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java index 3592a5cb..92855357 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/UserRecentOpeningDto.java @@ -1,19 +1,15 @@ package ca.bc.gov.restapi.results.postgres.dto; -import lombok.AllArgsConstructor; +import java.time.LocalDateTime; import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; import lombok.With; -import java.time.LocalDateTime; - -@Data -@AllArgsConstructor @With @Builder -public class UserRecentOpeningDto { - private final String userId; - private final String openingId; - private final LocalDateTime lastViewed; +public record UserRecentOpeningDto( + String userId, + String openingId, + LocalDateTime lastViewed +) { + } From 7497113a75fdb1f14fec8277e4507c5130454ba7 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Sun, 3 Nov 2024 20:19:48 -0800 Subject: [PATCH 20/43] chore: reformatted recent opening endpoint - Changed the URL to add the recent opening below the opening domain. - Changed the annotations to make it easier to navigate - changed the opening add endpoint to put - changed the opening add endpoint to void with accepted status --- .../endpoint/UserRecentOpeningEndpoint.java | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java index 99a7f92f..c6b1e872 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserRecentOpeningEndpoint.java @@ -2,48 +2,50 @@ import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; -import ca.bc.gov.restapi.results.postgres.dto.UserRecentOpeningDto; import ca.bc.gov.restapi.results.postgres.service.UserRecentOpeningService; import lombok.RequiredArgsConstructor; - -import java.util.List; - +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor +@RequestMapping("/api/openings/recent") public class UserRecentOpeningEndpoint { - private final UserRecentOpeningService userRecentOpeningService; + private final UserRecentOpeningService userRecentOpeningService; + + /** + * Retrieves a list of recent openings viewed by the user, limited by the number of results. + * + * @param limit The maximum number of results to return. + * @return A list of opening IDs viewed by the user. + */ + @GetMapping + public ResponseEntity> getUserRecentOpenings( + @RequestParam(defaultValue = "10") int limit) { + // Fetch recent openings for the logged-in user with the specified limit + return ResponseEntity.ok(userRecentOpeningService.getAllRecentOpeningsForUser(limit)); + } - /** - * Records the opening viewed by the user based on the provided opening ID. - * - * @param openingId The ID of the opening viewed by the user. - * @return A simple confirmation message or the HTTP code 204-No Content. - */ - @PostMapping("api/users/recent/{openingId}") - public ResponseEntity recordUserViewedOpening(@PathVariable String openingId) { - // Store the opening and return the DTO - UserRecentOpeningDto recentOpeningDto = userRecentOpeningService.storeViewedOpening(openingId); - return ResponseEntity.ok(recentOpeningDto); - } + /** + * Records the opening viewed by the user based on the provided opening ID. + * + * @param openingId The ID of the opening viewed by the user. + * @return A simple confirmation message or the HTTP code 204-No Content. + */ + @PutMapping("/{openingId}") + @ResponseStatus(HttpStatus.ACCEPTED) + public void recordUserViewedOpening( + @PathVariable String openingId) { + // Store the opening and return the DTO + userRecentOpeningService.storeViewedOpening(openingId); + } - /** - * Retrieves a list of recent openings viewed by the user, limited by the number of results. - * - * @param limit The maximum number of results to return. - * @return A list of opening IDs viewed by the user. - */ - @GetMapping("api/user/recent-openings") - public ResponseEntity> getUserRecentOpenings(@RequestParam(defaultValue = "10") int limit) { - // Fetch recent openings for the logged-in user with the specified limit - PaginatedResult recentOpenings = userRecentOpeningService.getAllRecentOpeningsForUser(limit); - return ResponseEntity.ok(recentOpenings); - } } \ No newline at end of file From 9e4c8a1b9a223e580f2fb82e24ba60e92844febf Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Sun, 3 Nov 2024 20:22:23 -0800 Subject: [PATCH 21/43] test: fixing test param --- .../postgres/service/UserRecentOpeningServiceTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java index 250817ae..ef0d56a1 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserRecentOpeningServiceTest.java @@ -57,8 +57,8 @@ void storeViewedOpening_newOpening_savesEntity() { UserRecentOpeningDto result = userRecentOpeningService.storeViewedOpening(openingId); assertNotNull(result); - assertEquals(userId, result.getUserId()); - assertEquals(openingId, result.getOpeningId()); + assertEquals(userId, result.userId()); + assertEquals(openingId, result.openingId()); verify(userRecentOpeningRepository, times(1)).save(any(UserRecentOpeningEntity.class)); } @@ -76,8 +76,8 @@ void storeViewedOpening_existingOpening_updatesEntity() { UserRecentOpeningDto result = userRecentOpeningService.storeViewedOpening(openingId); assertNotNull(result); - assertEquals(userId, result.getUserId()); - assertEquals(openingId, result.getOpeningId()); + assertEquals(userId, result.userId()); + assertEquals(openingId, result.openingId()); verify(userRecentOpeningRepository, times(1)).save(existingEntity); } From 9700a70ec7224829c0193400d3f21410386e0eec Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Sun, 3 Nov 2024 20:35:51 -0800 Subject: [PATCH 22/43] chore: changed frontend definitions Changed to match changes on the backend --- .../Openings/SearchScreenDataTable/index.tsx | 4 ++-- .../services/queries/dashboard/dashboardQueries.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx index 4d927a38..961eb266 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx @@ -42,7 +42,7 @@ import { } from "../../../../utils/fileConversions"; import { Tooltip } from "@carbon/react"; import { useNavigate } from "react-router-dom"; -import { usePostViewedOpening } from "../../../../services/queries/dashboard/dashboardQueries"; +import { usePutViewedOpening } from "../../../../services/queries/dashboard/dashboardQueries"; interface ISearchScreenDataTable { rows: OpeningsSearch[]; @@ -78,7 +78,7 @@ const SearchScreenDataTable: React.FC = ({ const [selectedRows, setSelectedRows] = useState([]); // State to store selected rows const [toastText, setToastText] = useState(null); const [openingDetails, setOpeningDetails] = useState(false); - const { mutate: markAsViewedOpening, isError, error } = usePostViewedOpening(); + const { mutate: markAsViewedOpening, isError, error } = usePutViewedOpening(); const navigate = useNavigate(); useEffect(() => { diff --git a/frontend/src/services/queries/dashboard/dashboardQueries.ts b/frontend/src/services/queries/dashboard/dashboardQueries.ts index 9a3d2dfb..9e414962 100644 --- a/frontend/src/services/queries/dashboard/dashboardQueries.ts +++ b/frontend/src/services/queries/dashboard/dashboardQueries.ts @@ -8,17 +8,17 @@ import { env } from "../../../env"; const backendUrl = env.VITE_BACKEND_URL; -// Function to send the POST request -export const postViewedOpening = async (openingId: string): Promise => { +// Function to send the PUT request +export const putViewedOpening = async (openingId: string): Promise => { const authToken = getAuthIdToken(); try { - const response = await axios.post(`${backendUrl}/api/users/recent/${openingId}`, null, { + const response = await axios.put(`${backendUrl}/api/openings/recent/${openingId}`, null, { headers: { Authorization: `Bearer ${authToken}` } }); return response.data; - } catch (error:any) { + } catch (error: any) { if (error.response?.status === 403) { throw new Error("Forbidden: You don't have permission to view this opening."); } else { @@ -28,9 +28,9 @@ export const postViewedOpening = async (openingId: string): Promise => { }; // Hook for using the mutation - export const usePostViewedOpening = () => { + export const usePutViewedOpening = () => { return useMutation({ - mutationFn: (openingId: string) => postViewedOpening(openingId) + mutationFn: (openingId: string) => putViewedOpening(openingId) }); }; From bb589fea3f025735339a4a09e70f564d0e27f29b Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 5 Nov 2024 11:57:31 -0800 Subject: [PATCH 23/43] removed the unused recent-opening endpoint by ricardo --- .../oracle/endpoint/OpeningEndpoint.java | 29 ----------- .../oracle/service/OpeningService.java | 51 +------------------ .../oracle/endpoint/OpeningEndpointTest.java | 1 - .../oracle/service/OpeningServiceTest.java | 47 ----------------- frontend/src/App.tsx | 4 -- frontend/src/components/OpeningsTab/index.tsx | 31 +---------- frontend/src/services/OpeningService.ts | 44 ---------------- frontend/src/services/search/openings.ts | 2 +- 8 files changed, 4 insertions(+), 205 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpoint.java index ed8e804f..0da1ef34 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpoint.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpoint.java @@ -1,17 +1,7 @@ package ca.bc.gov.restapi.results.oracle.endpoint; - -import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; -import ca.bc.gov.restapi.results.common.pagination.PaginatedViaQuery; -import ca.bc.gov.restapi.results.common.pagination.PaginationParameters; -import ca.bc.gov.restapi.results.oracle.dto.RecentOpeningDto; import ca.bc.gov.restapi.results.oracle.service.OpeningService; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -22,23 +12,4 @@ public class OpeningEndpoint { private final OpeningService openingService; - - /** - * Fetches all recent openings for the home screen. - * - * @param paginationParameters {@link PaginationParameters} parameters - * @return List of {@link RecentOpeningDto} or empty list. - */ - @GetMapping(path = "/recent-openings", produces = MediaType.APPLICATION_JSON_VALUE) - @PaginatedViaQuery - @CrossOrigin(exposedHeaders = "x-opening-source") - public ResponseEntity> getRecentOpenings( - @Valid PaginationParameters paginationParameters) { - PaginatedResult userOpenings = - openingService.getRecentOpeningsCurrentUser(paginationParameters); - - HttpHeaders headers = new HttpHeaders(); - headers.set("x-opening-source", "user-based"); - return ResponseEntity.ok().headers(headers).body(userOpenings); - } } diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningService.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningService.java index 967535cc..e92be6e4 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningService.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/service/OpeningService.java @@ -50,56 +50,7 @@ public class OpeningService { private final ForestClientApiProvider forestClientApiProvider; - /** - * Gets all recent openings for the Home Screen. - * - * @param pagination A {@link PaginationParameters} with pagination settings. - * @return {@link List} of {@link RecentOpeningDto} containing all recent openings for that user. - */ - public PaginatedResult getRecentOpeningsCurrentUser( - PaginationParameters pagination) { - log.info( - "Getting recent openings to logged user with page index {} and page size {}", - pagination.page(), - pagination.perPage()); - - if (pagination.perPage() > SilvaConstants.MAX_PAGE_SIZE) { - throw new MaxPageSizeException(SilvaConstants.MAX_PAGE_SIZE); - } - - String entryUserId = loggedUserService.getLoggedUserId(); - - // Openings - Pageable pageable = - PageRequest.of( - pagination.page(), pagination.perPage(), Sort.by("updateTimestamp").descending()); - Page openingPage = openingRepository.findAllByEntryUserId(entryUserId, pageable); - - PaginatedResult paginatedResult = new PaginatedResult<>(); - paginatedResult.setPageIndex(pagination.page()); - paginatedResult.setPerPage(pagination.perPage()); - - if (openingPage.getContent().isEmpty()) { - log.info("No recent openings for this user given page index and size!"); - paginatedResult.setData(List.of()); - paginatedResult.setTotalPages(0); - paginatedResult.setHasNextPage(false); - return paginatedResult; - } - - // Cut Block Open Admin - List openingIds = openingPage.getContent().stream().map(OpeningEntity::getId).toList(); - List cutBlocks = - cutBlockOpenAdminService.findAllByOpeningIdIn(openingIds); - - List list = createDtoFromEntity(openingPage.getContent(), cutBlocks); - paginatedResult.setData(list); - paginatedResult.setTotalPages(openingPage.getTotalPages()); - paginatedResult.setHasNextPage(openingPage.hasNext()); - - return paginatedResult; - } - + /** * Get recent openings given the opening creation date. * diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpointTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpointTest.java index 6a57c076..f0e4b06a 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpointTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpointTest.java @@ -64,7 +64,6 @@ void getRecentOpenings_fetchPaginated_shouldSucceed() throws Exception { PaginationParameters params = new PaginationParameters(0, 5); - when(openingService.getRecentOpeningsCurrentUser(params)).thenReturn(paginatedResult); mockMvc .perform( diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java index 33dbe06f..e2db4921 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java @@ -42,53 +42,6 @@ class OpeningServiceTest extends AbstractTestContainerIntegrationTest { @Autowired private OpeningService openingService; - @Test - @DisplayName("Get a list of recent openings for logged user") - void getRecentOpenings_fetchPaginated_shouldSucceed() { - int pages = 3; - int currentPage = 0; - - PaginationParameters pagination = new PaginationParameters(currentPage, pages); - PaginatedResult paginatedResult = - openingService.getRecentOpeningsCurrentUser(pagination); - - Assertions.assertNotNull(paginatedResult); - Assertions.assertEquals(currentPage, paginatedResult.getPageIndex()); - Assertions.assertEquals(1, paginatedResult.getTotalPages()); - Assertions.assertFalse(paginatedResult.getData().isEmpty()); - Assertions.assertEquals(1, paginatedResult.getData().size()); - } - - @Test - @DisplayName("Get an empty list of recent openings for the home screen") - void getRecentOpenings_emptyPages_shouldSucceed() { - - int currentPage = 0; - int pages = 1; - PaginationParameters pagination = new PaginationParameters(currentPage, pages); - PaginatedResult paginatedResult = - openingService.getRecentOpeningsCurrentUser(pagination); - - Assertions.assertNotNull(paginatedResult); - Assertions.assertEquals(currentPage, paginatedResult.getPageIndex()); - Assertions.assertEquals(1, paginatedResult.getTotalPages()); - Assertions.assertFalse(paginatedResult.getData().isEmpty()); - } - - @Test - @DisplayName("Get a list of recent openings without user") - void getRecentOpenings_fetchNoUserPaginated_shouldSucceed() { - - PaginatedResult paginatedResult = - openingService.getRecentOpenings(new PaginationParameters(1, 1)); - - Assertions.assertNotNull(paginatedResult); - Assertions.assertEquals(1, paginatedResult.getPageIndex()); - Assertions.assertEquals(3, paginatedResult.getTotalPages()); - Assertions.assertFalse(paginatedResult.getData().isEmpty()); - Assertions.assertEquals(1, paginatedResult.getData().size()); - } - @Test @DisplayName("Opening search file id happy path should succeed") void openingSearch_fileId_shouldSucceed() { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 33e5164f..1895b049 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -34,10 +34,6 @@ const router = createBrowserRouter([ path: "/silviculture-search", element: } /> }, - { - path: "/opening/reports", - element: } /> - }, { path: "/help", element: } /> diff --git a/frontend/src/components/OpeningsTab/index.tsx b/frontend/src/components/OpeningsTab/index.tsx index 3aa9ea34..11579350 100644 --- a/frontend/src/components/OpeningsTab/index.tsx +++ b/frontend/src/components/OpeningsTab/index.tsx @@ -5,7 +5,6 @@ import { Location } from '@carbon/icons-react'; import OpeningsMap from '../OpeningsMap'; import OpeningScreenDataTable from '../OpeningScreenDataTable/index'; import { columns } from '../Dashboard/Opening/RecentOpeningsDataTable/testData'; -import { fetchRecentOpenings } from '../../services/OpeningService'; import SectionTitle from '../SectionTitle'; import TableSkeleton from '../TableSkeleton'; import { InlineNotification } from '@carbon/react'; @@ -24,7 +23,6 @@ interface Props { } const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { - const [loading, setLoading] = useState(true); const [openingRows, setOpeningRows] = useState([]); const [error, setError] = useState(null); const [loadId, setLoadId] = useState(null); @@ -34,32 +32,7 @@ const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { const { data, isFetching } = useUserRecentOpeningQuery(10); const [headers, setHeaders] = useState(columns); - useEffect(() => { - const fetchData = async () => { - try { - const rows: RecentOpening[] = await fetchRecentOpenings(); - setOpeningRows(rows); - setLoading(false); - setError(null); - } catch (error) { - console.error('Error fetching recent openings:', error); - setLoading(false); - setError('Failed to fetch recent openings'); - } - }; - - const fetchAllowedPeople = async () => { - try { - const usersList: WmsLayersWhitelistUser[] = await getWmsLayersWhitelistUsers(); - setWmsUsersWhitelist(usersList); - } catch (error) { - console.error('Error fetching recent openings:', error); - } - }; - - fetchData(); - fetchAllowedPeople(); - }, []); + useEffect(() => {}, [loadId, openingPolygonNotFound, wmsUsersWhitelist]); @@ -143,7 +116,7 @@ const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { className = "inline-notification" /> ) : null } - {loading ? ( + {isFetching ? ( ) : ( } Array of objects found - */ -export async function fetchRecentOpenings(): Promise { - const authToken = getAuthIdToken(); - try { - const response = await axios.get(backendUrl.concat("/api/openings/recent-openings?page=0&perPage=100"), { - headers: { - Authorization: `Bearer ${authToken}` - } - }); - - if (response.status >= 200 && response.status < 300) { - const { data } = response; - - if (data.data) { - // Extracting row information from the fetched data - const rows: RecentOpening[] = data.data.map((opening: RecentOpeningApi) => ({ - id: opening.openingId.toString(), - openingId: opening.openingId.toString(), - forestFileId: opening.forestFileId ? opening.forestFileId : '-', - cuttingPermit: opening.cuttingPermit ? opening.cuttingPermit : '-', - timberMark: opening.timberMark ? opening.timberMark : '-', - cutBlock: opening.cutBlock ? opening.cutBlock : '-', - grossAreaHa: opening.grossAreaHa ? opening.grossAreaHa.toString() : '-', - status: opening.status && opening.status.description? opening.status.description : '-', - category: opening.category && opening.category.description? opening.category.description : '-', - disturbanceStart: opening.disturbanceStart ? opening.disturbanceStart : '-', - entryTimestamp: opening.entryTimestamp ? opening.entryTimestamp.split('T')[0] : '-', - updateTimestamp: opening.updateTimestamp ? opening.updateTimestamp.split('T')[0] : '-' - })); - - return rows; - } - } - return []; - } catch (error) { - console.error('Error fetching recent openings:', error); - throw error; - } -} - /** * Fetch openings per year data from backend. * diff --git a/frontend/src/services/search/openings.ts b/frontend/src/services/search/openings.ts index 58fe71a3..2b90bd1b 100644 --- a/frontend/src/services/search/openings.ts +++ b/frontend/src/services/search/openings.ts @@ -125,7 +125,7 @@ export const fetchUserRecentOpenings = async (limit: number): Promise => { const authToken = getAuthIdToken(); // Make the API request with the Authorization header - const response = await axios.get(`${backendUrl}/api/user/recent-openings`, { + const response = await axios.get(`${backendUrl}/api/openings/recent`, { headers: { Authorization: `Bearer ${authToken}` } From b540db633c06ed589d466e19230a8d228cd585a0 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 5 Nov 2024 12:03:47 -0800 Subject: [PATCH 24/43] resolved comments by Paulo --- .../RecentOpeningsDataTable/testData.ts | 231 +----------------- .../Openings/SearchScreenDataTable/index.tsx | 2 - .../SearchScreenDataTable/testData.ts | 229 ----------------- 3 files changed, 1 insertion(+), 461 deletions(-) diff --git a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts index a3e5d814..4985d07e 100644 --- a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts +++ b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts @@ -52,233 +52,4 @@ export const columns: ITableHeader[] = [ header: 'Actions', selected: true } -]; - - -export const rows:any = [ - { - id: '114207', - openingId: '114207', - fileId: 'TFL47', - cuttingPermit: '12S', - timberMark: '47/12S', - cutBlock: '12-69', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-10-27', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-27' - }, - { - id: '114206', - openingId: '114206', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-69', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-09-04', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-27' - }, - { - id: '114205', - openingId: '114205', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-09-04', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-27' - }, - { - id: '114204', - openingId: '114204', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-01-16', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-26' - }, - { - id: '114203', - openingId: '114203', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-12-08', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-26' - }, - { - id: '114202', - openingId: '114202', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-11-15', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-25' - }, - { - id: '114201', - openingId: '114201', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-11-15', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-25' - }, - { - id: '114200', - openingId: '114200', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-10-20', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-24' - }, - { - id: '114199', - openingId: '114199', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-10-20', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-24' - }, - { - id: '114198', - openingId: '114198', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-09-12', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-23' - }, - { - id: '114197', - openingId: '114197', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-09-12', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-23' - }, - { - id: '114196', - openingId: '114196', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-08-05', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-22' - }, - { - id: '114195', - openingId: '114195', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-08-05', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-22' - }, - { - id: '114194', - openingId: '114194', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-07-10', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-21' - }, - { - id: '114193', - openingId: '114193', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-07-10', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-21' - } -]; +]; \ No newline at end of file diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx index 961eb266..51ecd176 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx @@ -103,11 +103,9 @@ const SearchScreenDataTable: React.FC = ({ markAsViewedOpening(openingId, { onSuccess: () => { // setToastText(`Successfully marked opening ${openingId} as viewed.`); - console.log(`Successfully marked opening ${openingId} as viewed.`); }, onError: (err: any) => { // setToastText(`Failed to mark as viewed: ${err.message}`); - console.log(`Failed to mark as viewed: ${err.message}`); } }); }; diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts index f8c6d7ba..35c29760 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts @@ -52,232 +52,3 @@ export const columns: ITableHeader[] = [ selected: false } ]; - - -export const rows:any = [ - { - id: '114207', - openingId: '114207', - fileId: 'TFL47', - cuttingPermit: '12S', - timberMark: '47/12S', - cutBlock: '12-69', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-10-27', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-27' - }, - { - id: '114206', - openingId: '114206', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-69', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-09-04', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-27' - }, - { - id: '114205', - openingId: '114205', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-09-04', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-27' - }, - { - id: '114204', - openingId: '114204', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-01-16', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-26' - }, - { - id: '114203', - openingId: '114203', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-12-08', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-26' - }, - { - id: '114202', - openingId: '114202', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-11-15', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-25' - }, - { - id: '114201', - openingId: '114201', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-11-15', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-25' - }, - { - id: '114200', - openingId: '114200', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-10-20', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-24' - }, - { - id: '114199', - openingId: '114199', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-10-20', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-24' - }, - { - id: '114198', - openingId: '114198', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-09-12', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-23' - }, - { - id: '114197', - openingId: '114197', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-09-12', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-23' - }, - { - id: '114196', - openingId: '114196', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-08-05', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-22' - }, - { - id: '114195', - openingId: '114195', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-08-05', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-22' - }, - { - id: '114194', - openingId: '114194', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-07-10', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-21' - }, - { - id: '114193', - openingId: '114193', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-07-10', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-21' - } -]; From 7b27b18e40d6d16776f9780ccb0b16e5c963423c Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 5 Nov 2024 15:03:47 -0800 Subject: [PATCH 25/43] added test for the recentOpeningData table --- .../oracle/endpoint/OpeningEndpointTest.java | 96 --------------- .../Opening/RecentOpeningsDataTable.test.tsx | 111 ++++++++++++++++++ frontend/src/__test__/utils/DateUtils.test.ts | 38 ++++++ frontend/src/utils/DateUtils.ts | 8 -- 4 files changed, 149 insertions(+), 104 deletions(-) delete mode 100644 backend/src/test/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpointTest.java create mode 100644 frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx create mode 100644 frontend/src/__test__/utils/DateUtils.test.ts diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpointTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpointTest.java deleted file mode 100644 index f0e4b06a..00000000 --- a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningEndpointTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package ca.bc.gov.restapi.results.oracle.endpoint; - -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import ca.bc.gov.restapi.results.common.pagination.PaginatedResult; -import ca.bc.gov.restapi.results.common.pagination.PaginationParameters; -import ca.bc.gov.restapi.results.oracle.dto.RecentOpeningDto; -import ca.bc.gov.restapi.results.oracle.enums.OpeningCategoryEnum; -import ca.bc.gov.restapi.results.oracle.enums.OpeningStatusEnum; -import ca.bc.gov.restapi.results.oracle.service.OpeningService; -import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; - -@WebMvcTest(OpeningEndpoint.class) -@WithMockUser(roles = "user_read") -class OpeningEndpointTest { - - @Autowired private MockMvc mockMvc; - - @MockBean private OpeningService openingService; - - @Test - @DisplayName("Request a list of recent openings for the home screen") - void getRecentOpenings_fetchPaginated_shouldSucceed() throws Exception { - PaginatedResult paginatedResult = new PaginatedResult<>(); - paginatedResult.setPageIndex(0); - paginatedResult.setPerPage(5); - paginatedResult.setTotalPages(1); - paginatedResult.setHasNextPage(false); - - LocalDate now = LocalDate.now(); - LocalDateTime entryLastYear = LocalDateTime.now().minusYears(1L); - LocalDateTime updateLastMonth = entryLastYear.plusMonths(11L); - - RecentOpeningDto recentOpeningDto = - new RecentOpeningDto( - 114207L, - "TFL47", - "12T", - "47/12S", - "12-69", - new BigDecimal("12.9"), - OpeningStatusEnum.APP, - OpeningCategoryEnum.FTML, - now, - entryLastYear, - updateLastMonth); - paginatedResult.setData(List.of(recentOpeningDto)); - - PaginationParameters params = new PaginationParameters(0, 5); - - - mockMvc - .perform( - get("/api/openings/recent-openings") - .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(content().contentType("application/json")) - .andExpect(header().exists("x-opening-source")) - .andExpect(jsonPath("$.pageIndex").value("0")) - .andExpect(jsonPath("$.perPage").value("5")) - .andExpect(jsonPath("$.totalPages").value("1")) - .andExpect(jsonPath("$.hasNextPage").value("false")) - .andExpect(jsonPath("$.data[0].openingId").value("114207")) - .andExpect(jsonPath("$.data[0].fileId").value("TFL47")) - .andExpect(jsonPath("$.data[0].cuttingPermit").value("12T")) - .andExpect(jsonPath("$.data[0].timberMark").value("47/12S")) - .andExpect(jsonPath("$.data[0].cutBlock").value("12-69")) - .andExpect(jsonPath("$.data[0].grossAreaHa").value("12.9")) - .andExpect(jsonPath("$.data[0].status.code").value(OpeningStatusEnum.APP.getCode())) - .andExpect( - jsonPath("$.data[0].status.description").value(OpeningStatusEnum.APP.getDescription())) - .andExpect(jsonPath("$.data[0].category.code").value(OpeningCategoryEnum.FTML.getCode())) - .andExpect( - jsonPath("$.data[0].category.description") - .value(OpeningCategoryEnum.FTML.getDescription())) - .andExpect(jsonPath("$.data[0].disturbanceStart").value(now.toString())) - .andReturn(); - } -} diff --git a/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx b/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx new file mode 100644 index 00000000..d4b10571 --- /dev/null +++ b/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx @@ -0,0 +1,111 @@ +// src/__test__/components/SilvicultureSearch/Openings/OpeningsSearchBar.test.tsx + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import OpeningsSearchBar from "../../../../components/SilvicultureSearch/Openings/OpeningsSearchBar"; +import { vi } from "vitest"; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { OpeningsSearchProvider } from "../../../../contexts/search/OpeningsSearch"; +import { Pagination } from "@carbon/react"; +import {Provider } from "react-redux"; +import PaginationProvider from "../../../../contexts/PaginationProvider"; +import RecentOpeningsDataTable from "../../../../components/Dashboard/Opening/RecentOpeningsDataTable"; +import { MemoryRouter } from "react-router-dom"; + +describe("OpeningsSearchBar", () => { + // Create a new QueryClient instance for each test + const queryClient = new QueryClient(); + const handleCheckboxChange = vi.fn() + const setLoadId = vi.fn() + const toggleSpatial = vi.fn() + const showSpatial = false + const data = { data: [], perPage: 0, totalPages: 0 } + const headers = [] + + it("renders the blank table when data array is empty", () => { + render( + + + + + + + + ); + // Check if the search input field is present with the correct placeholder text + const searchInput = screen.getByText(/Total Search Results: 0/i); + expect(searchInput).toBeInTheDocument(); + }); + + it("renders the table with data", () => { + const data = { data: [{ + "openingId": 114203, + "forestFileId": "TFL47", + "categoryCode": "FTML", + "categoryDescription": null, + "statusCode": "Active", + "statusDescription": "Active", + "cuttingPermitId": "12T", + "cutBlockId": "12-44A", + "orgUnitName": "DCC - Cariboo chilcotin natural resources", + "updateTimestamp": "2021-12-08" + }], perPage: 1, totalPages: 1 } + const headers = [{ + key: 'openingId', + header: 'Opening Id', + selected: false + }, + { + key: 'forestFileId', + header: 'File Id', + selected: false + }, + { + key: 'cuttingPermit', + header: 'Cutting permit', + selected: false + }, + { + key: 'timberMark', + header: 'Timber mark', + selected: false + }, + { + key: 'cutBlock', + header: 'Cut block', + selected: false + },] + render( + + + + + + + + ); + console.log(screen.debug()) + // Check if the search input field is present with the correct placeholder text + const searchInput = screen.getByText(/Total Search Results: 1/i); + expect(searchInput).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/utils/DateUtils.test.ts b/frontend/src/__test__/utils/DateUtils.test.ts new file mode 100644 index 00000000..f5e8acd3 --- /dev/null +++ b/frontend/src/__test__/utils/DateUtils.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest'; +import { formatDate, dateStringToISO } from '../../utils/DateUtils'; + +describe('formatDate', () => { + it('should format a valid date string to "Month Day, Year"', () => { + const result = formatDate('2023-11-05'); + expect(result).toBe('November 5, 2023'); + }); + + it('should return "--" for an empty string', () => { + const result = formatDate(''); + expect(result).toBe('--'); + }); + + it('should return "--" for a null value', () => { + // @ts-ignore + const result = formatDate(null); + expect(result).toBe('--'); + }); +}); + +describe('dateStringToISO', () => { + it('should convert a date string to ISO format', () => { + const result = dateStringToISO('2023-11-05'); + expect(result).toBe('2023-11-05T00:00:00.000Z'); + }); + + it('should return an empty string for an invalid date', () => { + const result = dateStringToISO(''); + expect(result).toBe(''); + }); + + it('should return an empty string for a null value', () => { + // @ts-ignore + const result = dateStringToISO(null); + expect(result).toBe(''); + }); +}); diff --git a/frontend/src/utils/DateUtils.ts b/frontend/src/utils/DateUtils.ts index 44b66e3d..a5b416ce 100644 --- a/frontend/src/utils/DateUtils.ts +++ b/frontend/src/utils/DateUtils.ts @@ -12,11 +12,3 @@ export const dateStringToISO = (date: string): string => { } return ''; }; - -export const formatDateToString = (dateToFormat: Date) => { - if (!dateToFormat) return null; - const year = dateToFormat.getFullYear(); - const month = String(dateToFormat.getMonth() + 1).padStart(2, "0"); - const day = String(dateToFormat.getDate()).padStart(2, "0"); - return `${year}-${month}-${day}`; -}; From c6cb9ca07ec07f21fb7e5c6874afc4f84ed1b68d Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 5 Nov 2024 15:13:55 -0800 Subject: [PATCH 26/43] adding the tests from the multiSelect branch --- .../Opening/RecentOpeningsDataTable.test.tsx | 4 -- .../Openings/AdvancedSearchDropdown.test.tsx | 68 +++++++++++++++++++ .../Openings/OpeningSearchBar.test.tsx | 56 +++++++++++++++ 3 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 frontend/src/__test__/components/SilvicultureSearch/Openings/AdvancedSearchDropdown.test.tsx create mode 100644 frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx diff --git a/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx b/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx index d4b10571..d242634a 100644 --- a/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx +++ b/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx @@ -3,12 +3,8 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; -import OpeningsSearchBar from "../../../../components/SilvicultureSearch/Openings/OpeningsSearchBar"; import { vi } from "vitest"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { OpeningsSearchProvider } from "../../../../contexts/search/OpeningsSearch"; -import { Pagination } from "@carbon/react"; -import {Provider } from "react-redux"; import PaginationProvider from "../../../../contexts/PaginationProvider"; import RecentOpeningsDataTable from "../../../../components/Dashboard/Opening/RecentOpeningsDataTable"; import { MemoryRouter } from "react-router-dom"; diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/AdvancedSearchDropdown.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/AdvancedSearchDropdown.test.tsx new file mode 100644 index 00000000..994b8faf --- /dev/null +++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/AdvancedSearchDropdown.test.tsx @@ -0,0 +1,68 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import "@testing-library/jest-dom"; +import AdvancedSearchDropdown from "../../../../components/SilvicultureSearch/Openings/AdvancedSearchDropdown"; +import { useOpeningFiltersQuery } from "../../../../services/queries/search/openingQueries"; +import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; +import React from "react"; + +// Mocking the toggleShowFilters function +const toggleShowFilters = vi.fn(); + +// Mocking useOpeningFiltersQuery to return mock data for filters +vi.mock("../../../../services/queries/search/openingQueries", () => ({ + useOpeningFiltersQuery: vi.fn(), +})); + +// Mocking useOpeningsSearch to return the necessary functions and state +vi.mock("../../../../contexts/search/OpeningsSearch", () => ({ + useOpeningsSearch: vi.fn(), +})); + +describe("AdvancedSearchDropdown", () => { + beforeEach(() => { + // Mock data to return for the filters query + (useOpeningFiltersQuery as jest.Mock).mockReturnValue({ + data: { + categories: ["FTML", "CONT"], + orgUnits: ["DCK", "DCR"], + dateTypes: ["Disturbance", "Free Growing"], + }, + isLoading: false, + isError: false, + }); + + // Mock implementation of useOpeningsSearch context + (useOpeningsSearch as jest.Mock).mockReturnValue({ + filters: { + openingFilters: [], + orgUnit: [], + category: [], + clientAcronym: "", + clientLocationCode: "", + cutBlock: "", + cuttingPermit: "", + timberMark: "", + dateType: "", + startDate: null, + endDate: null, + status: [], + }, + setFilters: vi.fn(), + clearFilters: vi.fn(), + }); + }); + + it("displays an error message if there is an error", () => { + (useOpeningFiltersQuery as jest.Mock).mockReturnValue({ + isLoading: false, + isError: true, + data: null, + }); + + render(); + expect( + screen.getByText("There was an error while loading the advanced filters.") + ).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx new file mode 100644 index 00000000..3f99fe56 --- /dev/null +++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx @@ -0,0 +1,56 @@ +// src/__test__/components/SilvicultureSearch/Openings/OpeningsSearchBar.test.tsx + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import OpeningsSearchBar from "../../../../components/SilvicultureSearch/Openings/OpeningsSearchBar"; +import { vi } from "vitest"; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; + +// Mock the useOpeningsSearch context to avoid rendering errors +vi.mock("../../../../contexts/search/OpeningsSearch", () => ({ + useOpeningsSearch: vi.fn().mockReturnValue({ + filters: [], + clearFilters: vi.fn(), + searchTerm: "", + setSearchTerm: vi.fn(), + }), +})); + +describe("OpeningsSearchBar", () => { + // Create a new QueryClient instance for each test + const queryClient = new QueryClient(); + + it("renders the search input with the correct placeholder", () => { + render( + + {}} /> + + ); + + // Check if the search input field is present with the correct placeholder text + const searchInput = screen.getByPlaceholderText( + "Search by opening ID, opening number, timber mark or file ID" + ); + expect(searchInput).toBeInTheDocument(); + }); + + it("should call the onSearchClick function when the search button is clicked", () => { + // Create a mock function to pass as a prop + const onSearchClick = vi.fn(); + + render( + + + + ); + + // Click the search button + const searchButton = screen.getAllByRole("button", { name: "Search" })[1]; + searchButton.click(); + + // Check if the onSearchClick function was called + expect(onSearchClick).toHaveBeenCalled(); + }); +}); \ No newline at end of file From e7d29b451e5206b082cd4b25bcbc68331d4d8290 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 5 Nov 2024 16:18:27 -0800 Subject: [PATCH 27/43] added test for the OpeningsTab --- .../__test__/components/OpeningsTab.test.tsx | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 frontend/src/__test__/components/OpeningsTab.test.tsx diff --git a/frontend/src/__test__/components/OpeningsTab.test.tsx b/frontend/src/__test__/components/OpeningsTab.test.tsx new file mode 100644 index 00000000..c81f3901 --- /dev/null +++ b/frontend/src/__test__/components/OpeningsTab.test.tsx @@ -0,0 +1,50 @@ +// src/__test__/components/SilvicultureSearch/Openings/OpeningsSearchBar.test.tsx + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { vi } from "vitest"; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import OpeningsTab from "../../../src/components/OpeningsTab"; +import { Provider } from "react-redux"; +import store from "../../store"; + +describe("OpeningsTab", () => { + // Create a new QueryClient instance for each test + const queryClient = new QueryClient(); + const showSpatial = false + const setShowSpatial = vi.fn() + + it("renders the component successfully", () => { + render( + + + + + + ); + // Check if the component is present with the correct text + const searchInput = screen.getByText(/Track the history of openings you have looked at and check spatial information by selecting the openings in the table below/i); + expect(searchInput).toBeInTheDocument(); + }); + + it("shows the spatial area with Hide Spatial Button", () => { + render( + + + + + + ); + console.log(screen.debug()) + // Check if the component is present with the correct text + const searchInput = screen.getByRole('button', { name: /Hide Spatial/i }); + expect(searchInput).toBeInTheDocument(); + }); +}); \ No newline at end of file From bf586ce9c33779dc72de0fd74f5bcb6a63e3092c Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 5 Nov 2024 17:15:21 -0800 Subject: [PATCH 28/43] added new test for the OpeningsTab --- .../__test__/components/OpeningsTab.test.tsx | 87 +++++++++++++++---- 1 file changed, 68 insertions(+), 19 deletions(-) diff --git a/frontend/src/__test__/components/OpeningsTab.test.tsx b/frontend/src/__test__/components/OpeningsTab.test.tsx index c81f3901..b090b84e 100644 --- a/frontend/src/__test__/components/OpeningsTab.test.tsx +++ b/frontend/src/__test__/components/OpeningsTab.test.tsx @@ -1,50 +1,99 @@ -// src/__test__/components/SilvicultureSearch/Openings/OpeningsSearchBar.test.tsx - import React from "react"; import { render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; -import { vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import OpeningsTab from "../../../src/components/OpeningsTab"; import { Provider } from "react-redux"; import store from "../../store"; +import {useUserRecentOpeningQuery} from "../../../src/services/queries/search/openingQueries"; +import { MemoryRouter } from "react-router-dom"; +import PaginationContext from "../../contexts/PaginationContext"; + +// Mocking useUserRecentOpeningQuery to return the necessary functions and state +vi.mock("../../../src/services/queries/search/openingQueries", () => ({ + useUserRecentOpeningQuery: vi.fn(), +})); + + +const paginationValueMock = { + getCurrentData: () => [], + currentPage: 0, + totalPages: 0, + handlePageChange: vi.fn(), + handleItemsPerPageChange: vi.fn(), + itemsPerPage: 5, + totalResultItems:100, + setTotalResultItems:vi.fn(), + setPageData: vi.fn(), + setInitialItemsPerPage: vi.fn(), + }; describe("OpeningsTab", () => { - // Create a new QueryClient instance for each test const queryClient = new QueryClient(); - const showSpatial = false - const setShowSpatial = vi.fn() + const showSpatial = false; + const setShowSpatial = vi.fn(); it("renders the component successfully", () => { + (useUserRecentOpeningQuery as jest.Mock).mockReturnValue({ data: [], isFetching: false }); render( + + - + /> - + + + ); - // Check if the component is present with the correct text const searchInput = screen.getByText(/Track the history of openings you have looked at and check spatial information by selecting the openings in the table below/i); expect(searchInput).toBeInTheDocument(); }); it("shows the spatial area with Hide Spatial Button", () => { + (useUserRecentOpeningQuery as jest.Mock).mockReturnValue({ data: [], isFetching: false }); render( + + - + /> - + + + ); - console.log(screen.debug()) - // Check if the component is present with the correct text - const searchInput = screen.getByRole('button', { name: /Hide Spatial/i }); - expect(searchInput).toBeInTheDocument(); + const hideSpatialButton = screen.getByRole('button', { name: /Hide Spatial/i }); + expect(hideSpatialButton).toBeInTheDocument(); + }); + + it("shows table skeleton when the data is loading", () => { + (useUserRecentOpeningQuery as jest.Mock).mockReturnValue({ data: [], isFetching: true }); + + render( + + + + + + + + + + ); + console.log(screen.debug()); + + expect.poll(() => document.querySelector('--cds-skeleton')).toBeTruthy(); }); -}); \ No newline at end of file + +}); From 7945aaf965509e38ce69e11194b5d8a83c5f2763 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 6 Nov 2024 11:21:31 -0800 Subject: [PATCH 29/43] more refinements on test --- frontend/src/__test__/components/OpeningsTab.test.tsx | 2 +- .../SilvicultureSearch/Openings/AdvancedSearchDropdown.test.tsx | 2 +- .../Dashboard/Opening/RecentOpeningsDataTable/index.tsx | 2 -- frontend/src/components/OpeningsTab/index.tsx | 2 -- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/__test__/components/OpeningsTab.test.tsx b/frontend/src/__test__/components/OpeningsTab.test.tsx index b090b84e..0b5cd8d1 100644 --- a/frontend/src/__test__/components/OpeningsTab.test.tsx +++ b/frontend/src/__test__/components/OpeningsTab.test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import OpeningsTab from "../../../src/components/OpeningsTab"; import { Provider } from "react-redux"; diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/AdvancedSearchDropdown.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/AdvancedSearchDropdown.test.tsx index 994b8faf..eedadc7b 100644 --- a/frontend/src/__test__/components/SilvicultureSearch/Openings/AdvancedSearchDropdown.test.tsx +++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/AdvancedSearchDropdown.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import "@testing-library/jest-dom"; import AdvancedSearchDropdown from "../../../../components/SilvicultureSearch/Openings/AdvancedSearchDropdown"; diff --git a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx index 7cef2986..bd74fccc 100644 --- a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx +++ b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx @@ -13,8 +13,6 @@ import { TableRow, Button, Pagination, - OverflowMenu, - OverflowMenuItem, Popover, PopoverContent, Checkbox, diff --git a/frontend/src/components/OpeningsTab/index.tsx b/frontend/src/components/OpeningsTab/index.tsx index 11579350..6b71fff0 100644 --- a/frontend/src/components/OpeningsTab/index.tsx +++ b/frontend/src/components/OpeningsTab/index.tsx @@ -23,8 +23,6 @@ interface Props { } const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { - const [openingRows, setOpeningRows] = useState([]); - const [error, setError] = useState(null); const [loadId, setLoadId] = useState(null); const [openingPolygonNotFound, setOpeningPolygonNotFound] = useState(false); const [wmsUsersWhitelist, setWmsUsersWhitelist] = useState([]); From 2391107593b3136be296039622287fe6f1d8323c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 05:05:46 -0800 Subject: [PATCH 30/43] chore(deps): update maven all non-major dependencies to v3.5.2 (#443) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- backend/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index 6d3f6fcd..26ba013e 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -291,7 +291,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.5.1 + 3.5.2 integration-tests @@ -318,7 +318,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.1 + 3.5.2 @{argLine} -Xmx1024m ${skip.unit.tests} From c903285d6c58c72766a3e0cd7a572b8c17654f0d Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 6 Nov 2024 12:39:28 -0800 Subject: [PATCH 31/43] feat(SILVA-514): adding favorite function on the backend (#423) --- .../UserFavoriteNotFoundException.java | 9 + .../endpoint/OpeningFavoriteEndpoint.java | 40 ++++ .../endpoint/UserOpeningEndpoint.java | 6 +- .../postgres/service/UserOpeningService.java | 95 ++++++--- ...peningFavoriteEndpointIntegrationTest.java | 133 ++++++++++++ .../service/UserOpeningServiceTest.java | 22 +- .../components/FavoriteButton.test.tsx | 12 +- .../__test__/components/MyProfile.test.tsx | 1 - .../components/OpeningHistory.test.tsx | 69 ++++++ .../components/OpeningMetricsTab.test.tsx | 109 ++++++++++ .../src/__test__/screens/Opening.test.tsx | 196 ++++++++++++------ .../services/OpeningFavoriteService.test.ts | 116 +++++++++++ .../__test__/services/OpeningService.test.ts | 146 +++++++++++++ .../components/DoughnutChartView/index.tsx | 3 +- .../src/components/FavoriteButton/index.tsx | 9 +- .../src/components/OpeningHistory/index.tsx | 16 +- .../components/OpeningMetricsTab/index.tsx | 17 +- .../Openings/SearchScreenDataTable/index.tsx | 32 ++- frontend/src/mock-data/OpeningHistoryItems.ts | 111 ---------- frontend/src/screens/Opening/index.tsx | 13 +- .../src/services/OpeningFavoriteService.ts | 73 +++++++ frontend/src/services/OpeningService.ts | 109 +++++----- frontend/src/types/OpeningTypes.ts | 37 ++++ 23 files changed, 1056 insertions(+), 318 deletions(-) create mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/common/exception/UserFavoriteNotFoundException.java create mode 100644 backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpoint.java create mode 100644 backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpointIntegrationTest.java create mode 100644 frontend/src/__test__/components/OpeningHistory.test.tsx create mode 100644 frontend/src/__test__/components/OpeningMetricsTab.test.tsx create mode 100644 frontend/src/__test__/services/OpeningFavoriteService.test.ts create mode 100644 frontend/src/__test__/services/OpeningService.test.ts delete mode 100644 frontend/src/mock-data/OpeningHistoryItems.ts create mode 100644 frontend/src/services/OpeningFavoriteService.ts create mode 100644 frontend/src/types/OpeningTypes.ts diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/UserFavoriteNotFoundException.java b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/UserFavoriteNotFoundException.java new file mode 100644 index 00000000..5d931aa3 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/UserFavoriteNotFoundException.java @@ -0,0 +1,9 @@ +package ca.bc.gov.restapi.results.common.exception; + +public class UserFavoriteNotFoundException extends NotFoundGenericException { + + public UserFavoriteNotFoundException() { + super("UserFavoriteEntity"); + } + +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpoint.java new file mode 100644 index 00000000..d766e9f0 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpoint.java @@ -0,0 +1,40 @@ +package ca.bc.gov.restapi.results.postgres.endpoint; + +import ca.bc.gov.restapi.results.postgres.service.UserOpeningService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/api/openings/favorites", produces = MediaType.APPLICATION_JSON_VALUE) +@RequiredArgsConstructor +public class OpeningFavoriteEndpoint { + + private final UserOpeningService userOpeningService; + + @GetMapping + public List getFavorites() { + return userOpeningService.listUserFavoriteOpenings(); + } + + @PutMapping("/{id}") + @ResponseStatus(HttpStatus.ACCEPTED) + public void addToFavorites(@PathVariable Long id) { + userOpeningService.addUserFavoriteOpening(id); + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void removeFromFavorites(@PathVariable Long id) { + userOpeningService.removeUserFavoriteOpening(id); + } + +} diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java index c1bb619f..3b519f43 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java @@ -45,8 +45,8 @@ public ResponseEntity> getUserTrackedOpenings() * @return HTTP status code 201 if success, no response body. */ @PostMapping("/{id}") - public ResponseEntity saveUserOpening(Long id) { - userOpeningService.saveOpeningToUser(id); + public ResponseEntity saveUserOpening(@PathVariable Long id) { + userOpeningService.addUserFavoriteOpening(id); return ResponseEntity.status(HttpStatus.CREATED).build(); } @@ -60,7 +60,7 @@ public ResponseEntity saveUserOpening(Long id) { public ResponseEntity deleteUserOpening( @PathVariable Long id) { - userOpeningService.deleteOpeningFromUserFavourite(id); + userOpeningService.removeUserFavoriteOpening(id); return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); } } diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningService.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningService.java index 421ca6b5..448892c2 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningService.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningService.java @@ -1,7 +1,14 @@ package ca.bc.gov.restapi.results.postgres.service; +import ca.bc.gov.restapi.results.common.exception.OpeningNotFoundException; +import ca.bc.gov.restapi.results.common.exception.UserFavoriteNotFoundException; import ca.bc.gov.restapi.results.common.exception.UserOpeningNotFoundException; import ca.bc.gov.restapi.results.common.security.LoggedUserService; +import ca.bc.gov.restapi.results.oracle.dto.OpeningSearchResponseDto; +import ca.bc.gov.restapi.results.oracle.entity.OpeningEntity; +import ca.bc.gov.restapi.results.oracle.enums.OpeningCategoryEnum; +import ca.bc.gov.restapi.results.oracle.enums.OpeningStatusEnum; +import ca.bc.gov.restapi.results.oracle.repository.OpeningRepository; import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto; import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity; import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntity; @@ -11,13 +18,14 @@ import jakarta.transaction.Transactional; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.ocpsoft.prettytime.PrettyTime; import org.springframework.stereotype.Service; -/** This class contains methods for handling User favourite Openings. */ +/** + * This class contains methods for handling User favourite Openings. + */ @Slf4j @Service @RequiredArgsConstructor @@ -29,6 +37,8 @@ public class UserOpeningService { private final OpeningsActivityRepository openingsActivityRepository; + private final OpeningRepository openingRepository; + /** * Gets user's tracked Openings. * @@ -78,46 +88,63 @@ public List getUserTrackedOpenings() { return resultList; } - /** - * Saves one or more Openings IDs to an user. - * - * @param openingId The opening ID. - */ - @Transactional - public void saveOpeningToUser(Long openingId) { - log.info("Opening ID to save in the user favourites: {}", openingId); + public List listUserFavoriteOpenings() { + log.info("Loading user favorite openings for {}", loggedUserService.getLoggedUserId()); - final String userId = loggedUserService.getLoggedUserId(); + List userList = userOpeningRepository + .findAllByUserId(loggedUserService.getLoggedUserId()); - UserOpeningEntity entity = new UserOpeningEntity(); - entity.setUserId(userId); - entity.setOpeningId(openingId); + if (userList.isEmpty()) { + log.info("No saved openings for {}", loggedUserService.getLoggedUserId()); + return List.of(); + } - userOpeningRepository.saveAndFlush(entity); - log.info("Opening ID saved in the user's favourites!"); + return + userList + .stream() + .map(UserOpeningEntity::getOpeningId) + .toList(); } - /** - * Deletes one or more user opening from favourite. - * - * @param openingId The opening ID. - */ @Transactional - public void deleteOpeningFromUserFavourite(Long openingId) { - log.info("Opening ID to delete from the user's favourites: {}", openingId); - String userId = loggedUserService.getLoggedUserId(); - - UserOpeningEntityId openingPk = new UserOpeningEntityId(userId, openingId); + public void addUserFavoriteOpening(Long openingId) { + log.info("Adding opening ID {} as favorite for user {}", openingId, + loggedUserService.getLoggedUserId()); - Optional userOpeningsOp = userOpeningRepository.findById(openingPk); - - if (userOpeningsOp.isEmpty()) { - log.info("Opening id {} not found in the user's favourite list!", openingId); - throw new UserOpeningNotFoundException(); + if (openingRepository.findById(openingId).isEmpty()) { + log.info("Opening ID not found: {}", openingId); + throw new OpeningNotFoundException(); } - userOpeningRepository.delete(userOpeningsOp.get()); - userOpeningRepository.flush(); - log.info("Opening ID deleted from the favourites!"); + log.info("Opening ID {} added as favorite for user {}", openingId, + loggedUserService.getLoggedUserId()); + userOpeningRepository.saveAndFlush( + new UserOpeningEntity( + loggedUserService.getLoggedUserId(), + openingId + ) + ); + } + + @Transactional + public void removeUserFavoriteOpening(Long openingId) { + log.info("Removing opening ID {} from the favorites for user {}", openingId, + loggedUserService.getLoggedUserId()); + userOpeningRepository.findById( + new UserOpeningEntityId( + loggedUserService.getLoggedUserId(), + openingId + ) + ).ifPresentOrElse( + userOpening -> { + userOpeningRepository.delete(userOpening); + userOpeningRepository.flush(); + log.info("Opening ID deleted from the favourites!"); + }, + () -> { + log.info("Opening id {} not found in the user's favourite list!", openingId); + throw new UserFavoriteNotFoundException(); + } + ); } } diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpointIntegrationTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpointIntegrationTest.java new file mode 100644 index 00000000..3f064214 --- /dev/null +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/OpeningFavoriteEndpointIntegrationTest.java @@ -0,0 +1,133 @@ +package ca.bc.gov.restapi.results.postgres.endpoint; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import ca.bc.gov.restapi.results.extensions.AbstractTestContainerIntegrationTest; +import ca.bc.gov.restapi.results.extensions.WithMockJwt; +import ca.bc.gov.restapi.results.postgres.repository.UserOpeningRepository; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@DisplayName("Integration Test | Favorite Openings Endpoint") +@TestMethodOrder(OrderAnnotation.class) +@WithMockJwt +@AutoConfigureMockMvc +class OpeningFavoriteEndpointIntegrationTest extends AbstractTestContainerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserOpeningRepository userOpeningRepository; + + @Test + @Order(1) + @DisplayName("No favorites to begin with") + void shouldBeEmpty() throws Exception { + + mockMvc + .perform( + MockMvcRequestBuilders.get("/api/openings/favorites") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isEmpty()); + } + + @Test + @Order(2) + @DisplayName("Should add to favorite") + void shouldAddToFavorite() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.put("/api/openings/favorites/{openingId}", 101) + .with(csrf().asHeader()) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isAccepted()) + .andExpect(content().string(StringUtils.EMPTY)); + + assertThat(userOpeningRepository.findAll()) + .isNotNull() + .isNotEmpty() + .hasSize(1); + } + + @Test + @Order(3) + @DisplayName("Should not add to favorite if doesn't exist") + void shouldNotAddIfDoesNotExist() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.put("/api/openings/favorites/{openingId}", 987) + .with(csrf().asHeader()) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(content().string(StringUtils.EMPTY)); + //.andExpect(content().string("UserOpening record(s) not found!")); + } + + @Test + @Order(4) + @DisplayName("Multiple requests to add to favorite should not fail, nor duplicate") + void shouldAddToFavoriteAgain() throws Exception { + shouldAddToFavorite(); + } + + @Test + @Order(5) + @DisplayName("Should see list of favorites") + void shouldBeAbleToSeeOpening() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.get("/api/openings/favorites") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.[0]").value(101)); + } + + @Test + @Order(6) + @DisplayName("Should remove from favorite") + void shouldRemoveFromFavorites() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.delete("/api/openings/favorites/{openingId}", 101) + .with(csrf().asHeader()) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andExpect(content().string(StringUtils.EMPTY)); + + assertThat(userOpeningRepository.findAll()) + .isNotNull() + .isEmpty(); + } + + @Test + @Order(7) + @DisplayName("Should thrown an error if trying to remove entry that doesn't exist") + void shouldThrownErrorIfNoFavoriteFound() throws Exception { + mockMvc + .perform( + MockMvcRequestBuilders.delete("/api/openings/favorites/{openingId}", 101) + .with(csrf().asHeader()) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andExpect(content().string(StringUtils.EMPTY)); + + } + + +} \ No newline at end of file diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningServiceTest.java index 89bbcc27..eb6a30b5 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningServiceTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningServiceTest.java @@ -4,8 +4,11 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; +import ca.bc.gov.restapi.results.common.exception.UserFavoriteNotFoundException; import ca.bc.gov.restapi.results.common.exception.UserOpeningNotFoundException; import ca.bc.gov.restapi.results.common.security.LoggedUserService; +import ca.bc.gov.restapi.results.oracle.entity.OpeningEntity; +import ca.bc.gov.restapi.results.oracle.repository.OpeningRepository; import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto; import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity; import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntity; @@ -31,6 +34,8 @@ class UserOpeningServiceTest { @Mock OpeningsActivityRepository openingsActivityRepository; + @Mock OpeningRepository openingRepository; + private UserOpeningService userOpeningService; private static final String USER_ID = "TEST"; @@ -39,7 +44,7 @@ class UserOpeningServiceTest { void setup() { this.userOpeningService = new UserOpeningService( - loggedUserService, userOpeningRepository, openingsActivityRepository); + loggedUserService, userOpeningRepository, openingsActivityRepository,openingRepository); } @Test @@ -90,15 +95,16 @@ void getUserTrackedOpenings_noData_shouldSucceed() { @Test @DisplayName("Save opening to user happy path should succeed") - void saveOpeningToUser_happyPath_shouldSucceed() { + void addUser_FavoriteOpening_happyPath_shouldSucceed() { when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID); + when(openingRepository.findById(any())).thenReturn(Optional.of(new OpeningEntity())); when(userOpeningRepository.saveAndFlush(any())).thenReturn(new UserOpeningEntity()); - userOpeningService.saveOpeningToUser(112233L); + userOpeningService.addUserFavoriteOpening(112233L); } @Test @DisplayName("Delete opening from user's favourite happy path should succeed") - void deleteOpeningFromUserFavourite_happyPath_shouldSucceed() { + void removeUserFavoriteOpening_happyPath_shouldSucceed() { when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID); UserOpeningEntity userEntity = new UserOpeningEntity(); @@ -107,19 +113,19 @@ void deleteOpeningFromUserFavourite_happyPath_shouldSucceed() { doNothing().when(userOpeningRepository).delete(any()); doNothing().when(userOpeningRepository).flush(); - userOpeningService.deleteOpeningFromUserFavourite(112233L); + userOpeningService.removeUserFavoriteOpening(112233L); } @Test @DisplayName("Delete opening from user's favourite not found should fail") - void deleteOpeningFromUserFavourite_notFound_shouldFail() { + void removeUserFavoriteOpening_notFound_shouldFail() { when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID); when(userOpeningRepository.findById(any())).thenReturn(Optional.empty()); Assertions.assertThrows( - UserOpeningNotFoundException.class, + UserFavoriteNotFoundException.class, () -> { - userOpeningService.deleteOpeningFromUserFavourite(112233L); + userOpeningService.removeUserFavoriteOpening(112233L); }); } } diff --git a/frontend/src/__test__/components/FavoriteButton.test.tsx b/frontend/src/__test__/components/FavoriteButton.test.tsx index 152a8a3a..39e31f27 100644 --- a/frontend/src/__test__/components/FavoriteButton.test.tsx +++ b/frontend/src/__test__/components/FavoriteButton.test.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import FavoriteButton from '../../components/FavoriteButton'; import '@testing-library/jest-dom'; +import { on } from 'events'; describe('FavoriteButton Component', () => { const props = { @@ -10,6 +11,8 @@ describe('FavoriteButton Component', () => { kind: 'ghost', size: 'md', fill: 'red', + favorited: false, + onFavoriteChange: vi.fn(), }; it('should render the component with default state', () => { @@ -33,5 +36,12 @@ describe('FavoriteButton Component', () => { const imgElement = screen.getByTestId('favourite-button-icon'); expect(imgElement).toHaveStyle('fill: red'); }); + + it('should call onFavoriteChange with the new favorite state', () => { + render(); + const buttonElement = screen.getByRole('button'); + fireEvent.click(buttonElement); + expect(props.onFavoriteChange).toHaveBeenCalledWith(false); + }); }); diff --git a/frontend/src/__test__/components/MyProfile.test.tsx b/frontend/src/__test__/components/MyProfile.test.tsx index 5e723ce3..078e5ce9 100644 --- a/frontend/src/__test__/components/MyProfile.test.tsx +++ b/frontend/src/__test__/components/MyProfile.test.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import React from 'react'; import { render, act, waitFor, fireEvent, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import MyProfile from '../../components/MyProfile'; diff --git a/frontend/src/__test__/components/OpeningHistory.test.tsx b/frontend/src/__test__/components/OpeningHistory.test.tsx new file mode 100644 index 00000000..e65b5a08 --- /dev/null +++ b/frontend/src/__test__/components/OpeningHistory.test.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { render, act } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import OpeningHistory from '../../components/OpeningHistory'; +import History from '../../types/History'; +import { deleteOpeningFavorite } from '../../services/OpeningFavoriteService'; + +const mockHistories: History[] = [ + { + id: 1, + steps: [], + }, + { + id: 2, + steps: [ + { step: 1, status: 'complete', description: 'Step 1', subtitle: 'Completed' }, + { step: 2, status: 'invalid', description: 'Step 2', subtitle: 'Invalid' }, + { step: 3, status: 'disabled', description: 'Step 3', subtitle: 'Disabled' }, + ], + }, +]; + +vi.mock('../../services/OpeningFavoriteService', () => ({ + deleteOpeningFavorite: vi.fn(), +})); + +describe('OpeningHistory Component', () => { + it('renders correctly with given histories', async () => { + let getByText; + await act(async () => { + ({ getByText } = render( )); + }); + + // Check for the presence of Opening Ids + expect(getByText('Opening Id 1')).toBeInTheDocument(); + expect(getByText('Opening Id 2')).toBeInTheDocument(); + + // Check for the presence of step descriptions + expect(getByText('Step 1')).toBeInTheDocument(); + expect(getByText('Step 2')).toBeInTheDocument(); + expect(getByText('Step 3')).toBeInTheDocument(); + }); + + it('renders correctly with empty histories', async () => { + let container; + await act(async () => { + ({ container } = render( )); + }); + + // Select the div with the specific class + const activityHistoryContainer = container.querySelector('.row.activity-history-container.gx-4'); + + // Check if the container is empty + expect(activityHistoryContainer).toBeInTheDocument(); // Ensure the element exists + expect(activityHistoryContainer?.children.length).toBe(0); // Confirm it's empty by checking for no children + }); + + // check if when clicked on the FavoriteButton, the deleteOpeningFavorite function is called + it('should call deleteOpeningFavorite when FavoriteButton is clicked', async () => { + let container; + await act(async () => { + ({ container } = render( )); + }); + + const favoriteButton = container.querySelector('.favorite-icon button') + favoriteButton && favoriteButton.click(); + expect(deleteOpeningFavorite).toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/__test__/components/OpeningMetricsTab.test.tsx b/frontend/src/__test__/components/OpeningMetricsTab.test.tsx new file mode 100644 index 00000000..75a27d69 --- /dev/null +++ b/frontend/src/__test__/components/OpeningMetricsTab.test.tsx @@ -0,0 +1,109 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import React from 'react'; +import { render, act, waitFor, fireEvent, screen } from '@testing-library/react'; +import OpeningMetricsTab from '../../components/OpeningMetricsTab'; +import { fetchOpeningTrends } from '../../services/OpeningFavoriteService'; +import { fetchFreeGrowingMilestones, fetchOpeningsPerYear, fetchRecentOpenings } from '../../services/OpeningService'; + +vi.mock('../../services/OpeningFavoriteService', () => ({ + fetchOpeningTrends: vi.fn(), +})); +vi.mock('../../services/OpeningService', async () => { + const actual = await vi.importActual('../../services/OpeningService'); + return { + ...actual, + fetchRecentOpenings: vi.fn(), + fetchOpeningsPerYear: vi.fn(), + fetchFreeGrowingMilestones: vi.fn(), + }; +}); + +describe('OpeningMetricsTab', () => { + beforeEach(() => { + vi.clearAllMocks(); + (fetchRecentOpenings as vi.Mock).mockResolvedValue([{ + id: '123', + openingId: '123', + fileId: '1', + cuttingPermit: '1', + timberMark: '1', + cutBlock: '1', + grossAreaHa: 1, + statusDesc: 'Approved', + categoryDesc: 'Another:Another', + disturbanceStart: '1', + entryTimestamp: '1', + updateTimestamp: '1', + }]); + (fetchOpeningsPerYear as vi.Mock).mockResolvedValue([ + { group: '2022', key: 'Openings', value: 10 }, + { group: '2023', key: 'Openings', value: 15 }, + ]); + (fetchFreeGrowingMilestones as vi.Mock).mockResolvedValue([{ group: '1-5', value: 11 }]); + (fetchOpeningTrends as vi.Mock).mockResolvedValue([1, 2, 3]); + + }); + + it('should render the OpeningMetricsTab component with all sections', async () => { + + await act(async () => render()); + + expect(screen.getByText('Dashboard')).toBeInTheDocument(); + expect(screen.getByText('Manage and track silvicultural information about openings')).toBeInTheDocument(); + expect(screen.getByText('Openings submission trends')).toBeInTheDocument(); + expect(screen.getByText('Check quantity and evolution of openings')).toBeInTheDocument(); + expect(screen.getByText('Track Openings')).toBeInTheDocument(); + expect(screen.getByText('Follow your favourite openings')).toBeInTheDocument(); + expect(screen.getByText('Free growing milestone declarations')).toBeInTheDocument(); + expect(screen.getByText('Check opening standards unit for inspections purposes')).toBeInTheDocument(); + expect(screen.getByText('My recent actions')).toBeInTheDocument(); + expect(screen.getByText('Check your recent requests and files')).toBeInTheDocument(); + }); + + it('should call fetchOpeningTrends and set submissionTrends state', async () => { + + + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(fetchOpeningTrends).toHaveBeenCalled(); + expect(screen.getByText('Opening Id 1')).toBeInTheDocument(); + expect(screen.getByText('Opening Id 2')).toBeInTheDocument(); + expect(screen.getByText('Opening Id 3')).toBeInTheDocument(); + }); + }); + + it('should scroll to "Track Openings" section when scrollTo parameter is "trackOpenings"', async () => { + + const mockScrollIntoView = vi.fn(); + window.HTMLElement.prototype.scrollIntoView = mockScrollIntoView; + + const originalLocation = window.location; + delete window.location; + window.location = { search: '?scrollTo=trackOpenings' } as any; + + await act(async () => render()); + + expect(mockScrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' }); + + window.location = originalLocation; + }); + + it('should not scroll to "Track Openings" section when scrollTo parameter is not "trackOpenings"', async () => { + + const mockScrollIntoView = vi.fn(); + window.HTMLElement.prototype.scrollIntoView = mockScrollIntoView; + + const originalLocation = window.location; + delete window.location; + window.location = { search: '' } as any; + + await act(async () => render()); + + expect(mockScrollIntoView).not.toHaveBeenCalled(); + + window.location = originalLocation; + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/screens/Opening.test.tsx b/frontend/src/__test__/screens/Opening.test.tsx index 4ac7b4a3..8e4d3970 100644 --- a/frontend/src/__test__/screens/Opening.test.tsx +++ b/frontend/src/__test__/screens/Opening.test.tsx @@ -1,65 +1,42 @@ import React from 'react'; import { describe, expect, it, vi } from 'vitest'; -import { render, waitFor } from '@testing-library/react'; +import { render, waitFor, act } from '@testing-library/react'; import Opening from '../../screens/Opening'; import PaginationContext from '../../contexts/PaginationContext'; import { BrowserRouter } from 'react-router-dom'; import * as redux from 'react-redux'; import { RecentOpening } from '../../types/RecentOpening'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { getWmsLayersWhitelistUsers } from '../../services/SecretsService'; +import { fetchFreeGrowingMilestones, fetchOpeningsPerYear, fetchRecentOpenings } from '../../services/OpeningService'; +import { fetchOpeningTrends } from '../../services/OpeningFavoriteService'; -// Mock data and services const data = { - activityType: "Update", - openingId: "1541297", - statusCode: "APP", - statusDescription: "Approved", - lastUpdatedLabel: "1 minute ago", - lastUpdated: "2024-05-16T19:59:21.635Z" + "activityType": "Update", + "openingId": "1541297", + "statusCode": "APP", + "statusDescription": "Approved", + "lastUpdatedLabel": "1 minute ago", + "lastUpdated": "2024-05-16T19:59:21.635Z" }; -vi.mock('../../services/SecretsService', () => ({ - getWmsLayersWhitelistUsers: vi.fn(() => [ - { userName: 'TEST' } - ]) +vi.mock('../../services/OpeningFavoriteService', () => ({ + fetchOpeningTrends: vi.fn(), })); -vi.mock('../../services/OpeningService', () => ({ - fetchRecentOpenings: vi.fn(() => [ - { - id: '123', - openingId: '111', - fileId: 'FS7', - cuttingPermit: 'SS', - timberMark: '207S', - cutBlock: '111', - grossAreaHa: 265, - statusDesc: 'Approved', - categoryDesc: 'FTML', - disturbanceStart: '2023-01-02', - entryTimestamp: '', - updateTimestamp: '' - } - ]), - fetchOpeningsPerYear: vi.fn(() => Promise.resolve([ - { group: '2022', key: 'Openings', value: 10 }, - { group: '2023', key: 'Openings', value: 15 }, - ])), - fetchFreeGrowingMilestones: vi.fn(() => Promise.resolve([ - { group: '1-5', value: 11 } - ])), - fetchRecentActions: vi.fn(() => [ - { - activityType: data.activityType, - openingId: data.openingId.toString(), - statusCode: data.statusCode, - statusDescription: data.statusDescription, - lastUpdated: data.lastUpdated, - lastUpdatedLabel: data.lastUpdatedLabel - } - ]), +vi.mock('../../services/SecretsService', () => ({ + getWmsLayersWhitelistUsers: vi.fn() })); +vi.mock('../../services/OpeningService', async () => { + const actual = await vi.importActual('../../services/OpeningService'); + return { + ...actual, + fetchRecentOpenings: vi.fn(), + fetchOpeningsPerYear: vi.fn(), + fetchFreeGrowingMilestones: vi.fn(), + }; +}); + const state = { userDetails: { id: 1, @@ -70,11 +47,10 @@ const state = { vi.spyOn(redux, 'useSelector') .mockImplementation((callback) => callback(state)); -// Pagination context mock const rows: RecentOpening[] = [{ id: '123', openingId: '123', - forestFileId: '1', + fileId: '1', cuttingPermit: '1', timberMark: '1', cutBlock: '1', @@ -85,7 +61,7 @@ const rows: RecentOpening[] = [{ entryTimestamp: '1', updateTimestamp: '1', }]; - + const paginationValueMock = { getCurrentData: () => rows, currentPage: 0, @@ -97,30 +73,118 @@ const paginationValueMock = { setInitialItemsPerPage: vi.fn(), }; -// Create a query client for testing -const createTestQueryClient = () => new QueryClient({ - defaultOptions: { - queries: { - retry: false, // Disable retries for test stability - }, - }, -}); - describe('Opening screen test cases', () => { - it('should render Opening Page Title component', async () => { - const queryClient = createTestQueryClient(); + beforeEach(() => { + vi.clearAllMocks(); + + (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{userName: 'TEST'}]); + (fetchRecentOpenings as vi.Mock).mockResolvedValue(rows); + (fetchOpeningsPerYear as vi.Mock).mockResolvedValue([ + { group: '2022', key: 'Openings', value: 10 }, + { group: '2023', key: 'Openings', value: 15 }, + ]); + (fetchFreeGrowingMilestones as vi.Mock).mockResolvedValue([{ group: '1-5', value: 11 }]); + (fetchOpeningTrends as vi.Mock).mockResolvedValue([1,2,3]); + + + + + }); + + it('should renders Opening Page Title component', async () => { const { getByTestId } = render( - + + + + + + ); + + const pageTitleComp = await waitFor(() => getByTestId('opening-pagetitle')); + expect(pageTitleComp).toBeDefined(); + + //const subtitle = 'Create, manage or check opening information'; + //expect(screen.getByText(subtitle)).toBeDefined(); + }); + + describe('FavoriteCards test cases', () => { + + it('should render FavoriteCard component', async () => { + + let container: HTMLElement = document.createElement('div'); + await act(async () => { + ({ container } = render( - - ); + )); + }); + + // check if first FavoriteCard has the correct title and is active + expect(container.querySelector('#fav-card-1')).toBeDefined(); + expect(container.querySelector('#fav-card-1')?.textContent).toContain('Silviculture search'); + expect(container.querySelector('#fav-card-1')?.className).not.contain('cds--link--disable'); + + // check if the second FavoriteCard has the correct title and is inactive + expect(container.querySelector('#fav-card-2')).toBeDefined(); + expect(container.querySelector('#fav-card-2')?.textContent).toContain('Create an opening'); + expect(container.querySelector('#fav-card-2')?.className).toContain('cds--link--disable'); + + // check if the third FavoriteCard has the correct title and is inactive + expect(container.querySelector('#fav-card-3')).toBeDefined(); + expect(container.querySelector('#fav-card-3')?.textContent).toContain('Reports'); + expect(container.querySelector('#fav-card-3')?.className).toContain('cds--link--disable'); + + // check if the fourth FavoriteCard has the correct title and is inactive + expect(container.querySelector('#fav-card-4')).toBeDefined(); + expect(container.querySelector('#fav-card-4')?.textContent).toContain('Upcoming activities'); + expect(container.querySelector('#fav-card-4')?.className).toContain('cds--link--disable'); + + }); + + it('should not render tab when not selected', async () => { + let container: HTMLElement = document.createElement('div'); + let getByText: any; + await act(async () => { + ({ container, getByText } = render( + + + + + + )); + }); + + // check if the tab is not rendered + expect(container.querySelector('div.tab-openings')?.childNodes).toHaveLength(2); + expect(container.querySelector('div.tab-metrics')?.childNodes).toHaveLength(0); + + }); + + it('should render tab only when selected', async () => { + let container: HTMLElement = document.createElement('div'); + let getByText: any; + await act(async () => { + ({ container, getByText } = render( + + + + + + )); + }); + + await act(async () => getByText('Dashboard').click()); + + expect(container.querySelector('div.tab-openings')?.childNodes).toHaveLength(2); + expect(container.querySelector('div.tab-metrics')?.childNodes).toHaveLength(1); + + }); + - const pageTitleComp = await waitFor(() => getByTestId('opening-pagetitle')); - expect(pageTitleComp).toBeDefined(); }); + }); diff --git a/frontend/src/__test__/services/OpeningFavoriteService.test.ts b/frontend/src/__test__/services/OpeningFavoriteService.test.ts new file mode 100644 index 00000000..d24962fa --- /dev/null +++ b/frontend/src/__test__/services/OpeningFavoriteService.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, vi } from 'vitest'; +import axios from 'axios'; +import { fetchOpeningTrends} from '../../services/OpeningFavoriteService'; +import { getAuthIdToken } from '../../services/AuthService'; +import { env } from '../../env'; +import { fetchOpeningTrends, setOpeningFavorite, deleteOpeningFavorite } from '../../services/OpeningFavoriteService'; + +vi.mock('axios'); +vi.mock('../../services/AuthService'); + +describe('OpeningFavoriteService', () => { + const backendUrl = env.VITE_BACKEND_URL; + const authToken = 'test-token'; + + beforeEach(() => { + vi.clearAllMocks(); + (getAuthIdToken as vi.Mock).mockReturnValue(authToken); + }); + + it('should fetch submission trends successfully', async () => { + const mockData = [1, 2, 3]; + (axios.get as vi.Mock).mockResolvedValue({ data: mockData }); + + const result = await fetchOpeningTrends(); + + expect(axios.get).toHaveBeenCalledWith(`${backendUrl}/api/openings/favorites`, { + headers: { Authorization: `Bearer ${authToken}` } + }); + expect(result).toEqual(mockData); + }); + + it('should fetch submission trends with empty results', async () => { + const mockData = []; + (axios.get as vi.Mock).mockResolvedValue({ data: mockData }); + + const result = await fetchOpeningTrends(); + + expect(axios.get).toHaveBeenCalledWith(`${backendUrl}/api/openings/favorites`, { + headers: { Authorization: `Bearer ${authToken}` } + }); + expect(result).toEqual(mockData); + }); + + it('should handle error while fetching submission trends', async () => { + (axios.get as vi.Mock).mockRejectedValue(new Error('Network Error')); + + await expect(fetchOpeningTrends()).rejects.toThrow('Network Error'); + }); + + it('should fetch submission trends successfully', async () => { + const mockData = [1, 2, 3]; + (axios.get as vi.Mock).mockResolvedValue({ data: mockData }); + + const result = await fetchOpeningTrends(); + + expect(axios.get).toHaveBeenCalledWith(`${backendUrl}/api/openings/favorites`, { + headers: { Authorization: `Bearer ${authToken}` } + }); + expect(result).toEqual(mockData); + }); + + it('should fetch submission trends with empty results', async () => { + const mockData = []; + (axios.get as vi.Mock).mockResolvedValue({ data: mockData }); + + const result = await fetchOpeningTrends(); + + expect(axios.get).toHaveBeenCalledWith(`${backendUrl}/api/openings/favorites`, { + headers: { Authorization: `Bearer ${authToken}` } + }); + expect(result).toEqual(mockData); + }); + + it('should handle error while fetching submission trends', async () => { + (axios.get as vi.Mock).mockRejectedValue(new Error('Network Error')); + + await expect(fetchOpeningTrends()).rejects.toThrow('Network Error'); + }); + + it('should set an opening as favorite successfully', async () => { + const openingId = 1; + (axios.put as vi.Mock).mockResolvedValue({ status: 202 }); + + await setOpeningFavorite(openingId); + + expect(axios.put).toHaveBeenCalledWith(`${backendUrl}/api/openings/favorites/${openingId}`, null, { + headers: { Authorization: `Bearer ${authToken}` } + }); + }); + + it('should throw an error if setting an opening as favorite fails', async () => { + const openingId = 1; + (axios.put as vi.Mock).mockResolvedValue({ status: 500 }); + + await expect(setOpeningFavorite(openingId)).rejects.toThrow('Failed to set favorite opening. Status code: 500'); + }); + + it('should delete a favorite opening successfully', async () => { + const openingId = 1; + (axios.delete as vi.Mock).mockResolvedValue({ status: 204 }); + + await deleteOpeningFavorite(openingId); + + expect(axios.delete).toHaveBeenCalledWith(`${backendUrl}/api/openings/favorites/${openingId}`, { + headers: { Authorization: `Bearer ${authToken}` } + }); + }); + + it('should throw an error if deleting a favorite opening fails', async () => { + const openingId = 1; + (axios.delete as vi.Mock).mockResolvedValue({ status: 500 }); + + await expect(deleteOpeningFavorite(openingId)).rejects.toThrow('Failed to remove favorite opening. Status code: 500'); + }); + +}); \ No newline at end of file diff --git a/frontend/src/__test__/services/OpeningService.test.ts b/frontend/src/__test__/services/OpeningService.test.ts new file mode 100644 index 00000000..6dd4f320 --- /dev/null +++ b/frontend/src/__test__/services/OpeningService.test.ts @@ -0,0 +1,146 @@ +import { describe, it, expect, vi } from 'vitest'; +import axios from 'axios'; +import { + fetchRecentOpenings, + fetchOpeningsPerYear, + fetchFreeGrowingMilestones, + fetchRecentActions +} from '../../services/OpeningService'; +import { getAuthIdToken } from '../../services/AuthService'; +import { env } from '../../env'; + +vi.mock('axios'); +vi.mock('../../services/AuthService'); + +describe('OpeningService', () => { + const backendUrl = env.VITE_BACKEND_URL; + const authToken = 'test-token'; + + beforeEach(() => { + vi.clearAllMocks(); + (getAuthIdToken as vi.Mock).mockReturnValue(authToken); + }); + + describe('fetchRecentOpenings', () => { + it('should fetch recent openings successfully', async () => { + const mockData = { + data: [ + { + openingId: 1, + forestFileId: '123', + cuttingPermit: '456', + timberMark: '789', + cutBlock: 'A', + grossAreaHa: 10, + status: { description: 'Active' }, + category: { description: 'Category1' }, + disturbanceStart: '2023-01-01', + entryTimestamp: '2023-01-01T00:00:00Z', + updateTimestamp: '2023-01-02T00:00:00Z' + } + ] + }; + (axios.get as vi.Mock).mockResolvedValue({ status: 200, data: mockData }); + + const result = await fetchRecentOpenings(); + + expect(axios.get).toHaveBeenCalledWith(`${backendUrl}/api/openings/recent-openings?page=0&perPage=100`, { + headers: { Authorization: `Bearer ${authToken}` } + }); + expect(result).toEqual([ + { + id: '1', + openingId: '1', + forestFileId: '123', + cuttingPermit: '456', + timberMark: '789', + cutBlock: 'A', + grossAreaHa: '10', + status: 'Active', + category: 'Category1', + disturbanceStart: '2023-01-01', + entryTimestamp: '2023-01-01', + updateTimestamp: '2023-01-02' + } + ]); + }); + + it('should handle error while fetching recent openings', async () => { + (axios.get as vi.Mock).mockRejectedValue(new Error('Network Error')); + + await expect(fetchRecentOpenings()).rejects.toThrow('Network Error'); + }); + }); + + describe('fetchOpeningsPerYear', () => { + it('should fetch openings per year successfully', async () => { + const mockData = [ + { monthName: 'January', amount: 10 }, + { monthName: 'February', amount: 20 } + ]; + (axios.get as vi.Mock).mockResolvedValue({ data: mockData }); + + const props = { orgUnitCode: '001', statusCode: 'APP', entryDateStart: '2023-01-01', entryDateEnd: '2023-12-31' }; + const result = await fetchOpeningsPerYear(props); + + expect(axios.get).toHaveBeenCalledWith(`${backendUrl}/api/dashboard-metrics/submission-trends?orgUnitCode=001&statusCode=APP&entryDateStart=2023-01-01&entryDateEnd=2023-12-31`, { + headers: { Authorization: `Bearer ${authToken}` } + }); + expect(result).toEqual([ + { group: 'Openings', key: 'January', value: 10 }, + { group: 'Openings', key: 'February', value: 20 } + ]); + }); + + it('should handle error while fetching openings per year', async () => { + (axios.get as vi.Mock).mockRejectedValue(new Error('Network Error')); + + await expect(fetchOpeningsPerYear({})).rejects.toThrow('Network Error'); + }); + }); + + describe('fetchFreeGrowingMilestones', () => { + it('should fetch free growing milestones successfully', async () => { + const mockData = [ + { label: 'Milestone1', amount: 10 }, + { label: 'Milestone2', amount: 20 } + ]; + (axios.get as vi.Mock).mockResolvedValue({ data: mockData }); + + const props = { orgUnitCode: '001', clientNumber: '123', entryDateStart: '2023-01-01', entryDateEnd: '2023-12-31' }; + const result = await fetchFreeGrowingMilestones(props); + + expect(axios.get).toHaveBeenCalledWith(`${backendUrl}/api/dashboard-metrics/free-growing-milestones?orgUnitCode=001&clientNumber=123&entryDateStart=2023-01-01&entryDateEnd=2023-12-31`, { + headers: { Authorization: `Bearer ${authToken}` } + }); + expect(result).toEqual([ + { group: 'Milestone1', value: 10 }, + { group: 'Milestone2', value: 20 } + ]); + }); + + it('should handle error while fetching free growing milestones', async () => { + (axios.get as vi.Mock).mockRejectedValue(new Error('Network Error')); + + await expect(fetchFreeGrowingMilestones({})).rejects.toThrow('Network Error'); + }); + }); + + describe('fetchRecentActions', () => { + it('should fetch recent actions successfully', () => { + const result = fetchRecentActions(); + + expect(result).toEqual([ + { + activityType: 'Update', + openingId: '1541297', + statusCode: 'APP', + statusDescription: 'Approved', + lastUpdatedLabel: '1 minute ago', + lastUpdated: '2024-05-16T19:59:21.635Z' + } + ]); + }); + + }); +}); \ No newline at end of file diff --git a/frontend/src/components/DoughnutChartView/index.tsx b/frontend/src/components/DoughnutChartView/index.tsx index 07f52861..0855b351 100644 --- a/frontend/src/components/DoughnutChartView/index.tsx +++ b/frontend/src/components/DoughnutChartView/index.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect, ChangeEvent, useCallback } from "react"; import { DonutChart } from "@carbon/charts-react"; import { Dropdown, DatePicker, DatePickerInput, TextInput } from "@carbon/react"; import "./DoughnutChartView.scss"; -import { IFreeGrowingChartData, fetchFreeGrowingMilestones } from "../../services/OpeningService"; +import { fetchFreeGrowingMilestones } from "../../services/OpeningService"; +import { IFreeGrowingChartData } from "../../types/OpeningTypes"; interface IDropdownItem { value: string, diff --git a/frontend/src/components/FavoriteButton/index.tsx b/frontend/src/components/FavoriteButton/index.tsx index 9d1c7513..8d8cb11a 100644 --- a/frontend/src/components/FavoriteButton/index.tsx +++ b/frontend/src/components/FavoriteButton/index.tsx @@ -8,6 +8,8 @@ interface FavoriteButtonProps { kind: string; size: string; fill: string; + favorited: boolean; + onFavoriteChange: (newStatus: boolean) => void; } /** @@ -18,6 +20,7 @@ interface FavoriteButtonProps { * @param {string} props.kind - The favourite button kind. * @param {string} props.size - The favourite button size. * @param {string} props.fill - The favourite button fill. + * @param {boolean} props.favorited - The favourite button state. * @returns {JSX.Element} The FavoriteButton element to be rendered. */ function FavoriteButton({ @@ -25,11 +28,14 @@ function FavoriteButton({ kind, size, fill, + favorited = false, + onFavoriteChange }: FavoriteButtonProps): JSX.Element { - const [isFavorite, setIsFavorite] = useState(false); + const [isFavorite, setIsFavorite] = useState(favorited); const handleClick = () => { setIsFavorite(!isFavorite); + onFavoriteChange(!isFavorite); }; const iconName = isFavorite ? 'FavoriteFilled' : 'Favorite'; @@ -39,7 +45,6 @@ function FavoriteButton({ if (!Icon) { return
Invalid icon name
; } - const CustomIcon = () => ; return ( diff --git a/frontend/src/components/OpeningHistory/index.tsx b/frontend/src/components/OpeningHistory/index.tsx index 6dc6d22e..cad8b441 100644 --- a/frontend/src/components/OpeningHistory/index.tsx +++ b/frontend/src/components/OpeningHistory/index.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { ProgressIndicator, ProgressStep @@ -10,10 +8,22 @@ import statusClass from '../../utils/HistoryStatus'; import FavoriteButton from '../FavoriteButton'; import './styles.scss'; +import { deleteOpeningFavorite } from '../../services/OpeningFavoriteService'; + interface OpeningHistoryProps { histories: History[]; } +const handleFavoriteChange = async (newStatus: boolean, openingId: number) => { + try { + if(!newStatus){ + await deleteOpeningFavorite(openingId); + } + } catch (error) { + console.error(`Failed to update favorite status for ${openingId}`); + } +}; + const OpeningHistory = ({ histories }: OpeningHistoryProps) => (
@@ -28,6 +38,8 @@ const OpeningHistory = ({ histories }: OpeningHistoryProps) => ( kind="ghost" size="sm" fill="#0073E6" + favorited={true} + onFavoriteChange={(newStatus: boolean) => handleFavoriteChange(newStatus, history.id)} />
{`Opening Id ${history.id}`} diff --git a/frontend/src/components/OpeningMetricsTab/index.tsx b/frontend/src/components/OpeningMetricsTab/index.tsx index 49203529..44282295 100644 --- a/frontend/src/components/OpeningMetricsTab/index.tsx +++ b/frontend/src/components/OpeningMetricsTab/index.tsx @@ -1,24 +1,35 @@ -import React, { useRef, useEffect } from "react"; +import React, { useRef, useEffect, useState } from "react"; import './styles.scss'; import SectionTitle from "../SectionTitle"; import BarChartGrouped from "../BarChartGrouped"; import ChartContainer from "../ChartContainer"; import DoughnutChartView from "../DoughnutChartView"; import OpeningHistory from "../OpeningHistory"; -import OpeningHistoryItems from "../../mock-data/OpeningHistoryItems"; +import History from "../../types/History"; import MyRecentActions from "../MyRecentActions"; +import { fetchOpeningTrends } from "../../services/OpeningFavoriteService"; const OpeningMetricsTab: React.FC = () => { const trackOpeningRef = useRef(null); + const [submissionTrends, setSubmissionTrends] = useState([]); // Optional: Scroll to "Track Openings" when this component mounts useEffect(() => { + const params = new URLSearchParams(window.location.search); const scrollToSection = params.get('scrollTo'); if (scrollToSection === 'trackOpenings' && trackOpeningRef.current) { trackOpeningRef.current.scrollIntoView({ behavior: "smooth" }); } + + const loadTrends = async () => { + const response = await fetchOpeningTrends(); + setSubmissionTrends(response.map(item => ({ id: item, steps: [] }))); + }; + + loadTrends(); + }, []); return ( @@ -39,7 +50,7 @@ const OpeningMetricsTab: React.FC = () => {
{/* Add ref here to scroll */}
diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx index 51ecd176..af20ec9a 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx @@ -42,7 +42,7 @@ import { } from "../../../../utils/fileConversions"; import { Tooltip } from "@carbon/react"; import { useNavigate } from "react-router-dom"; -import { usePutViewedOpening } from "../../../../services/queries/dashboard/dashboardQueries"; +import { setOpeningFavorite } from '../../../../services/OpeningFavoriteService'; interface ISearchScreenDataTable { rows: OpeningsSearch[]; @@ -86,34 +86,26 @@ const SearchScreenDataTable: React.FC = ({ }, [rows, totalItems]); // Function to handle row selection changes - const handleRowSelectionChanged = (rowId: string) => { + const handleRowSelectionChanged = (openingId: string) => { setSelectedRows((prevSelectedRows) => { - if (prevSelectedRows.includes(rowId)) { + if (prevSelectedRows.includes(openingId)) { // If the row is already selected, remove it from the selected rows - return prevSelectedRows.filter((id) => id !== rowId); + return prevSelectedRows.filter((id) => id !== openingId); } else { // If the row is not selected, add it to the selected rows - return [...prevSelectedRows, rowId]; - } - }); - }; - - const handleRowClick = (openingId: string) => { - // Call the mutation to mark as viewed - markAsViewedOpening(openingId, { - onSuccess: () => { - // setToastText(`Successfully marked opening ${openingId} as viewed.`); - }, - onError: (err: any) => { - // setToastText(`Failed to mark as viewed: ${err.message}`); + return [...prevSelectedRows, openingId]; } }); }; //Function to handle the favourite feature of the opening for a user - const handleFavouriteOpening = (rowId: string) => { - //make a call to the api for the favourite opening when ready - setToastText(`Following "OpeningID ${rowId}"`); + const handleFavouriteOpening = (openingId: string) => { + try{ + setOpeningFavorite(parseInt(openingId)); + setToastText(`Following "OpeningID ${openingId}"`); + } catch (error) { + console.error(`Failed to update favorite status for ${openingId}`); + } } return ( diff --git a/frontend/src/mock-data/OpeningHistoryItems.ts b/frontend/src/mock-data/OpeningHistoryItems.ts deleted file mode 100644 index 1aeff650..00000000 --- a/frontend/src/mock-data/OpeningHistoryItems.ts +++ /dev/null @@ -1,111 +0,0 @@ -import History from "../types/History"; - -const OpeningHistoryItems: History[] = [ - { - id: 1541297, - steps: [ - { - step: 5, - status: 'invalid', - description: 'Milestone overdue', - subtitle: 'Please, update the milestone' - }, - { - step: 4, - status: 'complete', - description: 'Activity completed', - subtitle: '2023-11-15' - }, - { - step: 3, - status: 'complete', - description: 'Forest cover polygon', - subtitle: '2023-11-02' - }, - { - step: 2, - status: 'complete', - description: 'Disturbance activity', - subtitle: '2023-10-30' - }, - { - step: 1, - status: 'complete', - description: 'Opening ID', - subtitle: '2023-10-18' - } - ] - }, - { - id: 1541298, - steps: [ - { - step: 5, - status: 'complete', - description: 'Forest Cover', - subtitle: 'Now' - }, - { - step: 4, - status: 'complete', - description: 'Activity Planned', - subtitle: 'Now' - }, - { - step: 3, - status: 'complete', - description: 'Forest cover polygon', - subtitle: '2023-10-31' - }, - { - step: 2, - status: 'complete', - description: 'Disturbance Activity', - subtitle: '2023-10-19' - }, - { - step: 1, - status: 'complete', - description: 'Opening ID', - subtitle: '2023-10-01' - } - ] - }, - { - id: 1541299, - steps: [ - { - step: 5, - status: 'invalid', - description: 'Forest cover polygon', - subtitle: 'Please update the forest cover' - }, - { - step: 4, - status: 'invalid', - description: 'Forest cover polygon', - subtitle: 'PLease update the forest cover' - }, - { - step: 3, - status: 'complete', - description: 'Forest cover polygon', - subtitle: '2023-11-01' - }, - { - step: 2, - status: 'complete', - description: 'Disturbance Activity', - subtitle: '2023-10-29' - }, - { - step: 1, - status: 'complete', - description: 'Opening ID', - subtitle: '2023-10-17' - } - ] - } -]; - -export default OpeningHistoryItems; \ No newline at end of file diff --git a/frontend/src/screens/Opening/index.tsx b/frontend/src/screens/Opening/index.tsx index 131ee0fc..b5220d72 100644 --- a/frontend/src/screens/Opening/index.tsx +++ b/frontend/src/screens/Opening/index.tsx @@ -12,6 +12,11 @@ import OpeningMetricsTab from "../../components/OpeningMetricsTab"; const Opening: React.FC = () => { const [showSpatial, setShowSpatial] = useState(false); + const [activeTab, setActiveTab] = useState(0); // Track active tab index + + const tabChange = (tabSelection:{selectedIndex: number}) => { + setActiveTab(tabSelection.selectedIndex); + }; useEffect(() => { // @@ -74,20 +79,20 @@ const Opening: React.FC = () => {
)} - +
Recent Openings
Dashboard
- + - - + + {activeTab === 1 && }
diff --git a/frontend/src/services/OpeningFavoriteService.ts b/frontend/src/services/OpeningFavoriteService.ts new file mode 100644 index 00000000..5c85d5e3 --- /dev/null +++ b/frontend/src/services/OpeningFavoriteService.ts @@ -0,0 +1,73 @@ +import axios from 'axios'; +import { getAuthIdToken } from './AuthService'; +import { env } from '../env'; + +const backendUrl = env.VITE_BACKEND_URL; + +/** + * Fetches the submission trends/favorites from the backend. + * + * This function sends a GET request to the backend API to retrieve the user favorite openings. + * It includes an authorization token in the request headers. + * + * @returns {Promise} A promise that resolves to an array of numbers representing the opening ids. + * If the response data is not an array, it returns an empty array. + */ +export const fetchOpeningTrends = async (): Promise =>{ + const authToken = getAuthIdToken(); + const response = await axios.get( + `${backendUrl}/api/openings/favorites`, { + headers: { + Authorization: `Bearer ${authToken}` + } + }); + + const { data } = response; + if (data && Array.isArray(data)) { + return data; + } else { + return []; + } +} + +/** + * Sets an opening as a favorite for the authenticated user. + * + * @param {number} openingId - The ID of the opening to be set as favorite. + * @returns {Promise} A promise that resolves when the operation is complete. + * @throws {Error} Throws an error if the request fails with a status code other than 202. + */ +export const setOpeningFavorite = async (openingId: number): Promise => { + const authToken = getAuthIdToken(); + const response = await axios.put( + `${backendUrl}/api/openings/favorites/${openingId}`, null, { + headers: { + Authorization: `Bearer ${authToken}` + } + }); + + if (response.status !== 202) { + throw new Error(`Failed to set favorite opening. Status code: ${response.status}`); + } +} + +/** + * Deletes a favorite opening by its ID. + * + * @param {number} openingId - The ID of the opening to be removed from favorites. + * @returns {Promise} A promise that resolves when the favorite opening is successfully deleted. + * @throws {Error} Throws an error if the deletion fails or the response status is not 204. + */ +export const deleteOpeningFavorite = async (openingId: number): Promise => { + const authToken = getAuthIdToken(); + const response = await axios.delete( + `${backendUrl}/api/openings/favorites/${openingId}`, { + headers: { + Authorization: `Bearer ${authToken}` + } + }); + + if (response.status !== 204) { + throw new Error(`Failed to remove favorite opening. Status code: ${response.status}`); + } +} \ No newline at end of file diff --git a/frontend/src/services/OpeningService.ts b/frontend/src/services/OpeningService.ts index 9677e62c..3ae05474 100644 --- a/frontend/src/services/OpeningService.ts +++ b/frontend/src/services/OpeningService.ts @@ -4,27 +4,57 @@ import { env } from '../env'; import { RecentAction } from '../types/RecentAction'; import { OpeningPerYearChart } from '../types/OpeningPerYearChart'; import { RecentOpening } from '../types/RecentOpening'; -import { IOpeningPerYear } from '../types/IOpeningPerYear'; +import { + RecentOpeningApi, + IOpeningPerYear, + IFreeGrowingProps, + IFreeGrowingChartData +} from '../types/OpeningTypes'; const backendUrl = env.VITE_BACKEND_URL; -interface statusCategory { - code: string; - description: string; -} +/** + * Fetch recent openings data from backend. + * + * @returns {Promise} Array of objects found + */ +export async function fetchRecentOpenings(): Promise { + const authToken = getAuthIdToken(); + try { + const response = await axios.get(backendUrl.concat("/api/openings/recent-openings?page=0&perPage=100"), { + headers: { + Authorization: `Bearer ${authToken}` + } + }); -interface RecentOpeningApi { - openingId: number; - forestFileId: string; - cuttingPermit: string | null; - timberMark: string | null; - cutBlock: string | null; - grossAreaHa: number | null; - status: statusCategory | null; - category: statusCategory | null; - disturbanceStart: string | null; - entryTimestamp: string | null; - updateTimestamp: string | null; + if (response.status >= 200 && response.status < 300) { + const { data } = response; + + if (data.data) { + // Extracting row information from the fetched data + const rows: RecentOpening[] = data.data.map((opening: RecentOpeningApi) => ({ + id: opening.openingId.toString(), + openingId: opening.openingId.toString(), + forestFileId: opening.forestFileId ? opening.forestFileId : '-', + cuttingPermit: opening.cuttingPermit ? opening.cuttingPermit : '-', + timberMark: opening.timberMark ? opening.timberMark : '-', + cutBlock: opening.cutBlock ? opening.cutBlock : '-', + grossAreaHa: opening.grossAreaHa ? opening.grossAreaHa.toString() : '-', + status: opening.status && opening.status.description? opening.status.description : '-', + category: opening.category && opening.category.description? opening.category.description : '-', + disturbanceStart: opening.disturbanceStart ? opening.disturbanceStart : '-', + entryTimestamp: opening.entryTimestamp ? opening.entryTimestamp.split('T')[0] : '-', + updateTimestamp: opening.updateTimestamp ? opening.updateTimestamp.split('T')[0] : '-' + })); + + return rows; + } + } + return []; + } catch (error) { + console.error('Error fetching recent openings:', error); + throw error; + } } /** @@ -72,51 +102,6 @@ export async function fetchOpeningsPerYear(props: IOpeningPerYear): Promise => { - const authToken = getAuthIdToken(); - - try { - let url = `${backendUrl}/api/dashboard-metrics/submission-trends`; - if (props.orgUnitCode || props.statusCode || props.entryDateStart || props.entryDateEnd) { - url += "?"; - if (props.orgUnitCode) url += `orgUnitCode=${props.orgUnitCode}&`; - if (props.statusCode) url += `statusCode=${props.statusCode}&`; - if (props.entryDateStart) url += `entryDateStart=${props.entryDateStart}&`; - if (props.entryDateEnd) url += `entryDateEnd=${props.entryDateEnd}&`; - url = url.replace(/&$/, ""); - } - - const response = await axios.get(url, { - headers: { Authorization: `Bearer ${authToken}` } - }); - - if (response.data && Array.isArray(response.data)) { - return response.data.map(item => ({ - group: "Openings", - key: item.monthName, - value: item.amount - })); - } - - return []; - } catch (error) { - console.error("Error fetching openings per year:", error); - throw error; - } -}; - -interface IFreeGrowingProps { - orgUnitCode: string; - clientNumber: string; - entryDateStart: string | null; - entryDateEnd: string | null; -} - -export interface IFreeGrowingChartData { - group: string; - value: number; -} - /** * Fetch free growing milestones data from backend. * diff --git a/frontend/src/types/OpeningTypes.ts b/frontend/src/types/OpeningTypes.ts new file mode 100644 index 00000000..34969a73 --- /dev/null +++ b/frontend/src/types/OpeningTypes.ts @@ -0,0 +1,37 @@ +export interface StatusCategory { + code: string; + description: string; +} + +export interface RecentOpeningApi { + openingId: number; + forestFileId: string; + cuttingPermit: string | null; + timberMark: string | null; + cutBlock: string | null; + grossAreaHa: number | null; + status: StatusCategory | null; + category: StatusCategory | null; + disturbanceStart: string | null; + entryTimestamp: string | null; + updateTimestamp: string | null; +} + +export interface IOpeningPerYear { + orgUnitCode: string | null; + statusCode: string | null; + entryDateStart: string | null; + entryDateEnd: string | null; +} + +export interface IFreeGrowingProps { + orgUnitCode: string; + clientNumber: string; + entryDateStart: string | null; + entryDateEnd: string | null; +} + +export interface IFreeGrowingChartData { + group: string; + value: number; +} \ No newline at end of file From 95d2be5f4ba959a3bfee450f485cc97b88cae022 Mon Sep 17 00:00:00 2001 From: Derek Roberts Date: Wed, 6 Nov 2024 13:44:49 -0800 Subject: [PATCH 32/43] ci: use runs-on: ubuntu-latest (#445) --- .github/workflows/.deploy.yml | 195 +++++++++++++++++++++++++++++++++ .github/workflows/analysis.yml | 10 +- .github/workflows/merge.yml | 10 +- .github/workflows/pr-close.yml | 4 +- .github/workflows/pr-open.yml | 6 +- 5 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/.deploy.yml diff --git a/.github/workflows/.deploy.yml b/.github/workflows/.deploy.yml new file mode 100644 index 00000000..a5b5f2b1 --- /dev/null +++ b/.github/workflows/.deploy.yml @@ -0,0 +1,195 @@ +name: .Deploys + +on: + workflow_call: + inputs: + ### Required + # Nothing! Only `secrets: inherit` is required + + ### Typical / recommended + environment: + description: GitHub/OpenShift environment; usually PR number, test or prod + default: '' + required: false + type: string + tag: + description: Container tag; usually PR number + default: ${{ github.event.number }} + required: false + type: string + target: + description: Deployment target; usually PR number, test or prod + default: ${{ github.event.number }} + required: false + type: string + + outputs: + run_tests: + description: Run Cypress tests if the core apps have changed (excludes sync) + value: ${{ jobs.init.outputs.deploy_core }} + +jobs: + init: + name: Deploy (init) + environment: ${{ inputs.environment }} + outputs: + fam-modded-zone: ${{ steps.fam-modded-zone.outputs.fam-modded-zone }} + deploy_core: ${{ steps.triggers.outputs.core }} + deploy_sync: ${{ steps.triggers.outputs.sync }} + runs-on: ubuntu-latest + steps: + # Check triggers (omitted or matched) + - name: Check core triggers + uses: bcgov-nr/action-diff-triggers@v0.2.0 + id: check_core + with: + triggers: ('backend/' 'common/' 'database/' 'frontend/' 'oracle-api/') + + - name: Check sync triggers + uses: bcgov-nr/action-diff-triggers@v0.2.0 + id: check_sync + with: + triggers: ('common/' 'sync/') + + # Simplify triggers + - name: Simplify triggers + id: triggers + run: | + echo "core=${{ github.event_name != 'pull_request' || steps.check_core.outputs.triggered == 'true' }}" >> $GITHUB_OUTPUT + echo "sync=${{ github.event_name != 'pull_request' || steps.check_sync.outputs.triggered == 'true' }}" >> $GITHUB_OUTPUT + + - name: FAM routing + id: fam-modded-zone + if: steps.triggers.outputs.core == 'true' + run: | + if [ ${{ github.event_name }} == 'pull_request' ]; then + echo "fam-modded-zone=$(( ${{ inputs.target }} % 50 ))" >> $GITHUB_OUTPUT + else + echo "fam-modded-zone=${{ inputs.target }}" >> $GITHUB_OUTPUT + fi + + - name: OpenShift Init + if: steps.triggers.outputs.core == 'true' || steps.triggers.outputs.sync == 'true' + uses: bcgov-nr/action-deployer-openshift@v3.0.1 + with: + oc_namespace: ${{ vars.OC_NAMESPACE }} + oc_server: ${{ vars.OC_SERVER }} + oc_token: ${{ secrets.OC_TOKEN }} + file: common/openshift.init.yml + overwrite: true + parameters: + -p ZONE=${{ inputs.target }} + -p DB_PASSWORD='${{ secrets.DB_PASSWORD }}' + -p FORESTCLIENTAPI_KEY='${{ secrets.FORESTCLIENTAPI_KEY }}' + -p ORACLE_PASSWORD='${{ secrets.ORACLE_PASSWORD }}' + -p ORACLE_SERVICE='${{ vars.ORACLE_SERVICE }}' + -p ORACLE_USER='${{ vars.ORACLE_USER }}' + -p ORACLE_SYNC_USER='${{ vars.ORACLE_SYNC_USER }}' + -p ORACLE_SYNC_PASSWORD='${{ secrets.ORACLE_SYNC_PASSWORD }}' + -p ORACLE_CERT_SECRET='${{ secrets.ORACLE_CERT_SECRET }}' + -p ORACLE_HOST='${{ vars.ORACLE_HOST }}' + -p VITE_USER_POOLS_WEB_CLIENT_ID=${{ secrets.VITE_USER_POOLS_WEB_CLIENT_ID }} + + - name: Database + if: steps.triggers.outputs.core == 'true' || steps.triggers.outputs.sync == 'true' + uses: bcgov-nr/action-deployer-openshift@v3.0.1 + with: + oc_namespace: ${{ vars.OC_NAMESPACE }} + oc_server: ${{ vars.OC_SERVER }} + oc_token: ${{ secrets.OC_TOKEN }} + file: common/openshift.database.yml + overwrite: false + parameters: + -p ZONE=${{ inputs.target }} + ${{ github.event_name == 'pull_request' && '-p DB_PVC_SIZE=192Mi' || '' }} + ${{ github.event_name == 'pull_request' && '-p MEMORY_REQUEST=100Mi' || '' }} + ${{ github.event_name == 'pull_request' && '-p MEMORY_LIMIT=200Mi' || '' }} + + deploy: + name: Deploy + environment: ${{ inputs.environment }} + if: needs.init.outputs.deploy_core == 'true' + needs: [init] + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + name: [backend, frontend, oracle-api] + include: + - name: backend + file: backend/openshift.deploy.yml + overwrite: true + parameters: + -p AWS_COGNITO_ISSUER_URI=https://cognito-idp.ca-central-1.amazonaws.com/${{ vars.VITE_USER_POOLS_ID }} + verification_path: "health" + - name: frontend + file: frontend/openshift.deploy.yml + overwrite: true + parameters: + -p FAM_MODDED_ZONE=${{ needs.init.outputs.fam-modded-zone }} + -p VITE_SPAR_BUILD_VERSION=snapshot-${{ inputs.target || github.event.number }} + -p VITE_USER_POOLS_ID=${{ vars.VITE_USER_POOLS_ID }} + - name: oracle-api + file: oracle-api/openshift.deploy.yml + overwrite: true + parameters: + -p AWS_COGNITO_ISSUER_URI=https://cognito-idp.ca-central-1.amazonaws.com/${{ vars.VITE_USER_POOLS_ID }} + ${{ github.event_name == 'pull_request' && '-p CPU_LIMIT=100m' || '' }} + ${{ inputs.target == 'prod' && '-p MIN_REPLICAS=3' || '' }} + ${{ inputs.target == 'prod' && '-p MAX_REPLICAS=5' || '' }} + verification_path: "actuator/health" + + steps: + - uses: bcgov-nr/action-deployer-openshift@v3.0.1 + id: deploys + with: + file: ${{ matrix.file }} + oc_namespace: ${{ vars.OC_NAMESPACE }} + oc_server: ${{ vars.OC_SERVER }} + oc_token: ${{ secrets.OC_TOKEN }} + overwrite: ${{ matrix.overwrite }} + parameters: + -p TAG=${{ inputs.tag }} + -p ZONE=${{ inputs.target }} + ${{ github.event_name == 'pull_request' && '-p MIN_REPLICAS=1' || '' }} + ${{ github.event_name == 'pull_request' && '-p MAX_REPLICAS=1' || '' }} + ${{ matrix.parameters }} + verification_path: ${{ matrix.verification_path }} + verification_retry_attempts: 5 + verification_retry_seconds: 20 + + # ETL testing will only run on Pull Requests if the sync/ directory is modified + sync: + name: Deploy (sync) + environment: ${{ inputs.environment }} + if: needs.init.outputs.deploy_sync == 'true' + needs: [init] + runs-on: ubuntu-latest + steps: + - name: Deploy (sync) + uses: bcgov-nr/action-deployer-openshift@v3.0.1 + with: + file: sync/openshift.deploy.yml + oc_namespace: ${{ vars.OC_NAMESPACE }} + oc_server: ${{ vars.OC_SERVER }} + oc_token: ${{ secrets.OC_TOKEN }} + overwrite: true + parameters: + -p TAG=${{ inputs.tag }} + -p ZONE=${{ inputs.target }} + ${{ github.event_name == 'pull_request' && '-p TEST_MODE=true' || '' }} + + + - name: Override OpenShift version + if: github.event_name == 'pull_request' + env: + OC: https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable-4.13/openshift-client-linux.tar.gz + run: | + # Download and extract with retry, continuing on error + (wget ${{ env.OC }} -qcO - | tar -xzvf - oc)|| !! || true + oc version + working-directory: /usr/local/bin/ + + - name: Run sync ETL + if: github.event_name == 'pull_request' + run: ./sync/oc_run.sh ${{ inputs.tag }} ${{ secrets.oc_token }} diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 0a79ef2c..4b00749f 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -15,7 +15,7 @@ jobs: tests-backend: name: Tests (Backend) if: github.event_name != 'pull_request' || !github.event.pull_request.draft - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: bcgov-nr/action-test-and-analyse-java@v1.0.2 with: @@ -37,7 +37,7 @@ jobs: lint-frontend: name: Lint (Frontend) if: github.event_name != 'pull_request' || !github.event.pull_request.draft - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: bcgov-nr/action-test-and-analyse@v1.2.1 with: @@ -52,7 +52,7 @@ jobs: tests-frontend: name: Tests (Frontend) if: github.event_name != 'pull_request' || !github.event.pull_request.draft - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: bcgov-nr/action-test-and-analyse@v1.2.1 env: @@ -83,7 +83,7 @@ jobs: trivy: name: Trivy Security Scan if: github.event_name != 'pull_request' || !github.event.pull_request.draft - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -107,6 +107,6 @@ jobs: name: Analysis Results if: always() && (!failure()) && (!cancelled()) needs: [lint-frontend, tests-backend, tests-frontend] # Include trivy when/if it gets back to being reliable - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - run: echo "Workflow completed successfully!" diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index b0f39c68..c6942fb3 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -14,7 +14,7 @@ jobs: init-test: name: TEST Init environment: test - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - name: OpenShift Init uses: bcgov-nr/action-deployer-openshift@v3.0.1 @@ -41,7 +41,7 @@ jobs: name: TEST Deployments needs: [init-test] environment: test - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest permissions: issues: write strategy: @@ -87,7 +87,7 @@ jobs: name: PROD Init needs: [deploys-test] environment: prod - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - name: OpenShift Init uses: bcgov-nr/action-deployer-openshift@v3.0.1 @@ -113,7 +113,7 @@ jobs: image-promotions: name: Promote images to PROD needs: [deploys-test] - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest permissions: packages: write strategy: @@ -131,7 +131,7 @@ jobs: name: PROD Deployments needs: [init-prod, image-promotions] environment: prod - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest strategy: matrix: name: [database, backend, frontend, fluentbit] diff --git a/.github/workflows/pr-close.yml b/.github/workflows/pr-close.yml index d6695786..3ad1673f 100644 --- a/.github/workflows/pr-close.yml +++ b/.github/workflows/pr-close.yml @@ -15,7 +15,7 @@ jobs: cleanup-openshift: name: Cleanup OpenShift if: '!github.event.pull_request.head.repo.fork' - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: redhat-actions/openshift-tools-installer@v1 with: @@ -32,7 +32,7 @@ jobs: image-promotions: name: Image Promotions if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest strategy: matrix: package: [database, backend, frontend] diff --git a/.github/workflows/pr-open.yml b/.github/workflows/pr-open.yml index b7ccb11d..7cea2962 100644 --- a/.github/workflows/pr-open.yml +++ b/.github/workflows/pr-open.yml @@ -17,7 +17,7 @@ jobs: if: "!github.event.pull_request.head.repo.fork" outputs: route: ${{ github.event.number }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest permissions: pull-requests: write steps: @@ -70,7 +70,7 @@ jobs: builds: name: Builds - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest permissions: packages: write strategy: @@ -96,7 +96,7 @@ jobs: deploys: name: Deploys needs: [builds, init] - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest strategy: matrix: name: [database, backend, frontend, fluentbit] From 32c6eda6b73b236bff7a88ad2be3462a990cf61a Mon Sep 17 00:00:00 2001 From: Jazz Grewal <39718912+jazzgrewal@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:59:31 -0800 Subject: [PATCH 33/43] fix(SILVA-550): Enhance Org Unit and Category Selection with Filterable Multi-Select Dropdown (#442) Co-authored-by: Paulo Gomes da Cruz Junior Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../oracle/dto/OpeningSearchFiltersDto.java | 26 +++-- .../endpoint/OpeningSearchEndpoint.java | 4 +- .../repository/OpeningSearchRepository.java | 18 +-- .../OpeningSearchRepositoryTest.java | 18 +-- .../oracle/service/OpeningServiceTest.java | 4 +- .../Openings/OpeningSearchBar.test.tsx | 70 ++++++++++- .../__test__/contexts/OpeningsSearch.test.tsx | 56 +++++++++ .../services/search/openings.test.tsx | 109 ++++++++++++++++++ .../AdvancedSearchDropdown.scss | 4 + .../Openings/AdvancedSearchDropdown/index.tsx | 97 +++++++++------- .../Openings/OpeningsSearchBar/index.tsx | 11 +- .../Openings/OpeningsSearchTab/index.tsx | 5 - .../src/contexts/search/OpeningsSearch.tsx | 4 +- frontend/src/services/search/openings.ts | 9 +- 14 files changed, 350 insertions(+), 85 deletions(-) create mode 100644 frontend/src/__test__/contexts/OpeningsSearch.test.tsx create mode 100644 frontend/src/__test__/services/search/openings.test.tsx diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java index 964e2553..0753785e 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java @@ -14,8 +14,8 @@ @Getter @ToString public class OpeningSearchFiltersDto { - private final String orgUnit; - private final String category; + private final List orgUnit; + private final List category; private final List statusList; private final Boolean myOpenings; private final Boolean submittedToFrpa; @@ -39,8 +39,8 @@ public class OpeningSearchFiltersDto { /** Creates an instance of the search opening filter dto. */ public OpeningSearchFiltersDto( - String orgUnit, - String category, + List orgUnit, + List category, List statusList, Boolean myOpenings, Boolean submittedToFrpa, @@ -56,8 +56,18 @@ public OpeningSearchFiltersDto( String cutBlockId, String timberMark, String mainSearchTerm) { - this.orgUnit = Objects.isNull(orgUnit) ? null : orgUnit.toUpperCase().trim(); - this.category = Objects.isNull(category) ? null : category.toUpperCase().trim(); + this.orgUnit = new ArrayList<>(); + if (!Objects.isNull(orgUnit)) { + this.orgUnit.addAll(orgUnit.stream() + .map(s -> String.format("'%s'", s.toUpperCase().trim())) + .toList()); + } + this.category = new ArrayList<>(); + if (!Objects.isNull(category)) { + this.category.addAll(category.stream() + .map(s -> String.format("'%s'", s.toUpperCase().trim())) + .toList()); + } this.statusList = new ArrayList<>(); this.openingIds = new ArrayList<>(); if (!Objects.isNull(statusList)) { @@ -114,8 +124,8 @@ public OpeningSearchFiltersDto( */ public boolean hasValue(String prop) { return switch (prop) { - case SilvaOracleConstants.ORG_UNIT -> !Objects.isNull(this.orgUnit); - case SilvaOracleConstants.CATEGORY -> !Objects.isNull(this.category); + case SilvaOracleConstants.ORG_UNIT -> !this.orgUnit.isEmpty(); + case SilvaOracleConstants.CATEGORY -> !this.category.isEmpty(); case SilvaOracleConstants.STATUS_LIST -> !this.statusList.isEmpty(); case SilvaOracleConstants.OPENING_IDS -> !this.openingIds.isEmpty(); case SilvaOracleConstants.MY_OPENINGS -> !Objects.isNull(this.myOpenings); diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningSearchEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningSearchEndpoint.java index 8a90bdbb..69f316ca 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningSearchEndpoint.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningSearchEndpoint.java @@ -63,9 +63,9 @@ public PaginatedResult openingSearch( @RequestParam(value = "mainSearchTerm", required = false) String mainSearchTerm, @RequestParam(value = "orgUnit", required = false) - String orgUnit, + List orgUnit, @RequestParam(value = "category", required = false) - String category, + List category, @RequestParam(value = "statusList", required = false) List statusList, @RequestParam(value = "myOpenings", required = false) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java index 0a6feb71..25848e08 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java @@ -254,13 +254,13 @@ private Query setQueryParameters(OpeningSearchFiltersDto filtersDto, String nati // 1. Org Unit code if (filtersDto.hasValue(SilvaOracleConstants.ORG_UNIT)) { - log.info("Setting orgUnit filter value"); - query.setParameter("orgUnit", filtersDto.getOrgUnit()); + log.info("Setting orgUnit filter values"); + // No need to set value since the query already dit it. Didn't work set through named param } // 2. Category code if (filtersDto.hasValue(SilvaOracleConstants.CATEGORY)) { - log.info("Setting category filter value"); - query.setParameter("category", filtersDto.getCategory()); + log.info("Setting category filter values"); + // No need to set value since the query already dit it. Didn't work set through named param } // 3. Status list codes if (filtersDto.hasValue(SilvaOracleConstants.STATUS_LIST)) { @@ -427,13 +427,15 @@ private String createNativeSqlQuery(OpeningSearchFiltersDto filtersDto) { // 1. Org Unit code if (filtersDto.hasValue(SilvaOracleConstants.ORG_UNIT)) { - log.info("Filter orgUnit detected! orgUnit={}", filtersDto.getOrgUnit()); - builder.append("AND ou.ORG_UNIT_CODE = :orgUnit "); + String orgUnits = String.join(",", filtersDto.getOrgUnit()); + log.info("Filter orgUnit detected! orgUnit={}", orgUnits); + builder.append(String.format("AND ou.ORG_UNIT_CODE IN (%s) ", orgUnits)); } // 2. Category code if (filtersDto.hasValue(SilvaOracleConstants.CATEGORY)) { - log.info("Filter category detected! category={}", filtersDto.getCategory()); - builder.append("AND o.OPEN_CATEGORY_CODE = :category "); + String categories = String.join(",", filtersDto.getCategory()); + log.info("Filter category detected! statusList={}", categories); + builder.append(String.format("AND o.OPEN_CATEGORY_CODE IN (%s) ", categories)); } // 3. Status code if (filtersDto.hasValue(SilvaOracleConstants.STATUS_LIST)) { diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepositoryTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepositoryTest.java index 2e1c6a0f..6a629696 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepositoryTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepositoryTest.java @@ -42,8 +42,8 @@ class OpeningSearchRepositoryTest { private OpeningSearchRepository openingSearchRepository; private OpeningSearchFiltersDto mockFilter( - String orgUnit, - String category, + List orgUnit, + List category, List statusList, Boolean myOpenings, Boolean submittedToFrpa, @@ -79,7 +79,7 @@ private OpeningSearchFiltersDto mockFilter( mainSearchTerm); } - private OpeningSearchFiltersDto mockOrgUnit(String orgUnit) { + private OpeningSearchFiltersDto mockOrgUnit(List orgUnit) { return mockFilter( orgUnit, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); @@ -87,8 +87,8 @@ private OpeningSearchFiltersDto mockOrgUnit(String orgUnit) { private OpeningSearchFiltersDto mockAllFilters() { return mockFilter( - "DCR", - "FTML", + List.of("DCR"), + List.of("FTML"), List.of("APP"), true, false, @@ -481,7 +481,7 @@ void searchOpeningQuery_mainFilterString_shouldSucceed() { @Test @DisplayName("Search opening query org unit filter should succeed") void searchOpeningQuery_orgUnitFilter_shouldSucceed() { - OpeningSearchFiltersDto filters = mockOrgUnit("DCR"); + OpeningSearchFiltersDto filters = mockOrgUnit(List.of("DCR")); PaginationParameters pagination = new PaginationParameters(0, 10); @@ -575,7 +575,7 @@ void searchOpeningQuery_allFilters_shouldSucceed() { Integer openingId = 123456789; String openingNumber = "589"; - OpeningCategoryEnum category = OpeningCategoryEnum.of(filters.getCategory()); + OpeningCategoryEnum category = OpeningCategoryEnum.of("FTML"); OpeningStatusEnum status = OpeningStatusEnum.of(filters.getStatusList().get(0)); String cuttingPermitId = "123"; String timberMark = "EM2184"; @@ -583,7 +583,7 @@ void searchOpeningQuery_allFilters_shouldSucceed() { BigDecimal openingGrossArea = new BigDecimal("11"); Timestamp disturbanceStartDate = Timestamp.valueOf(LocalDateTime.now()); String forestFileId = "TFL47"; - String orgUnitCode = filters.getOrgUnit(); + String orgUnitCode = "DCR"; String orgUnitName = "Org Name"; String clientNumber = "00012797"; String clientLocation = "00"; @@ -655,7 +655,7 @@ void searchOpeningQuery_allFilters_shouldSucceed() { @Test @DisplayName("Search opening query no records found should succeed") void searchOpeningQuery_noRecordsFound_shouldSucceed() { - OpeningSearchFiltersDto filters = mockOrgUnit("AAA"); + OpeningSearchFiltersDto filters = mockOrgUnit(List.of("AAA")); PaginationParameters pagination = new PaginationParameters(0, 10); diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java index e2db4921..055103f2 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java @@ -16,6 +16,8 @@ import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.List; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -106,7 +108,7 @@ void openingSearch_orgUnit_shouldSucceed() { PaginatedResult result = openingService.openingSearch(new OpeningSearchFiltersDto( - "TWO", + List.of("TWO"), null, null, null, diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx index 3f99fe56..8044d8d6 100644 --- a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx +++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx @@ -6,7 +6,7 @@ import "@testing-library/jest-dom"; import OpeningsSearchBar from "../../../../components/SilvicultureSearch/Openings/OpeningsSearchBar"; import { vi } from "vitest"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; +import { OpeningsSearchProvider, useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; // Mock the useOpeningsSearch context to avoid rendering errors vi.mock("../../../../contexts/search/OpeningsSearch", () => ({ @@ -53,4 +53,72 @@ describe("OpeningsSearchBar", () => { // Check if the onSearchClick function was called expect(onSearchClick).toHaveBeenCalled(); }); + + it("should show AdvancedSearchDropdown if isOpen is true", () => { + // Create a mock function to pass as a prop + const onSearchClick = vi.fn(); + const isOpen = false; + // Mock the useState calls + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [true, vi.fn()]) // Mocking isOpen state as false + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false + .mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state + .mockImplementationOnce(() => [0, vi.fn()]) // Mocking filtersCount state + .mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state + render( + + + + ); + + // Check if an element with the class 'd-none' exists within the structure + const dNoneElement = screen.getAllByText("", {selector: ".d-block"})[0]; + expect(dNoneElement).toBeInTheDocument(); + }); + + it("should not show AdvancedSearchDropdown if isOpen is false", () => { + // Create a mock function to pass as a prop + const onSearchClick = vi.fn(); + const isOpen = false; + // Mock the useState calls + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking isOpen state as false + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false + .mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state + .mockImplementationOnce(() => [0, vi.fn()]) // Mocking filtersCount state + .mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state + render( + + + + ); + + // Check if an element with the class 'd-none' exists within the structure + const dNoneElement = screen.getAllByText("", {selector: ".d-none"})[0]; + expect(dNoneElement).toBeInTheDocument(); + }); + + it("should show correct filter count, when count is greater than 0", () => { + // Create a mock function to pass as a prop + const onSearchClick = vi.fn(); + // Mock the useState calls + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking isOpen state as false + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false + .mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state + .mockImplementationOnce(() => [2, vi.fn()]) // Mocking filtersCount state + .mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state + + render( + + + + ); + + console.log(screen.debug()); + + // Check if an element with the class 'd-none' exists within the structure + const dNoneElement = screen.getByText('+2'); + expect(dNoneElement).toBeInTheDocument(); + }); }); \ No newline at end of file diff --git a/frontend/src/__test__/contexts/OpeningsSearch.test.tsx b/frontend/src/__test__/contexts/OpeningsSearch.test.tsx new file mode 100644 index 00000000..810652a8 --- /dev/null +++ b/frontend/src/__test__/contexts/OpeningsSearch.test.tsx @@ -0,0 +1,56 @@ +// OpeningsSearchProvider.test.tsx +import React from 'react'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { OpeningsSearchProvider, useOpeningsSearch } from '../../contexts/search/OpeningsSearch'; + +const TestComponent: React.FC = () => { + const { filters, setFilters, searchTerm, setSearchTerm, clearFilters, clearIndividualField } = useOpeningsSearch(); + + return ( +
+

{searchTerm}

+

{String(filters.startDate)}

+ + + + +
+ ); +}; + +describe('OpeningsSearchProvider', () => { + beforeEach(() => { + render( + + + + ); + }); + + it('should initialize with default values', () => { + expect(screen.getByTestId('searchTerm').textContent).toBe(''); + expect(screen.getByTestId('startDate').textContent).toBe('null'); + }); + + it('should update searchTerm', () => { + fireEvent.click(screen.getByTestId('setSearchTerm')); + expect(screen.getByTestId('searchTerm').textContent).toBe('test search'); + }); + + it('should set and then clear filters', () => { + fireEvent.click(screen.getByTestId('setFilters')); + expect(screen.getByTestId('startDate').textContent).not.toBe('null'); + + fireEvent.click(screen.getByTestId('clearFilters')); + expect(screen.getByTestId('startDate').textContent).toBe('null'); + }); + + it('should clear individual field', () => { + fireEvent.click(screen.getByTestId('setFilters')); + expect(screen.getByTestId('startDate').textContent).not.toBe('null'); + + fireEvent.click(screen.getByTestId('clearStartDate')); + expect(screen.getByTestId('startDate').textContent).toBe('null'); + }); +}); diff --git a/frontend/src/__test__/services/search/openings.test.tsx b/frontend/src/__test__/services/search/openings.test.tsx new file mode 100644 index 00000000..6ddbf33e --- /dev/null +++ b/frontend/src/__test__/services/search/openings.test.tsx @@ -0,0 +1,109 @@ +// fetchOpenings.test.ts +import axios from "axios"; +import "@testing-library/jest-dom"; +import { fetchOpenings, OpeningFilters } from "../../../services/search/openings"; +import { getAuthIdToken } from "../../../services/AuthService"; +import { createDateParams } from "../../../utils/searchUtils"; +import { describe, it, beforeEach, afterEach, vi, expect } from "vitest"; + +// Mock dependencies +vi.mock("axios"); +vi.mock("../../../services/AuthService"); +vi.mock("../../../utils/searchUtils"); + +// Define mocked functions and modules +const mockedAxios = axios as vi.Mocked; +const mockedGetAuthIdToken = getAuthIdToken as vi.Mock; +const mockedCreateDateParams = createDateParams as vi.Mock; + +// Sample filters +const sampleFilters: OpeningFilters = { + searchInput: "", + startDate: "2024-11-19", + endDate: "2024-11-21", + orgUnit: ["DCC", "DCK", "DCR"], + category: ["EXCLU", "CONT"], + status: ["DFT", "APP"], + clientAcronym: "12", + blockStatus: "", + cutBlock: "L", + cuttingPermit: "PC", + timberMark: "123", + dateType: "Disturbance", + openingFilters: ["Openings created by me", "Submitted to FRPA section 108"], + blockStatuses: [], + page: 1, + perPage: 5, +}; + +// Mock response from the backend API +const mockApiResponse = { + data: { + pageIndex: 0, + perPage: 5, + totalPages: 100, + hasNextPage: false, + data: [ + { + openingId: 9100129, + openingNumber: "98", + cuttingPermitId: "S", + timberMark: "W1729S", + cutBlockId: "06-03", + orgUnitCode: "DPG", + orgUnitName: "Prince George Natural Resource District", + entryUserId: "Datafix107808", + statusCode: "APP", + statusDescription: "Approved", + categoryCode: "FTML", + categoryDescription: "Forest Tenure - Major Licensee", + }, + ], + }, +}; + +describe("fetchOpenings", () => { + beforeEach(() => { + mockedGetAuthIdToken.mockReturnValue("mocked-token"); + mockedCreateDateParams.mockReturnValue({ + dateStartKey: "disturbanceStartDate", + dateEndKey: "disturbanceEndDate", + }); + mockedAxios.get.mockResolvedValue(mockApiResponse); // Mock API response + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should fetch openings with the correct parameters and return flattened data", async () => { + const result = await fetchOpenings(sampleFilters); + const expectedToken = 'mocked-token'; + // Verify that axios was called with the correct URL and headers + expect(mockedAxios.get).toHaveBeenCalledWith( + expect.stringContaining("/api/opening-search?"), + expect.objectContaining({ + headers: { + Authorization: `Bearer ${expectedToken}`, + }, + }) + ); + + // Check if the result data matches the expected flattened structure + expect(result.data[0].openingId).toEqual(9100129); + }); + + it("should handle an empty response data array gracefully", async () => { + mockedAxios.get.mockResolvedValueOnce({ data: { data: [] } }); + const result = await fetchOpenings(sampleFilters); + + // Ensure the function returns an empty array when the response is empty + expect(result.data).toEqual([]); + }); + + it("should throw an error when the API request fails", async () => { + mockedAxios.get.mockRejectedValueOnce(new Error("Network error")); + + await expect(fetchOpenings(sampleFilters)).rejects.toThrow("Network error"); + }); +}); diff --git a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/AdvancedSearchDropdown.scss b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/AdvancedSearchDropdown.scss index 5f40c7c6..15e8ef86 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/AdvancedSearchDropdown.scss +++ b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/AdvancedSearchDropdown.scss @@ -15,6 +15,10 @@ background-color: var(--bx-field-01); border-bottom: 1px solid var(--bx-border-strong-01); } + .multi-select .bx--list-box__field--wrapper{ + background-color: var(--bx-field-01); + border-bottom: 1px solid var(--bx-border-strong-01); + } } diff --git a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx index 8822d0ee..c374ddbd 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { Checkbox, CheckboxGroup, @@ -17,7 +17,7 @@ import "./AdvancedSearchDropdown.scss"; import * as Icons from "@carbon/icons-react"; import { useOpeningFiltersQuery } from "../../../../services/queries/search/openingQueries"; import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; -import { color } from "@carbon/charts"; +import { FilterableMultiSelect } from "@carbon/react"; interface AdvancedSearchDropdownProps { toggleShowFilters: () => void; // Function to be passed as a prop @@ -28,11 +28,49 @@ const AdvancedSearchDropdown: React.FC = ({ const { filters, setFilters, clearFilters } = useOpeningsSearch(); const { data, isLoading, isError } = useOpeningFiltersQuery(); + // Initialize selected items for OrgUnit MultiSelect based on existing filters + const [selectedOrgUnits, setSelectedOrgUnits] = useState([]); + // Initialize selected items for category MultiSelect based on existing filters + const [selectedCategories, setSelectedCategories] = useState([]); + + + useEffect(() => { + // Split filters.orgUnit into array and format as needed for selectedItems + if (filters.orgUnit) { + const orgUnitsArray = filters.orgUnit.map((orgUnit: String) => ({ + text: orgUnit, + value: orgUnit, + })); + setSelectedOrgUnits(orgUnitsArray); + } else { + setSelectedOrgUnits([]); + } + // Split filters.category into array and format as needed for selectedItems + if (filters.category) { + const categoriesArray = filters.category.map((category: String) => ({ + text: category, + value: category, + })); + setSelectedCategories(categoriesArray); + } else{ + setSelectedCategories([]); + } + }, [filters.orgUnit, filters.category]); + const handleFilterChange = (updatedFilters: Partial) => { const newFilters = { ...filters, ...updatedFilters }; setFilters(newFilters); }; + const handleMultiSelectChange = (group: string, selectedItems: any) => { + const updatedGroup = selectedItems.map((item: any) => item.value); + if (group === "orgUnit") + setSelectedOrgUnits(updatedGroup); + if (group === "category") + setSelectedCategories(updatedGroup); + handleFilterChange({ [group]: updatedGroup }); + } + const handleCheckboxChange = (value: string, group: string) => { const selectedGroup = filters[group as keyof typeof filters] as string[]; const updatedGroup = selectedGroup.includes(value) @@ -72,12 +110,6 @@ const AdvancedSearchDropdown: React.FC = ({ value: item.value, })) || []; - const blockStatusItems = - data.blockStatuses?.map((item: any) => ({ - text: item.label, - value: item.value, - })) || []; - return (
@@ -119,41 +151,29 @@ const AdvancedSearchDropdown: React.FC = ({ - (item ? item.text : "")} - onChange={(e: any) => - handleFilterChange({ orgUnit: e.selectedItem.value }) - } + item.value === filters.orgUnit - ) - : "" - } + id="orgunit-multiselect" + className="multi-select" + titleText="Org Unit" + items={orgUnitItems} + itemToString={(item: any) => (item ? item.value : "")} + selectionFeedback="top-after-reopen" + onChange={(e: any) => handleMultiSelectChange("orgUnit", e.selectedItems)} + selectedItems={selectedOrgUnits} /> - (item ? item.text : "")} - onChange={(e: any) => - handleFilterChange({ category: e.selectedItem.value }) - } - label="Enter or choose a category" - selectedItem={ - filters.category - ? categoryItems.find( - (item: any) => item.value === filters.category - ) - : "" - } + itemToString={(item: any) => (item ? item.value : "")} + selectionFeedback="top-after-reopen" + onChange={(e: any) => handleMultiSelectChange("category", e.selectedItems)} + selectedItems={selectedCategories} /> @@ -168,7 +188,7 @@ const AdvancedSearchDropdown: React.FC = ({ label="If you don't remember the client information you can go to client search." > = ({ - = ({
{filtersCount > 0 ? ( - clearFilters()} > - {"+" + filtersCount} - + ) : null}

0 ? "text-active" : ""}> Advanced Search diff --git a/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchTab/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchTab/index.tsx index 14f9ea33..72dc8d38 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchTab/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchTab/index.tsx @@ -114,11 +114,6 @@ const OpeningsSearchTab: React.FC = () => { } },[]) - // useEffect(()=>{ - // console.log("new filters") - // console.log(filters) - // },[filters]) - return ( <>

diff --git a/frontend/src/contexts/search/OpeningsSearch.tsx b/frontend/src/contexts/search/OpeningsSearch.tsx index d3aca104..b198c30d 100644 --- a/frontend/src/contexts/search/OpeningsSearch.tsx +++ b/frontend/src/contexts/search/OpeningsSearch.tsx @@ -18,8 +18,8 @@ export const OpeningsSearchProvider: React.FC<{ children: ReactNode }> = ({ chil const defaultFilters = { startDate: null as Date | null, endDate: null as Date | null, - orgUnit: null as string | null, - category: null as string | null, + orgUnit: [] as string[], + category: [] as string[], status: [] as string[], clientAcronym: "", clientLocationCode: "", diff --git a/frontend/src/services/search/openings.ts b/frontend/src/services/search/openings.ts index 2b90bd1b..af648373 100644 --- a/frontend/src/services/search/openings.ts +++ b/frontend/src/services/search/openings.ts @@ -9,10 +9,11 @@ export interface OpeningFilters { searchInput?: string; startDate?: string; endDate?: string; - orgUnit?: string; - category?: string; + orgUnit?: string[]; + category?: string[]; clientAcronym?: string; blockStatus?: string; + dateType?: string; cutBlock?: string; cuttingPermit?: string; grossArea?: string; @@ -62,8 +63,8 @@ export const fetchOpenings = async (filters: OpeningFilters): Promise => { const params = { mainSearchTerm: filters.searchInput, - orgUnit: filters.orgUnit, - category: filters.category, + orgUnit: filters.orgUnit, //Keep it as an array + category: filters.category, // Keep it as an array statusList: filters.status, // Keep it as an array entryUserId: filters.clientAcronym, cutBlockId: filters.cutBlock, From 3f0b8ca3817bb7fed77c1604e22bd2f47b643cd4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 06:28:06 -0800 Subject: [PATCH 34/43] fix(deps): update dependency com.nimbusds:nimbus-jose-jwt to v9.43 (#446) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- backend/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/pom.xml b/backend/pom.xml index 26ba013e..6b4b0cbf 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -545,7 +545,7 @@ com.nimbusds nimbus-jose-jwt - 9.42 + 9.43 org.testcontainers From e833322c3ae68ce6b245045132ab2922e5e34560 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Thu, 7 Nov 2024 07:14:28 -0800 Subject: [PATCH 35/43] chore(SILVA-403): code refactor (#444) --- .github/workflows/analysis.yml | 2 +- frontend/package-lock.json | 76 +--------- frontend/package.json | 6 +- frontend/src/App.tsx | 5 - .../src/__test__/actions/userAction.test.ts | 86 ----------- .../components/BCHeaderwSide.test.tsx | 22 +-- .../components/OpeningHistory.test.tsx | 57 ++++---- .../components/OpeningMetricsTab.test.tsx | 9 +- .../__test__/components/OpeningsTab.test.tsx | 136 ++++++------------ .../__test__/components/ThemeToggle.test.tsx | 18 +++ .../__snapshots__/ThemeToggle.test.tsx.snap | 61 ++++++++ .../__test__/contexts/AuthProvider.test.tsx | 2 +- .../contexts/NotificationProvider.test.tsx | 128 +++++++++++++++++ frontend/src/__test__/screens/Help.test.tsx | 33 ----- .../src/__test__/screens/Opening.test.tsx | 29 +++- .../__test__/views/LoginOrgSelection.test.tsx | 67 --------- .../src/actions/selectedClientRolesActions.ts | 16 --- frontend/src/actions/userAction.ts | 41 ------ frontend/src/amplifyconfiguration.ts | 3 +- .../src/components/ActionsTable/index.tsx | 9 +- frontend/src/components/BCHeader/index.tsx | 135 +++++++++-------- .../src/components/OpeningHistory/index.tsx | 54 +++++-- .../components/OpeningMetricsTab/index.tsx | 20 +-- frontend/src/components/OpeningsTab/index.tsx | 83 +++++------ .../src/components/SectionTitle/index.tsx | 16 +-- .../Openings/AdvancedSearchDropdown/index.tsx | 56 ++++---- .../Openings/OpeningsSearchBar/index.tsx | 7 +- .../Openings/OpeningsSearchTab/index.tsx | 11 +- .../Openings/SearchScreenDataTable/index.tsx | 60 +++----- .../src/components/StandardCard/index.tsx | 39 ----- .../src/components/StandardCard/style.scss | 70 --------- frontend/src/components/Subtitle/index.tsx | 9 +- .../src/components/TableSkeleton/index.tsx | 2 +- frontend/src/components/ThemeToggle/index.tsx | 16 ++- .../src/contexts/NotificationProvider.tsx | 87 +++++++++++ frontend/src/index.tsx | 13 +- frontend/src/map-services/BcGwLatLongUtils.ts | 2 +- frontend/src/map-services/BcGwWfsApi.tsx | 3 +- .../reducers/selectedClientRolesReducer.ts | 23 --- frontend/src/reducers/userReducer.ts | 33 ----- frontend/src/screens/Help/Help.scss | 12 -- frontend/src/screens/Help/index.tsx | 27 ---- frontend/src/screens/Landing/index.tsx | 86 ++++++----- frontend/src/screens/Opening/index.tsx | 12 +- .../src/screens/SilvicultureSearch/index.tsx | 19 +-- frontend/src/store.ts | 51 ------- frontend/src/types/NotificationType.ts | 8 ++ frontend/src/utils/ThemePreference.tsx | 2 +- .../src/views/LoginOrgSelection/index.tsx | 39 ----- .../src/views/LoginOrgSelection/styles.scss | 48 ------- 50 files changed, 699 insertions(+), 1150 deletions(-) delete mode 100644 frontend/src/__test__/actions/userAction.test.ts create mode 100644 frontend/src/__test__/components/ThemeToggle.test.tsx create mode 100644 frontend/src/__test__/components/__snapshots__/ThemeToggle.test.tsx.snap create mode 100644 frontend/src/__test__/contexts/NotificationProvider.test.tsx delete mode 100644 frontend/src/__test__/screens/Help.test.tsx delete mode 100644 frontend/src/__test__/views/LoginOrgSelection.test.tsx delete mode 100644 frontend/src/actions/selectedClientRolesActions.ts delete mode 100644 frontend/src/actions/userAction.ts delete mode 100644 frontend/src/components/StandardCard/index.tsx delete mode 100644 frontend/src/components/StandardCard/style.scss create mode 100644 frontend/src/contexts/NotificationProvider.tsx delete mode 100644 frontend/src/reducers/selectedClientRolesReducer.ts delete mode 100644 frontend/src/reducers/userReducer.ts delete mode 100644 frontend/src/screens/Help/Help.scss delete mode 100644 frontend/src/screens/Help/index.tsx delete mode 100644 frontend/src/store.ts create mode 100644 frontend/src/types/NotificationType.ts delete mode 100644 frontend/src/views/LoginOrgSelection/index.tsx delete mode 100644 frontend/src/views/LoginOrgSelection/styles.scss diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 4b00749f..92f6d86b 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -72,7 +72,7 @@ jobs: -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info -Dsonar.typescript.tsconfigPaths=tsconfig.json -Dsonar.sources=src/ - -Dsonar.exclusions=src/__test__/** + -Dsonar.exclusions=src/__test__/**,src/amplifyconfiguration.*,src/**/*.scss,src/**/*.css,src/**/*.d.*,src/setupTests.* -Dsonar.tests=src/__test__/ -Dsonar.project.monorepo.enabled=true sonar_token: ${{ secrets.SONAR_TOKEN_FRONTEND }} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9212ab8e..9724ce05 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,7 +13,6 @@ "@carbon/icons-react": "^11.50.1", "@carbon/pictograms-react": "^11.49.0", "@carbon/react": "^1.27.0", - "@redux-devtools/extension": "^3.3.0", "@tanstack/react-query": "^5.50.1", "@types/node": "^22.0.0", "@vitejs/plugin-react": "^4.0.4", @@ -30,10 +29,7 @@ "react-esri-leaflet": "^2.0.1", "react-hash-string": "^1.0.0", "react-leaflet": "^4.2.1", - "react-redux": "^9.0.0", "react-router-dom": "^6.10.0", - "redux": "^5.0.0", - "redux-thunk": "^3.0.0", "vite": "^5.0.0", "vite-plugin-svgr": "^4.0.0", "vite-tsconfig-paths": "^5.0.0", @@ -3122,19 +3118,6 @@ "react-dom": "^18.0.0" } }, - "node_modules/@redux-devtools/extension": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@redux-devtools/extension/-/extension-3.3.0.tgz", - "integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2", - "immutable": "^4.3.4" - }, - "peerDependencies": { - "redux": "^3.1.0 || ^4.0.0 || ^5.0.0" - } - }, "node_modules/@remix-run/router": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", @@ -5226,7 +5209,7 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/qs": { @@ -5247,7 +5230,7 @@ "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -5330,12 +5313,6 @@ "@types/geojson": "*" } }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", - "license": "MIT" - }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -6657,7 +6634,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/d3": { @@ -11265,29 +11242,6 @@ "react-dom": "^18.0.0" } }, - "node_modules/react-redux": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", - "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.3", - "use-sync-external-store": "^1.0.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25", - "react": "^18.0", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -11356,21 +11310,6 @@ "node": ">=8" } }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -12756,15 +12695,6 @@ "react-dom": ">=16.8.0" } }, - "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 276e9e7b..7772c0a6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,11 +9,10 @@ "@carbon/icons-react": "^11.50.1", "@carbon/pictograms-react": "^11.49.0", "@carbon/react": "^1.27.0", - "@redux-devtools/extension": "^3.3.0", "@tanstack/react-query": "^5.50.1", "@types/node": "^22.0.0", "@vitejs/plugin-react": "^4.0.4", - "@vitejs/plugin-react-swc": "^3.3.2", + "@vitejs/plugin-react-swc": "^3.3.2", "aws-amplify": "^6.7.0", "axios": "^1.6.8", "jspdf": "^2.5.2", @@ -26,10 +25,7 @@ "react-esri-leaflet": "^2.0.1", "react-hash-string": "^1.0.0", "react-leaflet": "^4.2.1", - "react-redux": "^9.0.0", "react-router-dom": "^6.10.0", - "redux": "^5.0.0", - "redux-thunk": "^3.0.0", "vite": "^5.0.0", "vite-plugin-svgr": "^4.0.0", "vite-tsconfig-paths": "^5.0.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1895b049..3a618220 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,6 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import './custom.scss'; import Landing from "./screens/Landing"; -import Help from "./screens/Help"; import SideLayout from './layouts/SideLayout'; import ProtectedRoute from './routes/ProtectedRoute'; import Opening from './screens/Opening'; @@ -33,10 +32,6 @@ const router = createBrowserRouter([ { path: "/silviculture-search", element: } /> - }, - { - path: "/help", - element: } /> } ] }, diff --git a/frontend/src/__test__/actions/userAction.test.ts b/frontend/src/__test__/actions/userAction.test.ts deleted file mode 100644 index 592f409f..00000000 --- a/frontend/src/__test__/actions/userAction.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { describe, it, expect, vi } from 'vitest'; -import { getUserDetails, setClientRoles } from '../../actions/userAction'; -import { - USER_DETAILS_REQUEST, -USER_DETAILS_SUCCESS, -USER_DETAILS_FAIL, -SET_CLIENT_ROLES -} from '../../constants/userConstants'; -import { useGetAuth } from '../../contexts/AuthProvider'; -import { AppDispatch } from '../../store'; -import { UserClientRolesType } from '../../types/UserRoleType'; - - - -vi.mock('../../contexts/AuthProvider', () => ({ -useGetAuth: vi.fn(), -})); - -describe('userAction', () => { -describe('getUserDetails', () => { - it('should dispatch USER_DETAILS_REQUEST and USER_DETAILS_SUCCESS with user data when successful', async () => { - const mockDispatch = vi.fn(); - const mockIsLoggedIn = true; - const mockUser = { firstName: 'John', lastName: 'Doe' }; - const mockUserJSON = JSON.stringify(mockUser); - - (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: mockIsLoggedIn }); - localStorage.setItem('famLoginUser', mockUserJSON); - - await getUserDetails()(mockDispatch as unknown as AppDispatch); - - expect(mockDispatch).toHaveBeenCalledWith({ type: USER_DETAILS_REQUEST }); - expect(mockDispatch).toHaveBeenCalledWith({ - type: USER_DETAILS_SUCCESS, - payload: { ...mockUser, isLoggedIn: mockIsLoggedIn }, - }); - }); - - it('should dispatch USER_DETAILS_FAIL with error when an error occurs', async () => { - const mockDispatch = vi.fn(); - const mockError = new Error('Test error'); - - (useGetAuth as vi.Mock).mockImplementation(() => { - throw mockError; - }); - - await getUserDetails()(mockDispatch as unknown as AppDispatch); - - expect(mockDispatch).toHaveBeenCalledWith({ type: USER_DETAILS_REQUEST }); - expect(mockDispatch).toHaveBeenCalledWith({ - type: USER_DETAILS_FAIL, - payload: { error: mockError }, - }); - }); - - it('should handle missing user data in localStorage', async () => { - const mockDispatch = vi.fn(); - const mockIsLoggedIn = true; - - (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: mockIsLoggedIn }); - localStorage.removeItem('famLoginUser'); - - await getUserDetails()(mockDispatch as unknown as AppDispatch); - - expect(mockDispatch).toHaveBeenCalledWith({ type: USER_DETAILS_REQUEST }); - expect(mockDispatch).toHaveBeenCalledWith({ - type: USER_DETAILS_SUCCESS, - payload: { isLoggedIn: mockIsLoggedIn }, - }); - }); -}); - -describe('setClientRoles', () => { - it('should dispatch SET_CLIENT_ROLES with client roles', () => { - const mockDispatch = vi.fn(); - const mockClientRoles: UserClientRolesType[] = [{ clientId: '123', roles: ['admin'] }]; - - setClientRoles(mockClientRoles)(mockDispatch as unknown as AppDispatch); - - expect(mockDispatch).toHaveBeenCalledWith({ - type: SET_CLIENT_ROLES, - payload: mockClientRoles, - }); - }); -}); -}); \ No newline at end of file diff --git a/frontend/src/__test__/components/BCHeaderwSide.test.tsx b/frontend/src/__test__/components/BCHeaderwSide.test.tsx index c3a49c78..5a920686 100644 --- a/frontend/src/__test__/components/BCHeaderwSide.test.tsx +++ b/frontend/src/__test__/components/BCHeaderwSide.test.tsx @@ -5,9 +5,6 @@ import { BrowserRouter } from 'react-router-dom'; import BCHeaderwSide from '../../components/BCHeaderwSide'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import {leftMenu } from '../../components/BCHeaderwSide/constants'; -import * as redux from 'react-redux'; -import { Provider } from 'react-redux'; -import store from '../../store'; import { UserClientRolesType } from '../../types/UserRoleType'; import '@testing-library/jest-dom'; import { AuthProvider } from '../../contexts/AuthProvider'; @@ -31,12 +28,10 @@ const renderComponent = () => { render( - - - - - - + + + + ); @@ -65,9 +60,6 @@ const state = { }, }; -vi.spyOn(redux, 'useSelector') - .mockImplementation((callback) => callback(state)); - describe('BCHeaderwSide', () => { it('should renders the component', () => { renderComponent(); @@ -88,12 +80,6 @@ describe('BCHeaderwSide', () => { // expect(screen.queryByText('My Profile')).not.toBeVisible(); }); - // it('renders the correct number of top-level menu items', () => { - // renderComponent(); - // const menuItems = screen.getAllByRole('button', { name: /.*Category.*/ }); - // expect(menuItems).toHaveLength(leftMenu.length); - // }); - it('renders the correct menu item names', () => { renderComponent(); leftMenu.forEach(item => { diff --git a/frontend/src/__test__/components/OpeningHistory.test.tsx b/frontend/src/__test__/components/OpeningHistory.test.tsx index e65b5a08..e5be3a72 100644 --- a/frontend/src/__test__/components/OpeningHistory.test.tsx +++ b/frontend/src/__test__/components/OpeningHistory.test.tsx @@ -2,49 +2,32 @@ import React from 'react'; import { render, act } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import OpeningHistory from '../../components/OpeningHistory'; -import History from '../../types/History'; -import { deleteOpeningFavorite } from '../../services/OpeningFavoriteService'; - -const mockHistories: History[] = [ - { - id: 1, - steps: [], - }, - { - id: 2, - steps: [ - { step: 1, status: 'complete', description: 'Step 1', subtitle: 'Completed' }, - { step: 2, status: 'invalid', description: 'Step 2', subtitle: 'Invalid' }, - { step: 3, status: 'disabled', description: 'Step 3', subtitle: 'Disabled' }, - ], - }, -]; +import { NotificationProvider } from '../../contexts/NotificationProvider'; +import { deleteOpeningFavorite, fetchOpeningTrends } from '../../services/OpeningFavoriteService'; vi.mock('../../services/OpeningFavoriteService', () => ({ deleteOpeningFavorite: vi.fn(), + fetchOpeningTrends: vi.fn(), })); describe('OpeningHistory Component', () => { it('renders correctly with given histories', async () => { - let getByText; + (fetchOpeningTrends as vi.Mock).mockReturnValueOnce(Promise.resolve([1, 2])); + let container; await act(async () => { - ({ getByText } = render( )); + ({ container } = render()); }); // Check for the presence of Opening Ids - expect(getByText('Opening Id 1')).toBeInTheDocument(); - expect(getByText('Opening Id 2')).toBeInTheDocument(); - - // Check for the presence of step descriptions - expect(getByText('Step 1')).toBeInTheDocument(); - expect(getByText('Step 2')).toBeInTheDocument(); - expect(getByText('Step 3')).toBeInTheDocument(); + expect(container.querySelector('div[data-id="1"').innerHTML).toContain('Opening Id 1'); + expect(container.querySelector('div[data-id="2"').innerHTML).toContain('Opening Id 2'); }); it('renders correctly with empty histories', async () => { + (fetchOpeningTrends as vi.Mock).mockReturnValueOnce(Promise.resolve([])); let container; await act(async () => { - ({ container } = render( )); + ({ container } = render()); }); // Select the div with the specific class @@ -57,13 +40,29 @@ describe('OpeningHistory Component', () => { // check if when clicked on the FavoriteButton, the deleteOpeningFavorite function is called it('should call deleteOpeningFavorite when FavoriteButton is clicked', async () => { + (fetchOpeningTrends as vi.Mock).mockReturnValueOnce(Promise.resolve([1, 2])); let container; await act(async () => { - ({ container } = render( )); + ({ container } = render()); }); const favoriteButton = container.querySelector('.favorite-icon button') - favoriteButton && favoriteButton.click(); + await act(async () => favoriteButton && favoriteButton.click()); + + expect(deleteOpeningFavorite).toHaveBeenCalled(); + }); + + it('should call deleteOpeningFavorite and handle error when FavoriteButton is clicked', async () => { + (fetchOpeningTrends as vi.Mock).mockReturnValueOnce(Promise.resolve([1, 2])); + (deleteOpeningFavorite as vi.Mock).mockRejectedValueOnce(new Error('Failed to delete favorite')); + let container; + await act(async () => { + ({ container } = render()); + }); + + const favoriteButton = container.querySelector('.favorite-icon button') + await act(async () => favoriteButton && favoriteButton.click()); + expect(deleteOpeningFavorite).toHaveBeenCalled(); }); }); diff --git a/frontend/src/__test__/components/OpeningMetricsTab.test.tsx b/frontend/src/__test__/components/OpeningMetricsTab.test.tsx index 75a27d69..08a02b41 100644 --- a/frontend/src/__test__/components/OpeningMetricsTab.test.tsx +++ b/frontend/src/__test__/components/OpeningMetricsTab.test.tsx @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import React from 'react'; import { render, act, waitFor, fireEvent, screen } from '@testing-library/react'; import OpeningMetricsTab from '../../components/OpeningMetricsTab'; +import { NotificationProvider } from '../../contexts/NotificationProvider'; import { fetchOpeningTrends } from '../../services/OpeningFavoriteService'; import { fetchFreeGrowingMilestones, fetchOpeningsPerYear, fetchRecentOpenings } from '../../services/OpeningService'; @@ -46,7 +47,7 @@ describe('OpeningMetricsTab', () => { it('should render the OpeningMetricsTab component with all sections', async () => { - await act(async () => render()); + await act(async () => render()); expect(screen.getByText('Dashboard')).toBeInTheDocument(); expect(screen.getByText('Manage and track silvicultural information about openings')).toBeInTheDocument(); @@ -64,7 +65,7 @@ describe('OpeningMetricsTab', () => { await act(async () => { - render(); + render(); }); await waitFor(() => { @@ -84,7 +85,7 @@ describe('OpeningMetricsTab', () => { delete window.location; window.location = { search: '?scrollTo=trackOpenings' } as any; - await act(async () => render()); + await act(async () => render()); expect(mockScrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' }); @@ -100,7 +101,7 @@ describe('OpeningMetricsTab', () => { delete window.location; window.location = { search: '' } as any; - await act(async () => render()); + await act(async () => render()); expect(mockScrollIntoView).not.toHaveBeenCalled(); diff --git a/frontend/src/__test__/components/OpeningsTab.test.tsx b/frontend/src/__test__/components/OpeningsTab.test.tsx index 0b5cd8d1..10bd1b32 100644 --- a/frontend/src/__test__/components/OpeningsTab.test.tsx +++ b/frontend/src/__test__/components/OpeningsTab.test.tsx @@ -1,99 +1,51 @@ -import React from "react"; -import { render, screen } from "@testing-library/react"; -import "@testing-library/jest-dom"; -import { describe, expect, it, vi } from "vitest"; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import OpeningsTab from "../../../src/components/OpeningsTab"; -import { Provider } from "react-redux"; -import store from "../../store"; -import {useUserRecentOpeningQuery} from "../../../src/services/queries/search/openingQueries"; -import { MemoryRouter } from "react-router-dom"; -import PaginationContext from "../../contexts/PaginationContext"; - -// Mocking useUserRecentOpeningQuery to return the necessary functions and state -vi.mock("../../../src/services/queries/search/openingQueries", () => ({ - useUserRecentOpeningQuery: vi.fn(), +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import React from 'react'; +import { render, act, waitFor, screen } from '@testing-library/react'; +import OpeningsTab from '../../components/OpeningsTab'; +import { AuthProvider } from '../../contexts/AuthProvider'; +import { getWmsLayersWhitelistUsers } from '../../services/SecretsService'; +import { fetchRecentOpenings } from '../../services/OpeningService'; +import { RecentOpening } from '../../types/RecentOpening'; +import PaginationProvider from '../../contexts/PaginationProvider'; + + +vi.mock('../../services/SecretsService', () => ({ + getWmsLayersWhitelistUsers: vi.fn() })); - -const paginationValueMock = { - getCurrentData: () => [], - currentPage: 0, - totalPages: 0, - handlePageChange: vi.fn(), - handleItemsPerPageChange: vi.fn(), - itemsPerPage: 5, - totalResultItems:100, - setTotalResultItems:vi.fn(), - setPageData: vi.fn(), - setInitialItemsPerPage: vi.fn(), +vi.mock('../../services/OpeningService', async () => { + const actual = await vi.importActual('../../services/OpeningService'); + return { + ...actual, + fetchRecentOpenings: vi.fn(), }; +}); -describe("OpeningsTab", () => { - const queryClient = new QueryClient(); - const showSpatial = false; - const setShowSpatial = vi.fn(); - - it("renders the component successfully", () => { - (useUserRecentOpeningQuery as jest.Mock).mockReturnValue({ data: [], isFetching: false }); - render( - - - - - - - - - - ); - const searchInput = screen.getByText(/Track the history of openings you have looked at and check spatial information by selecting the openings in the table below/i); - expect(searchInput).toBeInTheDocument(); - }); - it("shows the spatial area with Hide Spatial Button", () => { - (useUserRecentOpeningQuery as jest.Mock).mockReturnValue({ data: [], isFetching: false }); - render( - - - - - - - - - - ); - const hideSpatialButton = screen.getByRole('button', { name: /Hide Spatial/i }); - expect(hideSpatialButton).toBeInTheDocument(); +const rows: RecentOpening[] = [{ + id: '123', + openingId: '123', + fileId: '1', + cuttingPermit: '1', + timberMark: '1', + cutBlock: '1', + grossAreaHa: 1, + statusDesc: 'Approved', + categoryDesc: 'Another:Another', + disturbanceStart: '1', + entryTimestamp: '1', + updateTimestamp: '1', +}]; + +describe('Openings Tab test',() => { + + it('should render properly',async () =>{ + (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{userName: 'TEST'}]); + (fetchRecentOpenings as vi.Mock).mockResolvedValue(rows); + await act(async () => { + render(); + }); + expect(screen.getByText('Recent openings')).toBeInTheDocument(); }); - - it("shows table skeleton when the data is loading", () => { - (useUserRecentOpeningQuery as jest.Mock).mockReturnValue({ data: [], isFetching: true }); - - render( - - - - - - - - - - ); - console.log(screen.debug()); - expect.poll(() => document.querySelector('--cds-skeleton')).toBeTruthy(); - }); - -}); +}); \ No newline at end of file diff --git a/frontend/src/__test__/components/ThemeToggle.test.tsx b/frontend/src/__test__/components/ThemeToggle.test.tsx new file mode 100644 index 00000000..41cdceea --- /dev/null +++ b/frontend/src/__test__/components/ThemeToggle.test.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import ThemeToggle from '../../components/ThemeToggle'; + +describe('Theme toggle component tests', () => { + it('should render correctly', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('should toggle theme when button is clicked', () => { + const { container } = render(); + const button = container.querySelector('button'); + button?.click(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/__test__/components/__snapshots__/ThemeToggle.test.tsx.snap b/frontend/src/__test__/components/__snapshots__/ThemeToggle.test.tsx.snap new file mode 100644 index 00000000..2c112feb --- /dev/null +++ b/frontend/src/__test__/components/__snapshots__/ThemeToggle.test.tsx.snap @@ -0,0 +1,61 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Theme toggle component tests > should render correctly 1`] = ` +
+
+
+ +
+
+
+`; + +exports[`Theme toggle component tests > should toggle theme when button is clicked 1`] = ` +
+
+
+ +
+
+
+`; diff --git a/frontend/src/__test__/contexts/AuthProvider.test.tsx b/frontend/src/__test__/contexts/AuthProvider.test.tsx index 3430ccca..7106ad23 100644 --- a/frontend/src/__test__/contexts/AuthProvider.test.tsx +++ b/frontend/src/__test__/contexts/AuthProvider.test.tsx @@ -91,7 +91,7 @@ describe('AuthProvider', () => { it('should handle login correctly', async () => { setAuthCookies(sampleAuthToken); const provider = 'idir'; - const envProvider = 'TEST-IDIR'; + const envProvider = `${env.VITE_ZONE ?? 'DEV'}-IDIR`; const TestComponent = () => { const { login } = useGetAuth(); diff --git a/frontend/src/__test__/contexts/NotificationProvider.test.tsx b/frontend/src/__test__/contexts/NotificationProvider.test.tsx new file mode 100644 index 00000000..320bda9b --- /dev/null +++ b/frontend/src/__test__/contexts/NotificationProvider.test.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { render, screen, act } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { NotificationProvider, useNotification } from '../../contexts/NotificationProvider'; +import { NotificationContent } from '../../types/NotificationType'; +import { ActionableNotification } from '@carbon/react'; + +// Mock the ActionableNotification component +vi.mock('@carbon/react', () => ({ + ActionableNotification: ({ title, subtitle, onClose, onActionButtonClick }: any) => ( +
+
{title}
+
{subtitle}
+ + +
+ ) +})); + +const onCloseMock = vi.fn(); + +const TestComponent = () => { + const { displayNotification } = useNotification(); + return ( + + ); +}; + +describe('NotificationProvider', () => { + it('should display notification when displayNotification is called', () => { + render( + + + + ); + + act(() => { + screen.getByText('Show Notification').click(); + }); + + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByText('Test Subtitle')).toBeInTheDocument(); + expect(screen.getByText('Close')).toBeInTheDocument(); + expect(screen.getByText('Action')).toBeInTheDocument(); + }); + + it('should hide notification when close button is clicked', () => { + render( + + + + ); + + act(() => { + screen.getByText('Show Notification').click(); + }); + + act(() => { + screen.getByText('Close').click(); + }); + + expect(screen.queryByText('Test Title')).not.toBeInTheDocument(); + expect(screen.queryByText('Test Subtitle')).not.toBeInTheDocument(); + }); + + it('should call onClose and hide notification when action button is clicked', () => { + + + render( + + + + ); + + act(() => { + screen.getByText('Show Notification').click(); + }); + + act(() => { + screen.getByText('Action').click(); + }); + + expect(onCloseMock).toHaveBeenCalled(); + expect(screen.queryByText('Test Title')).not.toBeInTheDocument(); + expect(screen.queryByText('Test Subtitle')).not.toBeInTheDocument(); + }); + + it('should hide notification after dismissIn time', async () => { + vi.useFakeTimers(); // Use fake timers + + await act(async () => render( + + + + )); + + await act(async () => { + screen.getByText('Show Notification').click(); + }); + + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByText('Test Subtitle')).toBeInTheDocument(); + + await act(async () => vi.advanceTimersByTime(1111)); // Fast-forward time by 1 second + + expect(screen.queryByText('Test Title')).not.toBeInTheDocument(); + expect(screen.queryByText('Test Subtitle')).not.toBeInTheDocument + + }); + + + afterEach(() => { + vi.useRealTimers(); // Restore real timers after each test + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/screens/Help.test.tsx b/frontend/src/__test__/screens/Help.test.tsx deleted file mode 100644 index ff823dc2..00000000 --- a/frontend/src/__test__/screens/Help.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import { render, screen, act } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; -import Help from '../../screens/Help'; -import { describe, expect, it } from 'vitest'; - -describe('Help component test cases', () => { - it('should renders the help page header', () => { - render(, { wrapper: MemoryRouter }); - const headerElement = screen.getByText('Help Page'); - expect(headerElement).not.toBeNull(); - }); - - it('should renders the help page content', () => { - render(, { wrapper: MemoryRouter }); - const contentElement = screen.getByText(/Welcome to the Help Page/i); - expect(contentElement).not.toBeNull(); - }); - - it('should navigates back to home page on button click', () => { - act(() => { - render(, { wrapper: MemoryRouter }); - }); - - act(() => { - const backButton = screen.getByRole('button', { name: /Back to Home/i }); - backButton.click(); - }); - - // Add assertions to test if navigation to the home page occurs - expect(location.pathname).toBe('/'); - }); -}); diff --git a/frontend/src/__test__/screens/Opening.test.tsx b/frontend/src/__test__/screens/Opening.test.tsx index 8e4d3970..863c0d76 100644 --- a/frontend/src/__test__/screens/Opening.test.tsx +++ b/frontend/src/__test__/screens/Opening.test.tsx @@ -3,12 +3,13 @@ import { describe, expect, it, vi } from 'vitest'; import { render, waitFor, act } from '@testing-library/react'; import Opening from '../../screens/Opening'; import PaginationContext from '../../contexts/PaginationContext'; +import { NotificationProvider } from '../../contexts/NotificationProvider'; import { BrowserRouter } from 'react-router-dom'; -import * as redux from 'react-redux'; import { RecentOpening } from '../../types/RecentOpening'; import { getWmsLayersWhitelistUsers } from '../../services/SecretsService'; import { fetchFreeGrowingMilestones, fetchOpeningsPerYear, fetchRecentOpenings } from '../../services/OpeningService'; import { fetchOpeningTrends } from '../../services/OpeningFavoriteService'; +import { AuthProvider } from '../../contexts/AuthProvider'; const data = { "activityType": "Update", @@ -44,8 +45,6 @@ const state = { } }; -vi.spyOn(redux, 'useSelector') - .mockImplementation((callback) => callback(state)); const rows: RecentOpening[] = [{ id: '123', @@ -96,7 +95,11 @@ describe('Opening screen test cases', () => { const { getByTestId } = render( - + + + + + ); @@ -117,7 +120,11 @@ describe('Opening screen test cases', () => { ({ container } = render( - + + + + + )); @@ -152,7 +159,11 @@ describe('Opening screen test cases', () => { ({ container, getByText } = render( - + + + + + )); @@ -171,7 +182,11 @@ describe('Opening screen test cases', () => { ({ container, getByText } = render( - + + + + + )); diff --git a/frontend/src/__test__/views/LoginOrgSelection.test.tsx b/frontend/src/__test__/views/LoginOrgSelection.test.tsx deleted file mode 100644 index a003fdc7..00000000 --- a/frontend/src/__test__/views/LoginOrgSelection.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; -import { Provider } from 'react-redux'; -import store from '../../store'; -import LoginOrgSelection from '../../views/LoginOrgSelection'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { UserClientRolesType } from '../../types/UserRoleType'; -import * as redux from 'react-redux'; - -vi.mock('../../services/TestService', () => ({ - getForestClientByNumberOrAcronym: vi.fn(() => [ - { - clientNumber: '00012797', - clientName: 'MINISTRY OF FORESTS', - legalFirstName: '', - legalMiddleName: '', - clientStatusCode: { code: 'ACT', description: 'Active' }, - clientTypeCode: { code: 'F', description: 'Ministry of Forests and Range' }, - acronym: 'MOF' - }, - ]), -})); - -const clientRoles: UserClientRolesType[] = [ - { - clientId: '00012797', - roles: ['ONE', 'TWO', 'THREE'], - clientName: 'MINISTRY OF FORESTS' - } -]; - -const state = { - userDetails: { - id: 1, - name: 'User', - user: { - firstName: 'John', - lastName: 'Doe', - providerUsername: 'johndoe123', - clientRoles: clientRoles - }, - loading: false, - error: null - }, -}; - -vi.spyOn(redux, 'useSelector') - .mockImplementation((callback) => callback(state)); - -describe('LoginOrgSelection', () => { - it('renders organization selection view with user details', () => { - const qc = new QueryClient(); - - render( - - - - - - ); - - // Check if elements are rendered correctly - expect(screen.getByText('Organization selection')).toBeDefined(); - expect(screen.getByText('John Doe (johndoe123) select which organization you\'re representing.')).toBeDefined(); - }); -}); diff --git a/frontend/src/actions/selectedClientRolesActions.ts b/frontend/src/actions/selectedClientRolesActions.ts deleted file mode 100644 index aa0ec954..00000000 --- a/frontend/src/actions/selectedClientRolesActions.ts +++ /dev/null @@ -1,16 +0,0 @@ -// selectedClientRolesActions.js -import { SET_SELECTED_CLIENT_ROLES } from '../constants/selectedClientRolesConstants'; -import { AppDispatch } from '../store'; -import { UserClientRolesType } from '../types/UserRoleType'; - -export interface SetSelectedClientRolesAction { - type: typeof SET_SELECTED_CLIENT_ROLES; - payload: UserClientRolesType; -} - -export const setSelectedClientRoles = (selectedClientRoles: UserClientRolesType) => (dispatch: AppDispatch) => { - dispatch({ - type: SET_SELECTED_CLIENT_ROLES, - payload: selectedClientRoles - }); -}; diff --git a/frontend/src/actions/userAction.ts b/frontend/src/actions/userAction.ts deleted file mode 100644 index 87d6369d..00000000 --- a/frontend/src/actions/userAction.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - USER_DETAILS_REQUEST, - USER_DETAILS_SUCCESS, - USER_DETAILS_FAIL, - SET_CLIENT_ROLES -} from '../constants/userConstants'; -import { AppDispatch } from '../store'; -import { UserClientRolesType } from '../types/UserRoleType'; -import { useGetAuth } from '../contexts/AuthProvider'; - -const FAM_LOGIN_USER = 'famLoginUser'; - -export const getUserDetails = () => async (dispatch: AppDispatch) => { - try { - dispatch({ - type: USER_DETAILS_REQUEST - }); - //first call the isCurrent and only after that extract the JSON - const { isLoggedIn } = useGetAuth(); - - const userJSON = localStorage.getItem(FAM_LOGIN_USER); // Retrieve the JSON string from local storage - const user = userJSON ? JSON.parse(userJSON) : null; // Parse the JSON string to a JavaScript object - - dispatch({ - type: USER_DETAILS_SUCCESS, - payload: { ...user, isLoggedIn } - }); - } catch (error) { - dispatch({ - type: USER_DETAILS_FAIL, - payload: { error: error } - }); - } -}; - -export const setClientRoles = (clientRoles:UserClientRolesType[]) => (dispatch: AppDispatch) => { - dispatch({ - type: SET_CLIENT_ROLES, - payload: clientRoles - }); -}; diff --git a/frontend/src/amplifyconfiguration.ts b/frontend/src/amplifyconfiguration.ts index 31a482f0..0598eda2 100644 --- a/frontend/src/amplifyconfiguration.ts +++ b/frontend/src/amplifyconfiguration.ts @@ -3,7 +3,8 @@ import { env } from './env'; const ZONE = env.VITE_ZONE.toLocaleLowerCase(); const redirectUri = window.location.origin; const logoutDomain = `https://logon${ZONE === "prod"?'':'test'}7.gov.bc.ca`; -const retUrl = `https://${ZONE === "prod" ? "loginproxy" :ZONE === "test" ? "test.loginproxy": "dev.loginproxy"}.gov.bc.ca/auth/realms/standard/protocol/openid-connect/logout`; +const returnUrlHost = ZONE === "prod" ? "loginproxy" :ZONE === "test" ? "test.loginproxy": "dev.loginproxy"; +const retUrl = `https://${returnUrlHost}.gov.bc.ca/auth/realms/standard/protocol/openid-connect/logout`; const redirectSignOut = env.VITE_REDIRECT_SIGN_OUT && env.VITE_REDIRECT_SIGN_OUT.trim() !== "" diff --git a/frontend/src/components/ActionsTable/index.tsx b/frontend/src/components/ActionsTable/index.tsx index 30203d40..0dc84b85 100644 --- a/frontend/src/components/ActionsTable/index.tsx +++ b/frontend/src/components/ActionsTable/index.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Table, TableHead, @@ -14,8 +13,8 @@ import { ActivityTagFileFormatEnum, ActivityTagTypeEnum } from '../../types/Acti import { ITableHeader } from '../../types/TableHeader'; interface IActionsTable { - rows: RecentAction[]; - headers: ITableHeader[]; + readonly rows: RecentAction[]; + readonly headers: ITableHeader[]; } /** @@ -68,8 +67,8 @@ function ActionsTable(props: IActionsTable): JSX.Element { - {props.rows.map((row: RecentAction, idx: number) => ( - + {props.rows.map((row: RecentAction) => ( + {headerKeys.map((key: string) => ( {key === "statusCode" ? ( diff --git a/frontend/src/components/BCHeader/index.tsx b/frontend/src/components/BCHeader/index.tsx index c54f62ad..36a68854 100644 --- a/frontend/src/components/BCHeader/index.tsx +++ b/frontend/src/components/BCHeader/index.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Link } from "react-router-dom"; import { useThemePreference } from "../../utils/ThemePreference"; import { toggleTheme } from "../../utils/ThemeFunction"; import { @@ -16,7 +15,7 @@ import { SideNavItems, HeaderSideNavItems, } from '@carbon/react'; -import { NavLink } from "react-router-dom"; +import { NavLink , Link} from "react-router-dom"; import * as Icons from '@carbon/icons-react'; import './BCHeader.scss' import { HeaderContainerProps } from "./definitions"; @@ -26,78 +25,76 @@ const BCHeader: React.FC = () => { const { theme, setTheme } = useThemePreference(); return ( - <> - ( -
- - - - SILVA - - - Link 1 - Link 2 - Link 3 - - Sub-link 1 - Sub-link 2 - Sub-link 3 - - - + ( +
+ + + + SILVA + + + Link 1 + Link 2 + Link 3 + + Sub-link 1 + Sub-link 2 + Sub-link 3 + + + - {toggleTheme(theme,setTheme)}} - > - {/* Must have a child component */} - <>{theme === 'g10'?:} - + {toggleTheme(theme,setTheme)}} + > + {/* Must have a child component */} + <>{theme === 'g10'?:} + - - - - - - + - + aria-label="Help" + > + + + + + + - - - - - Link 1 - Link 2 - Link 3 - - Sub-link 1 - Sub-link 2 - Sub-link 3 - - - - -
- )} - /> - +
+ + + + Link 1 + Link 2 + Link 3 + + Sub-link 1 + Sub-link 2 + Sub-link 3 + + + + +
+ )} + /> ); }; diff --git a/frontend/src/components/OpeningHistory/index.tsx b/frontend/src/components/OpeningHistory/index.tsx index cad8b441..936968d4 100644 --- a/frontend/src/components/OpeningHistory/index.tsx +++ b/frontend/src/components/OpeningHistory/index.tsx @@ -1,3 +1,4 @@ +import React, { useState, useEffect } from "react"; import { ProgressIndicator, ProgressStep @@ -7,31 +8,55 @@ import History from '../../types/History'; import statusClass from '../../utils/HistoryStatus'; import FavoriteButton from '../FavoriteButton'; import './styles.scss'; +import { useNotification } from '../../contexts/NotificationProvider'; +import { fetchOpeningTrends, deleteOpeningFavorite } from "../../services/OpeningFavoriteService"; -import { deleteOpeningFavorite } from '../../services/OpeningFavoriteService'; -interface OpeningHistoryProps { - histories: History[]; -} +const OpeningHistory: React.FC = () => { + const { displayNotification } = useNotification(); + const [histories, setHistories] = useState([]); -const handleFavoriteChange = async (newStatus: boolean, openingId: number) => { - try { - if(!newStatus){ - await deleteOpeningFavorite(openingId); + const loadTrends = async () => { + const history = await fetchOpeningTrends(); + setHistories(history?.map(item => ({ id: item, steps: [] })) || []); + }; + + useEffect(() => { loadTrends(); },[]); + + + const handleFavoriteChange = async (newStatus: boolean, openingId: number) => { + try { + if(!newStatus){ + await deleteOpeningFavorite(openingId); + displayNotification({ + title: 'Favorite Removed', + subTitle: `Opening Id ${openingId} removed from favorites`, + type: 'success', + dismissIn: 8000, + onClose: () => {} + }); + loadTrends(); + } + } catch (error) { + displayNotification({ + title: 'Error', + subTitle: `Failed to update favorite status for ${openingId}`, + type: 'error', + dismissIn: 8000, + onClose: () => {} + }); } - } catch (error) { - console.error(`Failed to update favorite status for ${openingId}`); - } -}; + }; + + return ( -const OpeningHistory = ({ histories }: OpeningHistoryProps) => (
{histories.map((history, index) => (
-
+
(
); +}; export default OpeningHistory; diff --git a/frontend/src/components/OpeningMetricsTab/index.tsx b/frontend/src/components/OpeningMetricsTab/index.tsx index 44282295..7325af4b 100644 --- a/frontend/src/components/OpeningMetricsTab/index.tsx +++ b/frontend/src/components/OpeningMetricsTab/index.tsx @@ -1,18 +1,15 @@ -import React, { useRef, useEffect, useState } from "react"; +import React, { useRef, useEffect } from "react"; import './styles.scss'; import SectionTitle from "../SectionTitle"; import BarChartGrouped from "../BarChartGrouped"; import ChartContainer from "../ChartContainer"; import DoughnutChartView from "../DoughnutChartView"; import OpeningHistory from "../OpeningHistory"; -import History from "../../types/History"; import MyRecentActions from "../MyRecentActions"; -import { fetchOpeningTrends } from "../../services/OpeningFavoriteService"; const OpeningMetricsTab: React.FC = () => { const trackOpeningRef = useRef(null); - const [submissionTrends, setSubmissionTrends] = useState([]); - + // Optional: Scroll to "Track Openings" when this component mounts useEffect(() => { @@ -23,13 +20,6 @@ const OpeningMetricsTab: React.FC = () => { trackOpeningRef.current.scrollIntoView({ behavior: "smooth" }); } - const loadTrends = async () => { - const response = await fetchOpeningTrends(); - setSubmissionTrends(response.map(item => ({ id: item, steps: [] }))); - }; - - loadTrends(); - }, []); return ( @@ -48,10 +38,8 @@ const OpeningMetricsTab: React.FC = () => {
{/* Add ref here to scroll */} - - + +
diff --git a/frontend/src/components/OpeningsTab/index.tsx b/frontend/src/components/OpeningsTab/index.tsx index 6b71fff0..b72bff80 100644 --- a/frontend/src/components/OpeningsTab/index.tsx +++ b/frontend/src/components/OpeningsTab/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Button } from '@carbon/react'; +import { Button, InlineNotification } from '@carbon/react'; import './styles.scss' import { Location } from '@carbon/icons-react'; import OpeningsMap from '../OpeningsMap'; @@ -7,71 +7,53 @@ import OpeningScreenDataTable from '../OpeningScreenDataTable/index'; import { columns } from '../Dashboard/Opening/RecentOpeningsDataTable/testData'; import SectionTitle from '../SectionTitle'; import TableSkeleton from '../TableSkeleton'; -import { InlineNotification } from '@carbon/react'; import { RecentOpening } from '../../types/RecentOpening'; -import { useSelector } from 'react-redux'; -import { RootState } from '../../store'; import { generateHtmlFile } from './layersGenerator'; import { getWmsLayersWhitelistUsers, WmsLayersWhitelistUser } from '../../services/SecretsService'; -import { useUserRecentOpeningQuery } from '../../services/queries/search/openingQueries'; -import RecentOpeningsDataTable from '../Dashboard/Opening/RecentOpeningsDataTable'; -import { ITableHeader } from '../../types/TableHeader'; +import { useGetAuth } from '../../contexts/AuthProvider'; interface Props { showSpatial: boolean; - setShowSpatial: Function; + setShowSpatial: (show: boolean) => void; } const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { const [loadId, setLoadId] = useState(null); const [openingPolygonNotFound, setOpeningPolygonNotFound] = useState(false); - const [wmsUsersWhitelist, setWmsUsersWhitelist] = useState([]); - const userDetails = useSelector((state: RootState) => state.userDetails); - const { data, isFetching } = useUserRecentOpeningQuery(10); - const [headers, setHeaders] = useState(columns); + const [wmsUsersWhitelist, setWmsUsersWhitelist] = useState([]); + const { user } = useGetAuth(); - + useEffect(() => { + const fetchData = async () => { + try { + const rows: RecentOpening[] = await fetchRecentOpenings(); + setOpeningRows(rows); + setLoading(false); + setError(null); + } catch (error) { + console.error('Error fetching recent openings:', error); + setLoading(false); + setError('Failed to fetch recent openings'); + } + }; - useEffect(() => {}, [loadId, openingPolygonNotFound, wmsUsersWhitelist]); + const fetchAllowedPeople = async () => { + try { + const usersList: WmsLayersWhitelistUser[] = await getWmsLayersWhitelistUsers(); + setWmsUsersWhitelist(usersList); + } catch (error) { + console.error('Error fetching recent openings:', error); + } + }; - const toggleSpatial = () => { - setShowSpatial((prevShowSpatial :boolean) => !prevShowSpatial); - }; + fetchData(); + fetchAllowedPeople(); + }, []); - const onClickFn = () => { - const allowed: string[] = wmsUsersWhitelist.map((user: WmsLayersWhitelistUser) => user.userName); - const { userName } = userDetails.user; - if (allowed.includes(userName)) { - const newWindow = window.open(); - if (newWindow) { - newWindow.document.body.innerHTML = generateHtmlFile(); - } - } - }; + useEffect(() => {}, [loadId, openingPolygonNotFound, wmsUsersWhitelist]); - const handleCheckboxChange = (columnKey: string) => { - if(columnKey === "select-default"){ - //set to the deafult - setHeaders(columns) - } - else if(columnKey === "select-all"){ - setHeaders((prevHeaders) => - prevHeaders.map((header) => ({ - ...header, - selected: true, // Select all headers - })) - ); - } - else{ - setHeaders((prevHeaders) => - prevHeaders.map((header) => - header.key === columnKey - ? { ...header, selected: !header.selected } - : header - ) - ); - } - + const toggleSpatial = () => { + setShowSpatial(!showSpatial); }; return ( @@ -81,7 +63,6 @@ const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { - -
-
-
- - ); - }; - -export default Help; \ No newline at end of file diff --git a/frontend/src/screens/Landing/index.tsx b/frontend/src/screens/Landing/index.tsx index 22d0668f..4c9416a9 100644 --- a/frontend/src/screens/Landing/index.tsx +++ b/frontend/src/screens/Landing/index.tsx @@ -25,58 +25,56 @@ const Landing: React.FC = () => { } return ( - <> -
-
-
-
- -
+
+
+
+
+ +
- {/* Welcome - Title and Subtitle */} -
-

Welcome to SILVA

-

- Plan, report, and analyze your silviculture activities -

+ {/* Welcome - Title and Subtitle */} +
+

Welcome to SILVA

+

+ Plan, report, and analyze your silviculture activities +

+
+ {/* Button Group */} +
+
+
- {/* Button Group */} -
-
- -
-
- -
+
+
+
+
+
+
+ {View}
-
- +
); }; diff --git a/frontend/src/screens/Opening/index.tsx b/frontend/src/screens/Opening/index.tsx index b5220d72..7ecdc3be 100644 --- a/frontend/src/screens/Opening/index.tsx +++ b/frontend/src/screens/Opening/index.tsx @@ -2,11 +2,13 @@ import React, {useEffect, useState} from "react"; import FavouriteCard from "../../components/FavouriteCard"; import PageTitle from "../../components/PageTitle"; import './Opening.scss' -import { TabList } from "@carbon/react"; -import { Tabs } from "@carbon/react"; -import { Tab } from "@carbon/react"; -import { TabPanels } from "@carbon/react"; -import { TabPanel } from "@carbon/react"; +import { + TabList, + Tabs, + Tab, + TabPanels, + TabPanel +} from "@carbon/react"; import OpeningsTab from "../../components/OpeningsTab"; import OpeningMetricsTab from "../../components/OpeningMetricsTab"; diff --git a/frontend/src/screens/SilvicultureSearch/index.tsx b/frontend/src/screens/SilvicultureSearch/index.tsx index faa59ccb..c50dae59 100644 --- a/frontend/src/screens/SilvicultureSearch/index.tsx +++ b/frontend/src/screens/SilvicultureSearch/index.tsx @@ -1,14 +1,15 @@ import React from "react"; import PageTitle from "../../components/PageTitle"; import './SilvicultureSearch.scss' -import { TabList } from "@carbon/react"; -import { Tabs } from "@carbon/react"; -import { Tab } from "@carbon/react"; -import { TabPanels } from "@carbon/react"; -import { TabPanel } from "@carbon/react"; +import { + TabList, + Tabs, + Tab, + TabPanels, + TabPanel +} from "@carbon/react"; import * as Icons from '@carbon/icons-react'; import OpeningsSearchTab from "../../components/SilvicultureSearch/Openings/OpeningsSearchTab"; -import { OpeningsSearchProvider } from "../../contexts/search/OpeningsSearch"; const SilvicultureSearch: React.FC = () => { @@ -26,9 +27,9 @@ const SilvicultureSearch: React.FC = () => {
Openings
-
Activities
-
Stocking standards
-
Standard units
+
Activities
+
Stocking standards
+
Standard units
diff --git a/frontend/src/store.ts b/frontend/src/store.ts deleted file mode 100644 index edba420c..00000000 --- a/frontend/src/store.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createStore, combineReducers, applyMiddleware } from 'redux' -import { thunk } from 'redux-thunk' -import { composeWithDevTools } from '@redux-devtools/extension' -import { userDetailsReducer } from './reducers/userReducer' -import { UserClientRolesType } from './types/UserRoleType' -import { selectedClientRolesReducer } from './reducers/selectedClientRolesReducer' -import { FamLoginUser } from './services/AuthService' - -const reducer = combineReducers({ - userDetails: userDetailsReducer, - selectedClientRoles: selectedClientRolesReducer -}); - -const FAM_LOGIN_USER = 'famLoginUser'; - - -const userInfoFromStorage = JSON.parse(localStorage.getItem(FAM_LOGIN_USER) as string) as - | FamLoginUser - | undefined - | null; - -const selectedClientRolesFromStorage = JSON.parse(localStorage.getItem('selectedClientRoles') as string) as - | UserClientRolesType - | undefined - | null; - -// set the initial state -const initialState: object = { - userDetails: { - user: { - ...userInfoFromStorage, - isLoggedIn: !!userInfoFromStorage?.authToken - }, - loading: true, - error: false - }, - selectedClientRoles: selectedClientRolesFromStorage -}; - -const middleware = [thunk]; - -const store = createStore( - reducer, - initialState, - composeWithDevTools(applyMiddleware(...middleware)) -); - -export type RootState = ReturnType; -export type AppDispatch = typeof store.dispatch; - -export default store; diff --git a/frontend/src/types/NotificationType.ts b/frontend/src/types/NotificationType.ts new file mode 100644 index 00000000..0e0e3bd5 --- /dev/null +++ b/frontend/src/types/NotificationType.ts @@ -0,0 +1,8 @@ +export interface NotificationContent { + title: string; + subTitle: string; + buttonLabel?: string; + dismissIn?: number; + type: 'info' | 'error' | 'success' | 'warning' | 'info-square' | 'error-square' | 'warning-alt'; + onClose: () => void; +} \ No newline at end of file diff --git a/frontend/src/utils/ThemePreference.tsx b/frontend/src/utils/ThemePreference.tsx index fb95ab1e..bddecb21 100644 --- a/frontend/src/utils/ThemePreference.tsx +++ b/frontend/src/utils/ThemePreference.tsx @@ -20,7 +20,7 @@ function useThemePreference() { } interface ThemePreferenceProps { - children?: ReactNode + readonly children?: ReactNode } /** diff --git a/frontend/src/views/LoginOrgSelection/index.tsx b/frontend/src/views/LoginOrgSelection/index.tsx deleted file mode 100644 index 901782e2..00000000 --- a/frontend/src/views/LoginOrgSelection/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useEffect } from 'react'; -import { FlexGrid, Row, Column } from '@carbon/react'; -import OrganizationSelection from '../../components/OrganizationSelection'; -import { RootState } from '../../store'; -import { useSelector } from 'react-redux'; -import './styles.scss'; - -/** - * Renders the organization selection view after login. - * - * This view allows users to select which organization they are representing. - * It displays the user's name and username, prompting them to choose an organization. - * - * @returns {JSX.Element} The rendered LoginOrgSelection component. - */ -function LoginOrgSelection (): JSX.Element { - const userDetails = useSelector((state: RootState) => state.userDetails); - const user = userDetails.user; - - return ( - - - -
-

- Organization selection -

-

- {`${user?.firstName} ${user?.lastName} ${user.providerUsername ? "("+user.providerUsername+")": ""} select which organization you're representing.`} -

-
- -
-
-
- ); -}; - -export default LoginOrgSelection; diff --git a/frontend/src/views/LoginOrgSelection/styles.scss b/frontend/src/views/LoginOrgSelection/styles.scss deleted file mode 100644 index db688f12..00000000 --- a/frontend/src/views/LoginOrgSelection/styles.scss +++ /dev/null @@ -1,48 +0,0 @@ -@use '@carbon/type'; -@use '@bcgov-nr/nr-theme/design-tokens/variables.scss' as vars; - -.login-org-selection-grid { - background: lightgray; // temporary change for demo linear-gradient(180deg, #E0EFFF 0%, #F2EEDB 100%); - height: 100vh; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - margin-inline: 0; - max-inline-size: none; - - .row-container { - max-width: none; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - } - - .col-container { - display: flex; - flex-direction: column; - padding: min(4.63%, 3rem) min(4.63%, 4rem); - border-radius: 0.5rem; - background: var(--bx-layer-02); - /* Light Theme/Shadows/Menu */ - box-shadow: 0rem 0.125rem 0.375rem 0rem rgba(0, 0, 0, 0.30); - min-height: 50vh; - } - - .title-section { - text-align: left; - margin-bottom: 2.5rem; - - .title-text { - margin-bottom: 1rem; - font-size: 2.625rem; - } - - .subtitle-text { - @include type.type-style('body-01'); - color: var(--#{vars.$bcgov-prefix}-text-secondary); - font-size: 1rem; - } - } -} From 9f4558e7d187c4f5947dcdadd8c5001dd1d93f52 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 29 Oct 2024 12:01:08 -0700 Subject: [PATCH 36/43] transferred the frontend changes from the old recentOpening branch --- frontend/src/App.tsx | 71 +++--- .../src/components/BCHeaderwSide/constants.ts | 14 +- .../src/components/BarChartGrouped/index.tsx | 2 +- .../Opening/RecentOpeningsDataTable/index.tsx | 3 + .../RecentOpeningsDataTable/testData.ts | 231 +++++++++++++++++- frontend/src/components/OpeningsTab/index.tsx | 64 ++++- .../Openings/SearchScreenDataTable/index.tsx | 91 ++++--- .../src/screens/DashboardRedirect/index.tsx | 30 ++- frontend/src/services/OpeningService.ts | 52 +++- .../queries/dashboard/dashboardQueries.ts | 21 +- frontend/src/services/search/openings.ts | 2 +- frontend/src/utils/DateUtils.ts | 8 + 12 files changed, 487 insertions(+), 102 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3a618220..486a4ce4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,49 +1,46 @@ -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { + BrowserRouter, Routes, Route +} from 'react-router-dom'; +import { Amplify } from 'aws-amplify'; +import amplifyconfig from './amplifyconfiguration'; + import './custom.scss'; + import Landing from "./screens/Landing"; +import Help from "./screens/Help"; import SideLayout from './layouts/SideLayout'; +import PostLoginRoute from './routes/PostLoginRoute'; import ProtectedRoute from './routes/ProtectedRoute'; import Opening from './screens/Opening'; import DashboardRedirect from './screens/DashboardRedirect'; import SilvicultureSearch from './screens/SilvicultureSearch'; -import ErrorHandling from './screens/ErrorHandling'; - -// Create the router instance -const router = createBrowserRouter([ - { - path: "/", - element: , - errorElement: // Handle errors for the Landing page route - }, - { - path: "/dashboard", - element: , - errorElement: // Handle errors for the dashboard route - }, - { - element: , - errorElement: , // Global error element for protected routes - children: [ - { - path: "/opening", - element: } /> - }, - { - path: "/silviculture-search", - element: } /> - } - ] - }, - // Catch-all route for unmatched paths - { - path: "*", - element: - } -]); +Amplify.configure(amplifyconfig); const App: React.FC = () => { - return ; + return ( + + + } /> + + + + } /> + + } /> + + } /> + + } /> + + } /> + } />} /> + + + ); }; -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/components/BCHeaderwSide/constants.ts b/frontend/src/components/BCHeaderwSide/constants.ts index 074342bc..08e63d00 100644 --- a/frontend/src/components/BCHeaderwSide/constants.ts +++ b/frontend/src/components/BCHeaderwSide/constants.ts @@ -16,7 +16,7 @@ export type LeftMenu = { const mainActivitiesItems: LeftMenu[] = [ { name: 'Main activities', - items: [ + items: [ { name: 'Opening', icon: 'MapBoundaryVegetation', @@ -32,6 +32,16 @@ const mainActivitiesItems: LeftMenu[] = [ name: 'Silviculture search', link: '/silviculture-search', disabled: false + }, + { + name: 'Create an opening', + link: '/opening/create', + disabled: true + }, + { + name: 'Upcoming activities', + link: '/opening/upcoming-activities', + disabled: true } ] } @@ -62,4 +72,4 @@ const managementItems: LeftMenu[] = [ export const leftMenu: LeftMenu[] = [ ...mainActivitiesItems, ...managementItems -]; +]; \ No newline at end of file diff --git a/frontend/src/components/BarChartGrouped/index.tsx b/frontend/src/components/BarChartGrouped/index.tsx index c395c2f8..d5a38a2b 100644 --- a/frontend/src/components/BarChartGrouped/index.tsx +++ b/frontend/src/components/BarChartGrouped/index.tsx @@ -1,5 +1,5 @@ // components/BarChartGrouped.tsx -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { GroupedBarChart, ScaleTypes } from "@carbon/charts-react"; import { Dropdown, DatePicker, DatePickerInput } from "@carbon/react"; import { useDistrictListQuery, useFetchOpeningsPerYear } from "../../services/queries/dashboard/dashboardQueries"; diff --git a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx index bd74fccc..772a6a9c 100644 --- a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx +++ b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/index.tsx @@ -13,6 +13,8 @@ import { TableRow, Button, Pagination, + OverflowMenu, + OverflowMenuItem, Popover, PopoverContent, Checkbox, @@ -88,6 +90,7 @@ const RecentOpeningsDataTable: React.FC = ({ //Function to handle the favourite feature of the opening for a user const handleFavouriteOpening = (rowId: string) => { + console.log(rowId + " has been added as a favourite for the user") //make a call to the api for the favourite opening when ready setToastText(`Following "OpeningID ${rowId}"`); } diff --git a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts index 4985d07e..a3e5d814 100644 --- a/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts +++ b/frontend/src/components/Dashboard/Opening/RecentOpeningsDataTable/testData.ts @@ -52,4 +52,233 @@ export const columns: ITableHeader[] = [ header: 'Actions', selected: true } -]; \ No newline at end of file +]; + + +export const rows:any = [ + { + id: '114207', + openingId: '114207', + fileId: 'TFL47', + cuttingPermit: '12S', + timberMark: '47/12S', + cutBlock: '12-69', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-10-27', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114206', + openingId: '114206', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-69', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-09-04', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114205', + openingId: '114205', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-09-04', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114204', + openingId: '114204', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-01-16', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-26' + }, + { + id: '114203', + openingId: '114203', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-12-08', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-26' + }, + { + id: '114202', + openingId: '114202', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-11-15', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-25' + }, + { + id: '114201', + openingId: '114201', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-11-15', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-25' + }, + { + id: '114200', + openingId: '114200', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-10-20', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-24' + }, + { + id: '114199', + openingId: '114199', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-10-20', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-24' + }, + { + id: '114198', + openingId: '114198', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-09-12', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-23' + }, + { + id: '114197', + openingId: '114197', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-09-12', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-23' + }, + { + id: '114196', + openingId: '114196', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-08-05', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-22' + }, + { + id: '114195', + openingId: '114195', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-08-05', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-22' + }, + { + id: '114194', + openingId: '114194', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-07-10', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-21' + }, + { + id: '114193', + openingId: '114193', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-07-10', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-21' + } +]; diff --git a/frontend/src/components/OpeningsTab/index.tsx b/frontend/src/components/OpeningsTab/index.tsx index b72bff80..d80bc70c 100644 --- a/frontend/src/components/OpeningsTab/index.tsx +++ b/frontend/src/components/OpeningsTab/index.tsx @@ -1,27 +1,38 @@ import React, { useEffect, useState } from 'react'; -import { Button, InlineNotification } from '@carbon/react'; +import { Button } from '@carbon/react'; import './styles.scss' import { Location } from '@carbon/icons-react'; import OpeningsMap from '../OpeningsMap'; import OpeningScreenDataTable from '../OpeningScreenDataTable/index'; import { columns } from '../Dashboard/Opening/RecentOpeningsDataTable/testData'; +import { fetchRecentOpenings } from '../../services/OpeningService'; import SectionTitle from '../SectionTitle'; import TableSkeleton from '../TableSkeleton'; +import { InlineNotification } from '@carbon/react'; import { RecentOpening } from '../../types/RecentOpening'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../store'; import { generateHtmlFile } from './layersGenerator'; import { getWmsLayersWhitelistUsers, WmsLayersWhitelistUser } from '../../services/SecretsService'; -import { useGetAuth } from '../../contexts/AuthProvider'; +import { useUserRecentOpeningQuery } from '../../services/queries/search/openingQueries'; +import RecentOpeningsDataTable from '../Dashboard/Opening/RecentOpeningsDataTable'; +import { ITableHeader } from '../../types/TableHeader'; interface Props { showSpatial: boolean; - setShowSpatial: (show: boolean) => void; + setShowSpatial: Function; } const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { + const [loading, setLoading] = useState(true); + const [openingRows, setOpeningRows] = useState([]); + const [error, setError] = useState(null); const [loadId, setLoadId] = useState(null); const [openingPolygonNotFound, setOpeningPolygonNotFound] = useState(false); - const [wmsUsersWhitelist, setWmsUsersWhitelist] = useState([]); - const { user } = useGetAuth(); + const [wmsUsersWhitelist, setWmsUsersWhitelist] = useState([]); + const userDetails = useSelector((state: RootState) => state.userDetails); + const { data, isFetching } = useUserRecentOpeningQuery(10); + const [headers, setHeaders] = useState(columns); useEffect(() => { const fetchData = async () => { @@ -53,7 +64,43 @@ const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { useEffect(() => {}, [loadId, openingPolygonNotFound, wmsUsersWhitelist]); const toggleSpatial = () => { - setShowSpatial(!showSpatial); + setShowSpatial((prevShowSpatial :boolean) => !prevShowSpatial); + }; + + const onClickFn = () => { + const allowed: string[] = wmsUsersWhitelist.map((user: WmsLayersWhitelistUser) => user.userName); + const { userName } = userDetails.user; + if (allowed.includes(userName)) { + const newWindow = window.open(); + if (newWindow) { + newWindow.document.body.innerHTML = generateHtmlFile(); + } + } + }; + + const handleCheckboxChange = (columnKey: string) => { + if(columnKey === "select-default"){ + //set to the deafult + setHeaders(columns) + } + else if(columnKey === "select-all"){ + setHeaders((prevHeaders) => + prevHeaders.map((header) => ({ + ...header, + selected: true, // Select all headers + })) + ); + } + else{ + setHeaders((prevHeaders) => + prevHeaders.map((header) => + header.key === columnKey + ? { ...header, selected: !header.selected } + : header + ) + ); + } + }; return ( @@ -63,6 +110,7 @@ const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => {