diff --git a/src/controllers/psychotherapist.controller.spec.ts b/src/controllers/psychotherapist.controller.spec.ts index 7ef8e0e..6f3de44 100644 --- a/src/controllers/psychotherapist.controller.spec.ts +++ b/src/controllers/psychotherapist.controller.spec.ts @@ -9,8 +9,10 @@ import { PsychotherapistController } from "./psychotherapist.controller"; Container.set("logger", new MockLogger()); const mockGetPsychotherapists = jest.fn(); +const mockGePsychoTherapistsLocations = jest.fn(); class MockPsychotherapistService { getPsychotherapists = mockGetPsychotherapists; + getTherapistsLocations = mockGePsychoTherapistsLocations; } Container.set(PsychotherapistService, new MockPsychotherapistService()); @@ -58,4 +60,30 @@ describe("The Psychotherapist Controller", () => { new InternalServerError(`failed to get Psychotherapists`) ); }); + + it("should call getTherapistsLocations function", async () => { + mockGePsychoTherapistsLocations.mockResolvedValue([ + "location1", + "location2", + ]); + + let response = await controller.getTherapistsLocations(); + + expect(mockGePsychoTherapistsLocations).toHaveBeenCalledTimes(1); + + expect(response).toEqual(["location1", "location2"]); + }); + + it("should throw an error if getTherapistsLocations throws an error", async () => { + mockGePsychoTherapistsLocations.mockImplementation(() => { + throw new Error("Ugly error"); + }); + + const functionToThrow = async () => + await controller.getTherapistsLocations(); + + await expect(functionToThrow()).rejects.toThrow( + new InternalServerError(`failed to get locations`) + ); + }); }); diff --git a/src/controllers/psychotherapist.controller.ts b/src/controllers/psychotherapist.controller.ts index e42ad0f..12acecf 100644 --- a/src/controllers/psychotherapist.controller.ts +++ b/src/controllers/psychotherapist.controller.ts @@ -36,4 +36,14 @@ export class PsychotherapistController { throw new InternalServerError(`failed to get Psychotherapists`); } } + + @Get("/locations") + async getTherapistsLocations(): Promise { + try { + return this.service.getTherapistsLocations(); + } catch (exception) { + console.error(exception); + throw new InternalServerError(`failed to get locations`); + } + } } diff --git a/src/services/psychotherapist.service.api.ts b/src/services/psychotherapist.service.api.ts index 738f39e..0e82d40 100644 --- a/src/services/psychotherapist.service.api.ts +++ b/src/services/psychotherapist.service.api.ts @@ -2,4 +2,5 @@ import { PsychotherapistResponse } from "src/types/Psychotherapist"; export abstract class PsychotherapistServiceApi { abstract getPsychotherapists(): Promise; + abstract getTherapistsLocations(): Promise; } diff --git a/src/services/psychotherapist.service.spec.ts b/src/services/psychotherapist.service.spec.ts index 11a9798..0990552 100644 --- a/src/services/psychotherapist.service.spec.ts +++ b/src/services/psychotherapist.service.spec.ts @@ -283,6 +283,7 @@ describe("The psychotherapist service", () => { price: ["price"], legalpersonality: ["legalpersonality"], name: ["name"], + location: ["location"], }; await psychotherapistService.getPsychotherapists(mockFilter); @@ -333,7 +334,13 @@ describe("The psychotherapist service", () => { } ); - expect(mockView).toHaveBeenCalledTimes(7); + expect(mockView).toHaveBeenCalledWith( + "therapistsDesignDoc", + "therapistsByLocation", + { keys: ["location"], include_docs: true } + ); + + expect(mockView).toHaveBeenCalledTimes(8); }); it("should return empty array if the view has returned no rows matching our filter", async () => { diff --git a/src/services/psychotherapist.service.ts b/src/services/psychotherapist.service.ts index 5e723cc..52053ca 100644 --- a/src/services/psychotherapist.service.ts +++ b/src/services/psychotherapist.service.ts @@ -78,6 +78,30 @@ export class PsychotherapistService implements PsychotherapistServiceApi { } } + async getTherapistsLocations(): Promise { + try { + const response = await this.psychotherapistDb.view( + "therapistsDesignDoc", + "therapistsByLocation", + { + include_docs: true, + } + ); + + const locations = response.rows.map((row) => { + return row.key; + }); + + // Use Set to remove duplicates and then convert it back to an array + const uniqueLocations = Array.from(new Set(locations)); + + return uniqueLocations; + } catch (error) { + console.log(error); + throw new InternalServerError(error.message); + } + } + async getPsychotherapists( filter?: FilterType ): Promise { @@ -121,6 +145,7 @@ export class PsychotherapistService implements PsychotherapistServiceApi { patientgroups: "therapistsByPatientGroups", price: "therapistsByPrice", legalpersonality: "therapistsByLegalPersonality", + location: "therapistsByLocation", }; const allPsychotherapists = []; @@ -243,6 +268,23 @@ export class PsychotherapistService implements PsychotherapistServiceApi { } } + if (filter.location) { + try { + let psychotherapists = await getTherapistsFromView( + availableViews.location, + filter.location, + this.psychotherapistDb + ); + + allPsychotherapists.push(...psychotherapists); + } catch (error) { + this.logger.error(error); + throw new InternalServerError( + `getPsychotherapists: Failed to retrieve psychotherapists` + ); + } + } + const seen = new Set(); response.psychotherapists = allPsychotherapists.filter((el) => { diff --git a/src/types/Filter.ts b/src/types/Filter.ts index f21b068..5808284 100644 --- a/src/types/Filter.ts +++ b/src/types/Filter.ts @@ -1,4 +1,5 @@ export interface FilterType { + location?: string[]; services?: string[]; appointments?: string[]; languages?: string[]; @@ -12,4 +13,9 @@ export interface FilterQueryParam { languages?: string[] | string; patientgroups?: string[] | string; name?: string[] | string; + location?: string[] | string; + services?: string[] | string; + appointments?: string[] | string; + price?: string[] | string; + legalpersonality?: string[] | string; } diff --git a/src/util/filter-util.ts b/src/util/filter-util.ts index 18e55e2..7356956 100644 --- a/src/util/filter-util.ts +++ b/src/util/filter-util.ts @@ -9,6 +9,7 @@ export function sanitizeFilter(unSafeFilter: any): FilterType { price: "string", legalpersonality: "string", name: "string", + location: "string", }; let cleanFilter: FilterType = {}; diff --git a/web-app/src/constants/directory.js b/web-app/src/constants/directory.js index 971f458..76dda57 100644 --- a/web-app/src/constants/directory.js +++ b/web-app/src/constants/directory.js @@ -211,4 +211,8 @@ export const collapsiblesInitial = { label: "Name of mental health professional or organization", value: "", }, + location: { + label: "Location", + selectValue: "", + }, }; diff --git a/web-app/src/pages/directory/components/Sidebar.js b/web-app/src/pages/directory/components/Sidebar.js index f9f76bc..095ae46 100644 --- a/web-app/src/pages/directory/components/Sidebar.js +++ b/web-app/src/pages/directory/components/Sidebar.js @@ -9,7 +9,7 @@ import SearchIcon from "../../../assets/images/Search.svg"; import Select from "react-select"; import { useEffect } from "react"; -const Sidebar = ({ onFilterChange }) => { +const Sidebar = ({ onFilterChange, locations }) => { const [collapsibles, setCollapsibles] = useState(collapsiblesInitial); useEffect(() => { @@ -69,6 +69,40 @@ const Sidebar = ({ onFilterChange }) => { ); + } else if (collapsibles[key].selectValue !== undefined) { + return ( + + + + +
+ + +
+ + + + + diff --git a/web-app/src/pages/directory/directoryPage.js b/web-app/src/pages/directory/directoryPage.js index eb16b7e..53f4d4e 100644 --- a/web-app/src/pages/directory/directoryPage.js +++ b/web-app/src/pages/directory/directoryPage.js @@ -14,7 +14,10 @@ import PhoneIcon from "../../assets/images/Phone.svg"; import FreeIcon from "../../assets/images/Free.svg"; import GlobalIcon from "../../assets/images/Global.svg"; import VirtualIcon from "../../assets/images/Virtual.svg"; -import { getTherapists } from "../../services/therapists.service"; +import { + getTherapists, + getTherapistLocations, +} from "../../services/therapists.service"; const TherapistCard = (props) => { return ( @@ -203,6 +206,7 @@ const TherapistCard = (props) => { const DirectoryPage = () => { const [therapists, setTherapists] = useState([]); + const [therapistLocations, setTherapistLocations] = useState([]); const retrieveTherapists = async (filter) => { if (filter) { @@ -215,6 +219,8 @@ const DirectoryPage = () => { .map((option) => option.value); } else if (filter[filterKey].value) { adaptedFilter[filterKey] = [filter[filterKey].value]; + } else if (filter[filterKey].selectValue) { + adaptedFilter[filterKey] = [filter[filterKey].selectValue]; } } @@ -226,8 +232,19 @@ const DirectoryPage = () => { } }; + const retrieveTherapistLocations = async () => { + try { + const result = await getTherapistLocations(); + setTherapistLocations(result); + } catch (error) { + console.error("Failed to retrieve therapist locations:", error); + setTherapistLocations([]); + } + }; + useEffect(() => { retrieveTherapists(); + retrieveTherapistLocations(); }, []); const TherapistsList = useCallback(() => { @@ -268,11 +285,14 @@ const DirectoryPage = () => { - { - retrieveTherapists(filter); - }} - /> + {therapistLocations.length > 0 && ( + { + retrieveTherapists(filter); + }} + locations={therapistLocations} + /> + )} diff --git a/web-app/src/pages/directory/directoryPage.spec.js b/web-app/src/pages/directory/directoryPage.spec.js index 8bc43e4..cae343d 100644 --- a/web-app/src/pages/directory/directoryPage.spec.js +++ b/web-app/src/pages/directory/directoryPage.spec.js @@ -36,6 +36,10 @@ jest.mock("./components/Sidebar", () => { jest.mock("../../services/therapists.service"); const getTherapistsSpy = jest.spyOn(therapistService, "getTherapists"); +const getTherapistsLocationsSpy = jest.spyOn( + therapistService, + "getTherapistsLocations" +); Date.now = jest.fn(() => 1482363367071); @@ -71,6 +75,8 @@ const mockTherapistsResponse = [ }, ]; +const mockLocationsResponse = ["beirut", "england"]; + afterEach(() => { jest.clearAllMocks(); }); @@ -78,6 +84,7 @@ afterEach(() => { describe("the DirectoryPage component", () => { fit("should match the snapshot", async () => { getTherapistsSpy.mockResolvedValue(mockTherapistsResponse); + getTherapistsLocationsSpy.mockResolvedValue(mockLocationsResponse); const { container } = render(); await wait(); @@ -86,6 +93,7 @@ describe("the DirectoryPage component", () => { fit("should retrieve therapists when filter executes onFilterChange", async () => { getTherapistsSpy.mockResolvedValue(mockTherapistsResponse); + getTherapistsLocationsSpy.mockResolvedValue(mockLocationsResponse); const { getByTestId } = render(); fireEvent.click(getByTestId("sidebar")); diff --git a/web-app/src/services/therapists.service.js b/web-app/src/services/therapists.service.js index e892cad..7d8df86 100644 --- a/web-app/src/services/therapists.service.js +++ b/web-app/src/services/therapists.service.js @@ -21,3 +21,17 @@ export async function getTherapists(filter) { throw new Error("Failed to fetch therapists"); } } + +export async function getTherapistLocations() { + try { + const response = await axios.get("/api/psychotherapists/locations"); + + if (!Array.isArray(response.data)) { + throw new Error("Invalid response"); + } + + return response.data; + } catch (error) { + throw new Error("Failed to fetch therapist locations"); + } +}