From 32c6eda6b73b236bff7a88ad2be3462a990cf61a Mon Sep 17 00:00:00 2001 From: Jazz Grewal <39718912+jazzgrewal@users.noreply.github.com> Date: Wed, 6 Nov 2024 15:59:31 -0800 Subject: [PATCH] fix(SILVA-550): Enhance Org Unit and Category Selection with Filterable Multi-Select Dropdown (#442) Co-authored-by: Paulo Gomes da Cruz Junior Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../oracle/dto/OpeningSearchFiltersDto.java | 26 +++-- .../endpoint/OpeningSearchEndpoint.java | 4 +- .../repository/OpeningSearchRepository.java | 18 +-- .../OpeningSearchRepositoryTest.java | 18 +-- .../oracle/service/OpeningServiceTest.java | 4 +- .../Openings/OpeningSearchBar.test.tsx | 70 ++++++++++- .../__test__/contexts/OpeningsSearch.test.tsx | 56 +++++++++ .../services/search/openings.test.tsx | 109 ++++++++++++++++++ .../AdvancedSearchDropdown.scss | 4 + .../Openings/AdvancedSearchDropdown/index.tsx | 97 +++++++++------- .../Openings/OpeningsSearchBar/index.tsx | 11 +- .../Openings/OpeningsSearchTab/index.tsx | 5 - .../src/contexts/search/OpeningsSearch.tsx | 4 +- frontend/src/services/search/openings.ts | 9 +- 14 files changed, 350 insertions(+), 85 deletions(-) create mode 100644 frontend/src/__test__/contexts/OpeningsSearch.test.tsx create mode 100644 frontend/src/__test__/services/search/openings.test.tsx diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java index 964e2553..0753785e 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/dto/OpeningSearchFiltersDto.java @@ -14,8 +14,8 @@ @Getter @ToString public class OpeningSearchFiltersDto { - private final String orgUnit; - private final String category; + private final List orgUnit; + private final List category; private final List statusList; private final Boolean myOpenings; private final Boolean submittedToFrpa; @@ -39,8 +39,8 @@ public class OpeningSearchFiltersDto { /** Creates an instance of the search opening filter dto. */ public OpeningSearchFiltersDto( - String orgUnit, - String category, + List orgUnit, + List category, List statusList, Boolean myOpenings, Boolean submittedToFrpa, @@ -56,8 +56,18 @@ public OpeningSearchFiltersDto( String cutBlockId, String timberMark, String mainSearchTerm) { - this.orgUnit = Objects.isNull(orgUnit) ? null : orgUnit.toUpperCase().trim(); - this.category = Objects.isNull(category) ? null : category.toUpperCase().trim(); + this.orgUnit = new ArrayList<>(); + if (!Objects.isNull(orgUnit)) { + this.orgUnit.addAll(orgUnit.stream() + .map(s -> String.format("'%s'", s.toUpperCase().trim())) + .toList()); + } + this.category = new ArrayList<>(); + if (!Objects.isNull(category)) { + this.category.addAll(category.stream() + .map(s -> String.format("'%s'", s.toUpperCase().trim())) + .toList()); + } this.statusList = new ArrayList<>(); this.openingIds = new ArrayList<>(); if (!Objects.isNull(statusList)) { @@ -114,8 +124,8 @@ public OpeningSearchFiltersDto( */ public boolean hasValue(String prop) { return switch (prop) { - case SilvaOracleConstants.ORG_UNIT -> !Objects.isNull(this.orgUnit); - case SilvaOracleConstants.CATEGORY -> !Objects.isNull(this.category); + case SilvaOracleConstants.ORG_UNIT -> !this.orgUnit.isEmpty(); + case SilvaOracleConstants.CATEGORY -> !this.category.isEmpty(); case SilvaOracleConstants.STATUS_LIST -> !this.statusList.isEmpty(); case SilvaOracleConstants.OPENING_IDS -> !this.openingIds.isEmpty(); case SilvaOracleConstants.MY_OPENINGS -> !Objects.isNull(this.myOpenings); diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningSearchEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningSearchEndpoint.java index 8a90bdbb..69f316ca 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningSearchEndpoint.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/endpoint/OpeningSearchEndpoint.java @@ -63,9 +63,9 @@ public PaginatedResult openingSearch( @RequestParam(value = "mainSearchTerm", required = false) String mainSearchTerm, @RequestParam(value = "orgUnit", required = false) - String orgUnit, + List orgUnit, @RequestParam(value = "category", required = false) - String category, + List category, @RequestParam(value = "statusList", required = false) List statusList, @RequestParam(value = "myOpenings", required = false) diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java index 0a6feb71..25848e08 100644 --- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java +++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepository.java @@ -254,13 +254,13 @@ private Query setQueryParameters(OpeningSearchFiltersDto filtersDto, String nati // 1. Org Unit code if (filtersDto.hasValue(SilvaOracleConstants.ORG_UNIT)) { - log.info("Setting orgUnit filter value"); - query.setParameter("orgUnit", filtersDto.getOrgUnit()); + log.info("Setting orgUnit filter values"); + // No need to set value since the query already dit it. Didn't work set through named param } // 2. Category code if (filtersDto.hasValue(SilvaOracleConstants.CATEGORY)) { - log.info("Setting category filter value"); - query.setParameter("category", filtersDto.getCategory()); + log.info("Setting category filter values"); + // No need to set value since the query already dit it. Didn't work set through named param } // 3. Status list codes if (filtersDto.hasValue(SilvaOracleConstants.STATUS_LIST)) { @@ -427,13 +427,15 @@ private String createNativeSqlQuery(OpeningSearchFiltersDto filtersDto) { // 1. Org Unit code if (filtersDto.hasValue(SilvaOracleConstants.ORG_UNIT)) { - log.info("Filter orgUnit detected! orgUnit={}", filtersDto.getOrgUnit()); - builder.append("AND ou.ORG_UNIT_CODE = :orgUnit "); + String orgUnits = String.join(",", filtersDto.getOrgUnit()); + log.info("Filter orgUnit detected! orgUnit={}", orgUnits); + builder.append(String.format("AND ou.ORG_UNIT_CODE IN (%s) ", orgUnits)); } // 2. Category code if (filtersDto.hasValue(SilvaOracleConstants.CATEGORY)) { - log.info("Filter category detected! category={}", filtersDto.getCategory()); - builder.append("AND o.OPEN_CATEGORY_CODE = :category "); + String categories = String.join(",", filtersDto.getCategory()); + log.info("Filter category detected! statusList={}", categories); + builder.append(String.format("AND o.OPEN_CATEGORY_CODE IN (%s) ", categories)); } // 3. Status code if (filtersDto.hasValue(SilvaOracleConstants.STATUS_LIST)) { diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepositoryTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepositoryTest.java index 2e1c6a0f..6a629696 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepositoryTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/repository/OpeningSearchRepositoryTest.java @@ -42,8 +42,8 @@ class OpeningSearchRepositoryTest { private OpeningSearchRepository openingSearchRepository; private OpeningSearchFiltersDto mockFilter( - String orgUnit, - String category, + List orgUnit, + List category, List statusList, Boolean myOpenings, Boolean submittedToFrpa, @@ -79,7 +79,7 @@ private OpeningSearchFiltersDto mockFilter( mainSearchTerm); } - private OpeningSearchFiltersDto mockOrgUnit(String orgUnit) { + private OpeningSearchFiltersDto mockOrgUnit(List orgUnit) { return mockFilter( orgUnit, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); @@ -87,8 +87,8 @@ private OpeningSearchFiltersDto mockOrgUnit(String orgUnit) { private OpeningSearchFiltersDto mockAllFilters() { return mockFilter( - "DCR", - "FTML", + List.of("DCR"), + List.of("FTML"), List.of("APP"), true, false, @@ -481,7 +481,7 @@ void searchOpeningQuery_mainFilterString_shouldSucceed() { @Test @DisplayName("Search opening query org unit filter should succeed") void searchOpeningQuery_orgUnitFilter_shouldSucceed() { - OpeningSearchFiltersDto filters = mockOrgUnit("DCR"); + OpeningSearchFiltersDto filters = mockOrgUnit(List.of("DCR")); PaginationParameters pagination = new PaginationParameters(0, 10); @@ -575,7 +575,7 @@ void searchOpeningQuery_allFilters_shouldSucceed() { Integer openingId = 123456789; String openingNumber = "589"; - OpeningCategoryEnum category = OpeningCategoryEnum.of(filters.getCategory()); + OpeningCategoryEnum category = OpeningCategoryEnum.of("FTML"); OpeningStatusEnum status = OpeningStatusEnum.of(filters.getStatusList().get(0)); String cuttingPermitId = "123"; String timberMark = "EM2184"; @@ -583,7 +583,7 @@ void searchOpeningQuery_allFilters_shouldSucceed() { BigDecimal openingGrossArea = new BigDecimal("11"); Timestamp disturbanceStartDate = Timestamp.valueOf(LocalDateTime.now()); String forestFileId = "TFL47"; - String orgUnitCode = filters.getOrgUnit(); + String orgUnitCode = "DCR"; String orgUnitName = "Org Name"; String clientNumber = "00012797"; String clientLocation = "00"; @@ -655,7 +655,7 @@ void searchOpeningQuery_allFilters_shouldSucceed() { @Test @DisplayName("Search opening query no records found should succeed") void searchOpeningQuery_noRecordsFound_shouldSucceed() { - OpeningSearchFiltersDto filters = mockOrgUnit("AAA"); + OpeningSearchFiltersDto filters = mockOrgUnit(List.of("AAA")); PaginationParameters pagination = new PaginationParameters(0, 10); diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java index e2db4921..055103f2 100644 --- a/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java +++ b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/service/OpeningServiceTest.java @@ -16,6 +16,8 @@ import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.List; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -106,7 +108,7 @@ void openingSearch_orgUnit_shouldSucceed() { PaginatedResult result = openingService.openingSearch(new OpeningSearchFiltersDto( - "TWO", + List.of("TWO"), null, null, null, diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx index 3f99fe56..8044d8d6 100644 --- a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx +++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx @@ -6,7 +6,7 @@ import "@testing-library/jest-dom"; import OpeningsSearchBar from "../../../../components/SilvicultureSearch/Openings/OpeningsSearchBar"; import { vi } from "vitest"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; +import { OpeningsSearchProvider, useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; // Mock the useOpeningsSearch context to avoid rendering errors vi.mock("../../../../contexts/search/OpeningsSearch", () => ({ @@ -53,4 +53,72 @@ describe("OpeningsSearchBar", () => { // Check if the onSearchClick function was called expect(onSearchClick).toHaveBeenCalled(); }); + + it("should show AdvancedSearchDropdown if isOpen is true", () => { + // Create a mock function to pass as a prop + const onSearchClick = vi.fn(); + const isOpen = false; + // Mock the useState calls + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [true, vi.fn()]) // Mocking isOpen state as false + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false + .mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state + .mockImplementationOnce(() => [0, vi.fn()]) // Mocking filtersCount state + .mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state + render( + + + + ); + + // Check if an element with the class 'd-none' exists within the structure + const dNoneElement = screen.getAllByText("", {selector: ".d-block"})[0]; + expect(dNoneElement).toBeInTheDocument(); + }); + + it("should not show AdvancedSearchDropdown if isOpen is false", () => { + // Create a mock function to pass as a prop + const onSearchClick = vi.fn(); + const isOpen = false; + // Mock the useState calls + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking isOpen state as false + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false + .mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state + .mockImplementationOnce(() => [0, vi.fn()]) // Mocking filtersCount state + .mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state + render( + + + + ); + + // Check if an element with the class 'd-none' exists within the structure + const dNoneElement = screen.getAllByText("", {selector: ".d-none"})[0]; + expect(dNoneElement).toBeInTheDocument(); + }); + + it("should show correct filter count, when count is greater than 0", () => { + // Create a mock function to pass as a prop + const onSearchClick = vi.fn(); + // Mock the useState calls + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking isOpen state as false + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false + .mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state + .mockImplementationOnce(() => [2, vi.fn()]) // Mocking filtersCount state + .mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state + + render( + + + + ); + + console.log(screen.debug()); + + // Check if an element with the class 'd-none' exists within the structure + const dNoneElement = screen.getByText('+2'); + expect(dNoneElement).toBeInTheDocument(); + }); }); \ No newline at end of file diff --git a/frontend/src/__test__/contexts/OpeningsSearch.test.tsx b/frontend/src/__test__/contexts/OpeningsSearch.test.tsx new file mode 100644 index 00000000..810652a8 --- /dev/null +++ b/frontend/src/__test__/contexts/OpeningsSearch.test.tsx @@ -0,0 +1,56 @@ +// OpeningsSearchProvider.test.tsx +import React from 'react'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { OpeningsSearchProvider, useOpeningsSearch } from '../../contexts/search/OpeningsSearch'; + +const TestComponent: React.FC = () => { + const { filters, setFilters, searchTerm, setSearchTerm, clearFilters, clearIndividualField } = useOpeningsSearch(); + + return ( +
+

{searchTerm}

+

{String(filters.startDate)}

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

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

diff --git a/frontend/src/contexts/search/OpeningsSearch.tsx b/frontend/src/contexts/search/OpeningsSearch.tsx index d3aca104..b198c30d 100644 --- a/frontend/src/contexts/search/OpeningsSearch.tsx +++ b/frontend/src/contexts/search/OpeningsSearch.tsx @@ -18,8 +18,8 @@ export const OpeningsSearchProvider: React.FC<{ children: ReactNode }> = ({ chil const defaultFilters = { startDate: null as Date | null, endDate: null as Date | null, - orgUnit: null as string | null, - category: null as string | null, + orgUnit: [] as string[], + category: [] as string[], status: [] as string[], clientAcronym: "", clientLocationCode: "", diff --git a/frontend/src/services/search/openings.ts b/frontend/src/services/search/openings.ts index 2b90bd1b..af648373 100644 --- a/frontend/src/services/search/openings.ts +++ b/frontend/src/services/search/openings.ts @@ -9,10 +9,11 @@ export interface OpeningFilters { searchInput?: string; startDate?: string; endDate?: string; - orgUnit?: string; - category?: string; + orgUnit?: string[]; + category?: string[]; clientAcronym?: string; blockStatus?: string; + dateType?: string; cutBlock?: string; cuttingPermit?: string; grossArea?: string; @@ -62,8 +63,8 @@ export const fetchOpenings = async (filters: OpeningFilters): Promise => { const params = { mainSearchTerm: filters.searchInput, - orgUnit: filters.orgUnit, - category: filters.category, + orgUnit: filters.orgUnit, //Keep it as an array + category: filters.category, // Keep it as an array statusList: filters.status, // Keep it as an array entryUserId: filters.clientAcronym, cutBlockId: filters.cutBlock,