diff --git a/backend/Api/Departments/DeparmentController.cs b/backend/Api/Departments/DeparmentController.cs new file mode 100644 index 00000000..91c99176 --- /dev/null +++ b/backend/Api/Departments/DeparmentController.cs @@ -0,0 +1,16 @@ +using Database.DatabaseContext; +using Microsoft.AspNetCore.Mvc; + +[Route("/departments")] +[ApiController] +public class DepartmentController : ControllerBase { + + [HttpGet] + public ActionResult> Get(ApplicationContext applicationContext){ + + return applicationContext.Department.Select(d => new DepartmentReadModel(d.Id, d.Name)).ToList(); + + } +} + +public record DepartmentReadModel(string Id, string Name); diff --git a/frontend/mockdata/mockDepartments.ts b/frontend/mockdata/mockDepartments.ts new file mode 100644 index 00000000..7a0120bb --- /dev/null +++ b/frontend/mockdata/mockDepartments.ts @@ -0,0 +1,8 @@ +import { Department, Variant } from "@/types"; + +export const MockDepartments: Department[] = [ + { + id: "myDepartment", + name: "My Department", + }, +]; diff --git a/frontend/src/app/bemanning/layout.tsx b/frontend/src/app/bemanning/layout.tsx index c4f698ff..e84dc83b 100644 --- a/frontend/src/app/bemanning/layout.tsx +++ b/frontend/src/app/bemanning/layout.tsx @@ -1,3 +1,4 @@ +import DepartmentFilter from "@/components/DepartmentFilter"; import SearchBarComponent from "@/components/SearchBarComponent"; export default function BemanningLayout({ @@ -10,6 +11,7 @@ export default function BemanningLayout({

Filter

+
{children}
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index d844075f..b2a37e94 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -104,4 +104,10 @@ flex: 1; background-color: transparent; } + + .interaction-chip { + font-size: 0.75rem; + font-family: "Graphik-SemiBold"; + line-height: 0.875rem; + } } diff --git a/frontend/src/auth/fetchWithToken.ts b/frontend/src/auth/fetchWithToken.ts index d490d452..6b05e7d2 100644 --- a/frontend/src/auth/fetchWithToken.ts +++ b/frontend/src/auth/fetchWithToken.ts @@ -1,6 +1,7 @@ import { msalInstance } from "@/auth/msalInstance"; import { loginRequest } from "@/authConfig"; import { MockConsultants } from "../../mockdata/mockConsultants"; +import { MockDepartments } from "../../mockdata/mockDepartments"; export async function fetchWithToken(path: string) { if (process.env.NEXT_PUBLIC_NO_AUTH) { @@ -49,4 +50,7 @@ function mockedCall(path: string) { if (path.includes("/variants")) { return MockConsultants; } + if (path.includes("/departments")) { + return MockDepartments; + } } diff --git a/frontend/src/components/AppProviders.tsx b/frontend/src/components/AppProviders.tsx index a0837014..39325977 100644 --- a/frontend/src/components/AppProviders.tsx +++ b/frontend/src/components/AppProviders.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { EventType } from "@azure/msal-browser"; import { MsalProvider } from "@azure/msal-react"; import { CssBaseline } from "@mui/material"; @@ -7,38 +7,39 @@ import { msalInstance } from "@/auth/msalInstance"; import PageLayout from "./PageLayout"; import ThemeRegistry from "./ThemeRegistry/ThemeRegistry"; - msalInstance.initialize().then(() => { - // Account selection logic is app dependent. Adjust as needed for different use cases. - const accounts = msalInstance.getAllAccounts(); - if (accounts.length > 0) { - msalInstance.setActiveAccount(accounts[0]); - } + // Account selection logic is app dependent. Adjust as needed for different use cases. + const accounts = msalInstance.getAllAccounts(); + if (accounts.length > 0) { + msalInstance.setActiveAccount(accounts[0]); + } - msalInstance.addEventCallback((event) => { - // Types are outdated: Event payload is an object with account, token, etcw - //@ts-ignore - if (event.eventType === EventType.LOGIN_SUCCESS && event.payload.account) { - //@ts-ignore - const account = event.payload.account; - msalInstance.setActiveAccount(account); - } - }); + msalInstance.addEventCallback((event) => { + // Types are outdated: Event payload is an object with account, token, etcw + //@ts-ignore + if (event.eventType === EventType.LOGIN_SUCCESS && event.payload.account) { + //@ts-ignore + const account = event.payload.account; + msalInstance.setActiveAccount(account); + } + }); }); -const queryClient = new QueryClient() - -export default function AppProviders({ children }: { children: React.ReactNode }) { - - return ( - - - - - {children} - - - - ) +const queryClient = new QueryClient(); +export default function AppProviders({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + {children} + + + + ); } diff --git a/frontend/src/components/DepartmentFilter.tsx b/frontend/src/components/DepartmentFilter.tsx new file mode 100644 index 00000000..de46c300 --- /dev/null +++ b/frontend/src/components/DepartmentFilter.tsx @@ -0,0 +1,27 @@ +"use client"; +import FilterButton from "./FilterButton"; +import useDepartmentsApi from "@/hooks/useDepartmentsApi"; +import { CircularProgress } from "@mui/material"; + +export default function DepartmentFilter() { + const { data, isLoading } = useDepartmentsApi(); + + if (isLoading) { + ; + } + + if (data) { + return ( +
+
+

Avdelinger

+
+ {data?.map((department, index) => ( + + ))} +
+
+
+ ); + } +} diff --git a/frontend/src/components/FilterButton.tsx b/frontend/src/components/FilterButton.tsx new file mode 100644 index 00000000..054ba132 --- /dev/null +++ b/frontend/src/components/FilterButton.tsx @@ -0,0 +1,44 @@ +"use client"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useState } from "react"; + +export default function FilterButton({ filterName }: { filterName: string }) { + const router = useRouter(); + const searchParams = useSearchParams(); + const [isButtonActive, setIsButtonActive] = useState(checkFilterInUrl); + + function handleFilterClick() { + setIsButtonActive((prevState) => !prevState); + + const currentSearch = searchParams.get("search"); + const currentFilter = searchParams.get("filter") || ""; + const filters = currentFilter.split(","); + const filterIndex = filters.indexOf(filterName); + const newFilters = [...filters]; + if (filterIndex === -1) { + newFilters.push(filterName); + } else { + newFilters.splice(filterIndex, 1); + } + const newFilterString = newFilters.join(",").replace(/^,/, ""); + router.push(`/bemanning?search=${currentSearch}&filter=${newFilterString}`); + } + + function checkFilterInUrl() { + const currentFilter = searchParams.get("filter") || ""; + return currentFilter.includes(filterName); + } + + return ( + + ); +} diff --git a/frontend/src/components/FilteredConsultantsList.tsx b/frontend/src/components/FilteredConsultantsList.tsx index 7c04bcc9..96750e34 100644 --- a/frontend/src/components/FilteredConsultantsList.tsx +++ b/frontend/src/components/FilteredConsultantsList.tsx @@ -11,18 +11,23 @@ export default function FilteredConsultantList({ }) { const searchParams = useSearchParams(); const search = searchParams.get("search"); + const filter = searchParams.get("filter"); const [filteredConsultants, setFilteredConsultants] = useState(consultants); useEffect(() => { + var newFilteredConsultants = consultants; if (search && search.length > 0) { - const filtered = consultants?.filter((consultant) => + newFilteredConsultants = newFilteredConsultants?.filter((consultant) => consultant.name.toLowerCase().includes(search.toLowerCase()), ); - setFilteredConsultants(filtered); - } else { - setFilteredConsultants(consultants); } - }, [search, consultants]); + if (filter && filter.length > 0) { + newFilteredConsultants = newFilteredConsultants?.filter((consultant) => + filter.toLowerCase().includes(consultant.department.toLowerCase()), + ); + } + setFilteredConsultants(newFilteredConsultants); + }, [consultants, filter, search]); return (
diff --git a/frontend/src/components/SearchBarComponent.tsx b/frontend/src/components/SearchBarComponent.tsx index ecfa1047..0e146b4d 100644 --- a/frontend/src/components/SearchBarComponent.tsx +++ b/frontend/src/components/SearchBarComponent.tsx @@ -1,16 +1,18 @@ "use client"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { Search } from "react-feather"; export default function SearchBarComponent() { const router = useRouter(); + const searchParams = useSearchParams(); const [searchText, setSearchText] = useState(""); const inputRef = useRef(null); useEffect(() => { - router.push(`/bemanning?search=${searchText}`); - }, [searchText, router]); + const currentFilter = searchParams.get("filter") || ""; + router.push(`/bemanning?search=${searchText}&filter=${currentFilter}`); + }, [searchText, searchParams, router]); useEffect(() => { function keyDownHandler(e: { code: string }) { diff --git a/frontend/src/hooks/useDepartmentsApi.ts b/frontend/src/hooks/useDepartmentsApi.ts new file mode 100644 index 00000000..5ad441c7 --- /dev/null +++ b/frontend/src/hooks/useDepartmentsApi.ts @@ -0,0 +1,32 @@ +"use client"; + +import { useIsAuthenticated } from "@azure/msal-react"; +import { useQuery } from "react-query"; +import { Department } from "@/types"; +import { fetchWithToken } from "@/auth/fetchWithToken"; + +function useDepartmentsApi() { + const isAuthenticated = + useIsAuthenticated() || process.env.NEXT_PUBLIC_NO_AUTH; + + return useQuery({ + queryKey: "departments", + queryFn: async () => { + if (isAuthenticated) { + try { + const response: Department[] = + await fetchWithToken(`/api/departments`); + return response; + } catch (err) { + console.error(err); + return []; + } + } + // If not authenticated, return an empty array + return []; + }, + refetchOnWindowFocus: false, + }); +} + +export default useDepartmentsApi; diff --git a/frontend/src/hooks/useVibesApi.ts b/frontend/src/hooks/useVibesApi.ts index e8607057..bfbfaa89 100644 --- a/frontend/src/hooks/useVibesApi.ts +++ b/frontend/src/hooks/useVibesApi.ts @@ -2,16 +2,11 @@ import { Variant } from "@/types"; import { fetchWithToken } from "@/auth/fetchWithToken"; import { useIsAuthenticated } from "@azure/msal-react"; -import { useQuery, useQueryClient } from "react-query"; -import { useEffect } from "react"; +import { useQuery } from "react-query"; function useVibesApi(includeOccupied: boolean) { const isAuthenticated = useIsAuthenticated() || process.env.NEXT_PUBLIC_NO_AUTH; - const client = useQueryClient(); - - //TODO: We need a better way of handling state/cache. This works for now though, but it's a bit hacky ngl - useEffect(() => client.clear(), [includeOccupied, client]); return useQuery({ queryKey: "vibes", diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 08d79a07..1310ea7a 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -15,4 +15,15 @@ export type Variant = { ]; }; -export type AnchorProp = Element | (() => Element) | PopoverVirtualElement | (() => PopoverVirtualElement) | null | undefined; \ No newline at end of file +export type Department = { + id: string; + name: string; +}; + +export type AnchorProp = + | Element + | (() => Element) + | PopoverVirtualElement + | (() => PopoverVirtualElement) + | null + | undefined; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 13c3e9e1..31563393 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -11,6 +11,7 @@ module.exports = { primary_l4: "#F6F5F9", secondary_default: "#F076A6", neutral_l1: "#858585", + transparent: "transparent", }, extend: {}, screens: {