Skip to content

Commit

Permalink
added ability to filter by location
Browse files Browse the repository at this point in the history
  • Loading branch information
matejoslav committed Jan 15, 2024
1 parent 1f1a18d commit 215b823
Show file tree
Hide file tree
Showing 14 changed files with 311 additions and 12 deletions.
28 changes: 28 additions & 0 deletions src/controllers/psychotherapist.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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`)
);
});
});
10 changes: 10 additions & 0 deletions src/controllers/psychotherapist.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,14 @@ export class PsychotherapistController {
throw new InternalServerError(`failed to get Psychotherapists`);
}
}

@Get("/locations")
async getTherapistsLocations(): Promise<string[]> {
try {
return this.service.getTherapistsLocations();
} catch (exception) {
console.error(exception);
throw new InternalServerError(`failed to get locations`);
}
}
}
1 change: 1 addition & 0 deletions src/services/psychotherapist.service.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { PsychotherapistResponse } from "src/types/Psychotherapist";

export abstract class PsychotherapistServiceApi {
abstract getPsychotherapists(): Promise<PsychotherapistResponse>;
abstract getTherapistsLocations(): Promise<string[]>;
}
9 changes: 8 additions & 1 deletion src/services/psychotherapist.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ describe("The psychotherapist service", () => {
price: ["price"],
legalpersonality: ["legalpersonality"],
name: ["name"],
location: ["location"],
};

await psychotherapistService.getPsychotherapists(mockFilter);
Expand Down Expand Up @@ -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 () => {
Expand Down
42 changes: 42 additions & 0 deletions src/services/psychotherapist.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,30 @@ export class PsychotherapistService implements PsychotherapistServiceApi {
}
}

async getTherapistsLocations(): Promise<string[]> {
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<PsychotherapistResponse> {
Expand Down Expand Up @@ -121,6 +145,7 @@ export class PsychotherapistService implements PsychotherapistServiceApi {
patientgroups: "therapistsByPatientGroups",
price: "therapistsByPrice",
legalpersonality: "therapistsByLegalPersonality",
location: "therapistsByLocation",
};

const allPsychotherapists = [];
Expand Down Expand Up @@ -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) => {
Expand Down
6 changes: 6 additions & 0 deletions src/types/Filter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface FilterType {
location?: string[];
services?: string[];
appointments?: string[];
languages?: string[];
Expand All @@ -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;
}
1 change: 1 addition & 0 deletions src/util/filter-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function sanitizeFilter(unSafeFilter: any): FilterType {
price: "string",
legalpersonality: "string",
name: "string",
location: "string",
};

let cleanFilter: FilterType = {};
Expand Down
4 changes: 4 additions & 0 deletions web-app/src/constants/directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,8 @@ export const collapsiblesInitial = {
label: "Name of mental health professional or organization",
value: "",
},
location: {
label: "Location",
selectValue: "",
},
};
36 changes: 35 additions & 1 deletion web-app/src/pages/directory/components/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -69,6 +69,40 @@ const Sidebar = ({ onFilterChange }) => {
</SearchInputContainer>
</Collapsible>
);
} else if (collapsibles[key].selectValue !== undefined) {
return (
<Collapsible
key={`${key}_${index}`}
trigger={collapsibles[key].label}
>
<Select
isSearchable={true}
isClearable={true}
placeholder="Select location"
onChange={(e) => {
if (e === null) {
let collapsiblesCopy = { ...collapsibles };
collapsiblesCopy[key].selectValue = "";
setCollapsibles({ ...collapsiblesCopy });
return;
}
let collapsiblesCopy = { ...collapsibles };
collapsiblesCopy[key].selectValue = e.value;
setCollapsibles({ ...collapsiblesCopy });
}}
options={locations.map((location) => {
return {
value: location,
label: location
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" "),
};
})}
/>
{/* </div> */}
</Collapsible>
);
}
return null;
});
Expand Down
30 changes: 26 additions & 4 deletions web-app/src/pages/directory/components/Sidebar.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ let getByText, getByLabelText;
beforeEach(() => {
jest.clearAllMocks();

({ getByText, getByLabelText } = render(<Sidebar />));
({ getByText, getByLabelText } = render(
<Sidebar locations={["beirut", "england"]} />
));
});

describe("the Sidebar component", () => {
it("should match the snapshot", () => {
const { container } = render(<Sidebar />);
const { container } = render(<Sidebar locations={["beirut", "england"]} />);
expect(container).toMatchSnapshot();
});

Expand Down Expand Up @@ -99,8 +101,26 @@ describe("the Sidebar component", () => {
}
});

// it("should be able to select location", () => {
// const locationCollapsible = getByText(collapsiblesInitial.location.label);

// act(() => {
// fireEvent.click(locationCollapsible);
// });

// const locationCheckbox = getByLabelText("Beirut");

// act(() => {
// fireEvent.click(locationCheckbox);
// });

// expect(locationCheckbox.checked).toBe(true);
// });

xit("should be able to click on collapsible to show more options", async () => {
const { container, getByText } = render(<Sidebar />);
const { container, getByText } = render(
<Sidebar locations={["beirut", "england"]} />
);
let clickable = getByText("Are you looking for a centre or individual?");

act(() => {
Expand All @@ -114,7 +134,9 @@ describe("the Sidebar component", () => {
});

xit("should be able to select/deselect option from the list", () => {
const { container, getByText, getByTestId } = render(<Sidebar />);
const { container, getByText, getByTestId } = render(
<Sidebar locations={["beirut", "england"]} />
);
let clickable = getByText("Are you looking for a centre or individual?");

act(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1520,6 +1520,108 @@ exports[`the Sidebar component should match the snapshot 1`] = `
</div>
</div>
</div>
<div
class="Collapsible"
>
<span
aria-controls="collapsible-content-1482363367071"
aria-disabled="false"
aria-expanded="false"
class="Collapsible__trigger is-closed"
id="collapsible-trigger-1482363367071"
role="button"
>
Location
</span>
<div
aria-labelledby="collapsible-trigger-1482363367071"
class="Collapsible__contentOuter"
id="collapsible-content-1482363367071"
role="region"
style="height: 0px; transition: height 400ms linear; overflow: hidden;"
>
<div
class="Collapsible__contentInner"
>
<div
class=" css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-3-live-region"
/>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class=" css-1s2u09g-control"
>
<div
class=" css-319lph-ValueContainer"
>
<div
class=" css-14el2xx-placeholder"
id="react-select-3-placeholder"
>
Select location
</div>
<div
class=" css-6j8wv5-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-controls="react-select-3-listbox"
aria-describedby="react-select-3-placeholder"
aria-expanded="false"
aria-haspopup="true"
aria-owns="react-select-3-listbox"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class=""
id="react-select-3-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class=" css-1hb7zxy-IndicatorsContainer"
>
<span
class=" css-1okebmr-indicatorSeparator"
/>
<div
aria-hidden="true"
class=" css-tlfecz-indicatorContainer"
>
<svg
aria-hidden="true"
class="css-tj5bde-Svg"
focusable="false"
height="20"
viewBox="0 0 20 20"
width="20"
>
<path
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
Expand Down
Loading

0 comments on commit 215b823

Please sign in to comment.