From 57c926afddd6ff8b630ee2048257a596010454ee Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Tue, 29 Oct 2024 12:01:08 -0700 Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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 + } +}