diff --git a/frontend/src/app/[organisation]/kunder/page.tsx b/frontend/src/app/[organisation]/kunder/page.tsx index 8c7f4e18..1473ff0b 100644 --- a/frontend/src/app/[organisation]/kunder/page.tsx +++ b/frontend/src/app/[organisation]/kunder/page.tsx @@ -1,6 +1,7 @@ import { EngagementPerCustomerReadModel, EngagementState } from "@/api-types"; -import CustomerRow from "@/components/CostumerTable/CustomerRow"; import { fetchWithToken } from "@/data/apiCallsWithToken"; +import { CustomerFilterProvider } from "@/hooks/CustomerFilterProvider"; +import { CustomerContent } from "@/pagecontent/CustomerContent"; import { Metadata } from "next"; export const metadata: Metadata = { @@ -26,69 +27,8 @@ export default async function Kunder({ ); return ( - <> - -
-

Kunder

- - - a.customerName.localeCompare(b.customerName), - )} - /> - -
- + + + ); - - function Sidebar() { - return ( -
-
-
-

Filter

-
- Ikke implementert -
-
- ); - } - - function CustomerTable({ - title, - customers, - }: { - title: string; - customers: EngagementPerCustomerReadModel[]; - }) { - return ( - - - - - {[1].map((_, index) => ( - - ))} - - - - - - - - {customers.map((customer) => ( - - ))} - -
-
-

{title}

-

- {customers?.length} -

-
-
- ); - } } diff --git a/frontend/src/components/Agreement/components/EditDateInput.tsx b/frontend/src/components/Agreement/components/EditDateInput.tsx index 47044009..76660341 100644 --- a/frontend/src/components/Agreement/components/EditDateInput.tsx +++ b/frontend/src/components/Agreement/components/EditDateInput.tsx @@ -35,6 +35,7 @@ export function EditDateInput({ useLayoutEffect(() => { if (inEdit && inputRef.current && clicked) { inputRef.current.focus(); + inputRef.current.showPicker(); } }, [inEdit, clicked]); @@ -56,7 +57,7 @@ export function EditDateInput({ required={required} defaultValue={value ? format(value!, "yyyy-MM-dd") : undefined} type="date" - className="border-one_and_a_half shadow-sm border-primary rounded-md px-2 pt-1 mt-1 block w-full" + className="border-one_and_a_half shadow-sm border-primary rounded-md px-2 pt-1 mt-1 block w-full " /> ) : ( diff --git a/frontend/src/components/Agreement/components/EditTextarea.tsx b/frontend/src/components/Agreement/components/EditTextarea.tsx index 05495e18..a4824a49 100644 --- a/frontend/src/components/Agreement/components/EditTextarea.tsx +++ b/frontend/src/components/Agreement/components/EditTextarea.tsx @@ -50,7 +50,7 @@ export function EditTextarea({ name={name} defaultValue={value} aria-label={label} - className="border-one_and_a_half border-primary/80 rounded-md p-2 mt-1 block w-full focus:outline-none focus:bg-primary/10 transitionEase" + className="border-one_and_a_half border-primary/80 rounded-md p-2 mt-1 block w-full focus:outline-none focus:outline-primary" /> ) : ( diff --git a/frontend/src/components/CostumerTable/ActiveCustomerFilters.tsx b/frontend/src/components/CostumerTable/ActiveCustomerFilters.tsx new file mode 100644 index 00000000..4096c8a0 --- /dev/null +++ b/frontend/src/components/CostumerTable/ActiveCustomerFilters.tsx @@ -0,0 +1,26 @@ +"use client"; +import { useContext } from "react"; +import { FilteredCustomerContext } from "@/hooks/CustomerFilterProvider"; + +export default function ActiveCustomerFilters() { + const filterTextComponents: string[] = []; + const { activeFilters } = useContext(FilteredCustomerContext); + + const { searchFilter, engagementIsBillableFilter, bookingTypeFilter } = + activeFilters; + + if (searchFilter != "") filterTextComponents.push(` "${searchFilter}"`); + if (engagementIsBillableFilter != "") filterTextComponents.push(`engagement`); + if (bookingTypeFilter != "") + filterTextComponents.push(` "${bookingTypeFilter}"`); + + const filterSummaryText = filterTextComponents.join(", "); + + return ( +
+ {filterSummaryText != "" && ( +

{filterSummaryText}

+ )} +
+ ); +} diff --git a/frontend/src/components/CostumerTable/CustomerSidebarWithFilters.tsx b/frontend/src/components/CostumerTable/CustomerSidebarWithFilters.tsx new file mode 100644 index 00000000..3c3b63d1 --- /dev/null +++ b/frontend/src/components/CostumerTable/CustomerSidebarWithFilters.tsx @@ -0,0 +1,15 @@ +import { FilteredCustomerContext } from "@/hooks/CustomerFilterProvider"; +import SearchBarComponent from "../SearchBarComponent"; + +export default function CustomerSidebarWithFilters() { + return ( + <> +
+
+

Filter

+
+ +
+ + ); +} diff --git a/frontend/src/components/CostumerTable/FilteredCustomersTable.tsx b/frontend/src/components/CostumerTable/FilteredCustomersTable.tsx new file mode 100644 index 00000000..7c92d162 --- /dev/null +++ b/frontend/src/components/CostumerTable/FilteredCustomersTable.tsx @@ -0,0 +1,39 @@ +"use client"; +import { EngagementPerCustomerReadModel } from "@/api-types"; +import CustomerRow from "@/components/CostumerTable/CustomerRow"; +import { useCustomerFilter } from "@/hooks/staffing/useCustomerFilter"; + +function FilteredCustomerTable() { + const filteredCustomers = useCustomerFilter(); + + return ( + + + + + {[1].map((_, index) => ( + + ))} + + + + + + + + {filteredCustomers.map((customer: EngagementPerCustomerReadModel) => ( + + ))} + +
+
+

Kunder

+

+ {filteredCustomers?.length} +

+
+
+ ); +} + +export { FilteredCustomerTable }; diff --git a/frontend/src/components/SearchBarComponent.tsx b/frontend/src/components/SearchBarComponent.tsx index e6957635..5a5eed4f 100644 --- a/frontend/src/components/SearchBarComponent.tsx +++ b/frontend/src/components/SearchBarComponent.tsx @@ -1,15 +1,17 @@ "use client"; -import { useContext, useEffect, useRef, useState } from "react"; +import { Context, useContext, useEffect, useRef, useState } from "react"; import { Search } from "react-feather"; import { useNameSearch } from "@/hooks/staffing/useNameSearch"; import { FilteredContext } from "@/hooks/ConsultantFilterProvider"; export default function SearchBarComponent({ hidden = false, + context, }: { hidden?: boolean; + context: Context; }) { - const { setNameSearch, activeNameSearch } = useNameSearch(); + const { setNameSearch, activeNameSearch } = useNameSearch(context); const inputRef = useRef(null); const [searchIsActive, setIsSearchActive] = useState(false); const { isDisabledHotkeys } = useContext(FilteredContext); diff --git a/frontend/src/components/StaffingSidebar.tsx b/frontend/src/components/StaffingSidebar.tsx index 0388d156..f43c4f2f 100644 --- a/frontend/src/components/StaffingSidebar.tsx +++ b/frontend/src/components/StaffingSidebar.tsx @@ -6,6 +6,7 @@ import { ArrowLeft } from "react-feather"; import RawYearsFilter from "./RawYearsFilter"; import CompetenceFilter from "./CompetenceFilter"; import ExperienceFilter from "./ExperienceFilter"; +import { FilteredContext } from "@/hooks/ConsultantFilterProvider"; // @ts-ignore export default function StaffingSidebar({ @@ -30,7 +31,7 @@ export default function StaffingSidebar({ - + {isStaffing ? : null} @@ -40,7 +41,10 @@ export default function StaffingSidebar({ )} {!isSidebarOpen && (
-
)} diff --git a/frontend/src/hooks/CustomerFilterProvider.tsx b/frontend/src/hooks/CustomerFilterProvider.tsx new file mode 100644 index 00000000..dd8757cd --- /dev/null +++ b/frontend/src/hooks/CustomerFilterProvider.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { EngagementPerCustomerReadModel, EngagementState } from "@/api-types"; +import { usePathname, useSearchParams } from "next/navigation"; +import { createContext, ReactNode, useEffect, useState } from "react"; + +export type CustomerFilters = { + searchFilter: string; + engagementIsBillableFilter: boolean | string; + bookingTypeFilter: EngagementState | string; +}; + +interface UpdateFilterParams { + search?: string; + engagementIsBillable?: boolean; + bookingType?: EngagementState; +} +export type UpdateFilters = (updateParams: UpdateFilterParams) => void; +const defaultFilters: CustomerFilters = { + searchFilter: "", + engagementIsBillableFilter: "", + bookingTypeFilter: "", +}; + +type CustomerFilterContextType = { + activeFilters: CustomerFilters; + updateFilters: UpdateFilters; + customers: EngagementPerCustomerReadModel[]; + setCustomers: React.Dispatch< + React.SetStateAction + >; +}; + +export const FilteredCustomerContext = createContext( + { + customers: [], + setCustomers: () => null, + activeFilters: defaultFilters, + updateFilters: () => null, + }, +); + +export function CustomerFilterProvider(props: { + customers: EngagementPerCustomerReadModel[]; + children: ReactNode; +}) { + const [customers, setCustomers] = useState( + [], + ); + const [activeFilters, updateFilters] = useUrlRouteFilter(); + + useEffect(() => setCustomers(props.customers), [props.customers]); + + return ( + + {props.children} + + ); +} + +function useUrlRouteFilter(): [CustomerFilters, UpdateFilters] { + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const [searchFilter, setSearchFilter] = useState( + searchParams.get("search") || "", + ); + const [engagementIsBillableFilter, setEngagementIsBillableFilter] = useState( + searchParams.get("engagementIsBillable") || false, + ); + const [bookingTypeFilter, setBookingTypeFilter] = useState( + searchParams.get("bookingType") || "", + ); + + function updateRoute(updateParams: UpdateFilterParams) { + // If not defined, defaults to current value: + const { search = searchFilter } = updateParams; + const { engagementIsBillable = engagementIsBillableFilter } = updateParams; + const { bookingType = bookingTypeFilter } = updateParams; + + const url = `${pathname}?search=${search}&engagementIsBillable=${engagementIsBillable}&bookingType=${bookingType}}`; + + setSearchFilter(search); + setEngagementIsBillableFilter(engagementIsBillable); + setBookingTypeFilter(bookingType); + + window.history.pushState({}, "", url); + } + + return [ + { + searchFilter, + engagementIsBillableFilter, + bookingTypeFilter, + }, + updateRoute, + ]; +} diff --git a/frontend/src/hooks/staffing/useCustomerFilter.ts b/frontend/src/hooks/staffing/useCustomerFilter.ts new file mode 100644 index 00000000..c8762029 --- /dev/null +++ b/frontend/src/hooks/staffing/useCustomerFilter.ts @@ -0,0 +1,63 @@ +import { useContext } from "react"; +import { FilteredCustomerContext } from "../CustomerFilterProvider"; +import { + EngagementPerCustomerReadModel, + EngagementReadModel, + EngagementState, +} from "@/api-types"; + +export function useCustomerFilter() { + const { customers } = useContext(FilteredCustomerContext); + + const { searchFilter, engagementIsBillableFilter, bookingTypeFilter } = + useContext(FilteredCustomerContext).activeFilters; + + const filteredCustomers = filterCustomers({ + search: searchFilter, + engagementIsBillable: engagementIsBillableFilter, + bookingType: bookingTypeFilter, + customers, + }); + return filteredCustomers; +} + +export function filterCustomers({ + search, + engagementIsBillable, + bookingType, + customers, +}: { + search: string; + engagementIsBillable: boolean | string; + bookingType: EngagementState | string; + customers: EngagementPerCustomerReadModel[]; +}) { + let newFilteredCustomers = customers; + if (search && search.length > 0) { + newFilteredCustomers = newFilteredCustomers?.filter( + (customer: EngagementPerCustomerReadModel) => + customer.customerName.match( + new RegExp(`(? + customer.engagements.filter( + (engagement: EngagementReadModel) => engagement.isBillable, + ), + ); + } + + if (bookingType) { + newFilteredCustomers = newFilteredCustomers?.filter( + (customer: EngagementPerCustomerReadModel) => + customer.engagements.filter( + (engagement: EngagementReadModel) => + engagement.bookingType == bookingType, + ), + ); + } + return newFilteredCustomers; +} diff --git a/frontend/src/hooks/staffing/useNameSearch.ts b/frontend/src/hooks/staffing/useNameSearch.ts index 6657185e..a1ff1599 100644 --- a/frontend/src/hooks/staffing/useNameSearch.ts +++ b/frontend/src/hooks/staffing/useNameSearch.ts @@ -1,8 +1,7 @@ -import { useState, useEffect, useContext } from "react"; -import { FilteredContext } from "@/hooks/ConsultantFilterProvider"; +import { useState, useEffect, useContext, Context } from "react"; -export function useNameSearch() { - const { updateFilters, activeFilters } = useContext(FilteredContext); +export function useNameSearch(context: Context) { + const { updateFilters, activeFilters } = useContext(context); const searchFilter = activeFilters.searchFilter; function setNameSearch(newSearch: string) { diff --git a/frontend/src/pagecontent/CustomerContent.tsx b/frontend/src/pagecontent/CustomerContent.tsx new file mode 100644 index 00000000..6a18b557 --- /dev/null +++ b/frontend/src/pagecontent/CustomerContent.tsx @@ -0,0 +1,64 @@ +"use client"; +import { EngagementPerCustomerReadModel } from "@/api-types"; +import CustomerSidebarWithFilters from "@/components/CostumerTable/CustomerSidebarWithFilters"; +import CustomerRow from "@/components/CostumerTable/CustomerRow"; +import { FilteredCustomerTable } from "@/components/CostumerTable/FilteredCustomersTable"; + +export function CustomerContent({ + customers, + absence, +}: { + customers: EngagementPerCustomerReadModel[]; + absence: EngagementPerCustomerReadModel[]; +}) { + return ( + <> + +
+

Kunder

+
+ {/* */} +
+ + +
+ + ); +} + +function CustomerTable({ + title, + customers, +}: { + title: string; + customers: EngagementPerCustomerReadModel[]; +}) { + return ( + + + + + {[1].map((_, index) => ( + + ))} + + + + + + + + {customers.map((customer) => ( + + ))} + +
+
+

{title}

+

+ {customers?.length} +

+
+
+ ); +}