+ {canClearAllFilters ? (
+ // @ts-expect-error bad type definition
+
+ ) : null}
+
+ );
+};
+
+export default ClearFilters
\ No newline at end of file
diff --git a/frontend/src/features/models/components/filters/index.ts b/frontend/src/features/models/components/filters/index.ts
index 88d16056..ae7cf1b0 100644
--- a/frontend/src/features/models/components/filters/index.ts
+++ b/frontend/src/features/models/components/filters/index.ts
@@ -2,3 +2,5 @@ export { default as OrderingFilter } from "./ordering-filter";
export { default as DateRangeFilter } from "./date-range-filter";
export { default as CategoryFilter } from "./category-filter";
export { default as SearchFilter } from "./search-filter";
+export { default as ClearFilters } from './clear-filters'
+export { default as MobileFilter } from './mobile-filter'
\ No newline at end of file
diff --git a/frontend/src/features/models/components/filters/mobile-filter.tsx b/frontend/src/features/models/components/filters/mobile-filter.tsx
new file mode 100644
index 00000000..28554b48
--- /dev/null
+++ b/frontend/src/features/models/components/filters/mobile-filter.tsx
@@ -0,0 +1,22 @@
+import { FilterIcon } from "@/components/ui/icons";
+
+const MobileFilter = ({
+ openMobileFilterModal,
+}: {
+ openMobileFilterModal: () => void;
+ isMobile?: boolean;
+}) => {
+ return (
+
- {APP_CONTENT.models.modelsList.pageTitle}
+ {title ?? APP_CONTENT.models.modelsList.pageTitle}
- {APP_CONTENT.models.modelsList.description}
+ {description ?? APP_CONTENT.models.modelsList.description}
void;
+ query: TQueryParams;
+ isMobile?: boolean;
+ disabled?: boolean;
+}) => {
+ const activeLayout = query[SEARCH_PARAMS.layout];
+ return (
+
+ );
+};
+
+
+export default LayoutToggle
\ No newline at end of file
diff --git a/frontend/src/features/models/components/map-toggle.tsx b/frontend/src/features/models/components/map-toggle.tsx
new file mode 100644
index 00000000..287a1afd
--- /dev/null
+++ b/frontend/src/features/models/components/map-toggle.tsx
@@ -0,0 +1,38 @@
+import { SEARCH_PARAMS } from "@/app/routes/models/models-list";
+import { Switch } from "@/components/ui/form";
+import { LayoutView } from "@/enums/models";
+import { TQueryParams } from "@/types";
+import { APP_CONTENT } from "@/utils";
+
+const ModelMapToggle = ({
+ query,
+ updateQuery,
+ isMobile,
+}: {
+ updateQuery: (params: TQueryParams) => void;
+ query: TQueryParams;
+ isMobile?: boolean;
+}) => {
+ return (
+
+
+ {APP_CONTENT.models.modelsList.filtersSection.mapViewToggleText}
+
+
{
+ updateQuery({
+ [SEARCH_PARAMS.mapIsActive]: !query[SEARCH_PARAMS.mapIsActive],
+ });
+ }}
+ />
+
+ );
+};
+
+
+export default ModelMapToggle
\ No newline at end of file
diff --git a/frontend/src/features/models/hooks/use-models.ts b/frontend/src/features/models/hooks/use-models.ts
index 576670a5..e48fb63a 100644
--- a/frontend/src/features/models/hooks/use-models.ts
+++ b/frontend/src/features/models/hooks/use-models.ts
@@ -3,7 +3,17 @@ import {
getModelsQueryOptions,
getModelDetailsQueryOptions,
getModelsMapDataQueryOptions,
-} from "../api/factory";
+} from "@/features/models/api/factory";
+import { useSearchParams } from "react-router-dom";
+import { SEARCH_PARAMS } from "@/app/routes/models/models-list";
+import { ORDERING_FIELDS } from "@/features/models/components/filters/ordering-filter";
+import { TQueryParams } from "@/types";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { buildDateFilterQueryString } from "@/utils";
+import { PAGE_LIMIT } from "@/components/pagination";
+import { dateFilters } from "@/features/models/components/filters/date-range-filter";
+import useDebounce from "@/hooks/use-debounce";
+import { LayoutView } from "@/enums/models";
type UseModelsOptions = {
limit: number;
@@ -13,6 +23,7 @@ type UseModelsOptions = {
dateFilters: Record;
status?: number;
id: number;
+ userId?: number
};
export const useModels = ({
@@ -23,6 +34,7 @@ export const useModels = ({
searchQuery,
dateFilters,
id,
+ userId
}: UseModelsOptions) => {
return useQuery({
...getModelsQueryOptions({
@@ -33,6 +45,7 @@ export const useModels = ({
searchQuery,
dateFilters,
id,
+ userId
}),
//@ts-expect-error bad type definition
throwOnError: (error) => error.response?.status >= 500,
@@ -64,3 +77,122 @@ export const useModelsMapData = () => {
throwOnError: (error) => error.response?.status >= 500,
});
};
+
+
+
+export const useModelsListFilters = (userId?: number) => {
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const defaultQueries = {
+ [SEARCH_PARAMS.offset]: 0,
+ [SEARCH_PARAMS.searchQuery]:
+ searchParams.get(SEARCH_PARAMS.searchQuery) || "",
+ [SEARCH_PARAMS.ordering]:
+ searchParams.get(SEARCH_PARAMS.ordering) ||
+ (ORDERING_FIELDS[1].apiValue as string),
+ [SEARCH_PARAMS.mapIsActive]:
+ searchParams.get(SEARCH_PARAMS.mapIsActive) || false,
+ [SEARCH_PARAMS.startDate]: searchParams.get(SEARCH_PARAMS.startDate) || "",
+ [SEARCH_PARAMS.endDate]: searchParams.get(SEARCH_PARAMS.endDate) || "",
+ [SEARCH_PARAMS.dateFilter]:
+ searchParams.get(SEARCH_PARAMS.dateFilter) || dateFilters[0].searchParams,
+ [SEARCH_PARAMS.layout]:
+ searchParams.get(SEARCH_PARAMS.layout) || LayoutView.GRID,
+ [SEARCH_PARAMS.id]: searchParams.get(SEARCH_PARAMS.id) || "",
+ };
+ const [query, setQuery] = useState(defaultQueries);
+
+
+ const debouncedSearchText = useDebounce(
+ query[SEARCH_PARAMS.searchQuery] as string,
+ 300,
+ );
+
+ const { data, isPending, isPlaceholderData, isError } = useModels({
+ searchQuery: debouncedSearchText,
+ limit: PAGE_LIMIT,
+ offset: query[SEARCH_PARAMS.offset] as number,
+ orderBy: query[SEARCH_PARAMS.ordering] as string,
+ id: query[SEARCH_PARAMS.id] as number,
+ dateFilters: buildDateFilterQueryString(
+ dateFilters.find(
+ (filter) => filter.searchParams === query[SEARCH_PARAMS.dateFilter],
+ ),
+ query[SEARCH_PARAMS.startDate] as string,
+ query[SEARCH_PARAMS.endDate] as string,
+ ),
+ userId: userId
+ });
+
+ const updateQuery = useCallback(
+ (newParams: TQueryParams) => {
+ setQuery((prevQuery) => ({
+ ...prevQuery,
+ ...newParams,
+ }));
+ const updatedParams = new URLSearchParams(searchParams);
+
+ Object.entries(newParams).forEach(([key, value]) => {
+ if (value) {
+ updatedParams.set(key, String(value));
+ } else {
+ updatedParams.delete(key);
+ }
+ });
+
+ setSearchParams(updatedParams, { replace: true });
+ },
+ [searchParams, setSearchParams],
+ );
+
+ //reset offset back to 0 when searching or when ID filtering is applied from the map.
+ useEffect(() => {
+ if (
+ (query[SEARCH_PARAMS.searchQuery] !== "" ||
+ query[SEARCH_PARAMS.id] !== "") &&
+ (query[SEARCH_PARAMS.offset] as number) > 0
+ ) {
+ updateQuery({ [SEARCH_PARAMS.offset]: 0 });
+ }
+ }, [query]);
+
+
+
+ useEffect(() => {
+ const newQuery = {
+ [SEARCH_PARAMS.offset]: defaultQueries[SEARCH_PARAMS.offset],
+ [SEARCH_PARAMS.ordering]: defaultQueries[SEARCH_PARAMS.ordering],
+ [SEARCH_PARAMS.mapIsActive]: defaultQueries[SEARCH_PARAMS.mapIsActive],
+ [SEARCH_PARAMS.startDate]: defaultQueries[SEARCH_PARAMS.startDate],
+ [SEARCH_PARAMS.endDate]: defaultQueries[SEARCH_PARAMS.endDate],
+ [SEARCH_PARAMS.dateFilter]: defaultQueries[SEARCH_PARAMS.dateFilter],
+ [SEARCH_PARAMS.layout]: defaultQueries[SEARCH_PARAMS.layout],
+ [SEARCH_PARAMS.searchQuery]: defaultQueries[SEARCH_PARAMS.searchQuery],
+ [SEARCH_PARAMS.id]: defaultQueries[SEARCH_PARAMS.id],
+ };
+ setQuery(newQuery);
+ }, []);
+
+ const mapViewIsActive = useMemo(
+ () => query[SEARCH_PARAMS.mapIsActive],
+ [query],
+ );
+
+ const clearAllFilters = useCallback(() => {
+ const resetParams = new URLSearchParams();
+ setSearchParams(resetParams);
+ setQuery((prev) => ({
+ // Preserve existing query params
+ ...prev,
+ // Clear only the filter fields
+ [SEARCH_PARAMS.searchQuery]: "",
+ [SEARCH_PARAMS.startDate]: "",
+ [SEARCH_PARAMS.endDate]: "",
+ [SEARCH_PARAMS.id]: "",
+ }));
+ }, []);
+
+
+ return { query, data, isPending, isPlaceholderData, isError, updateQuery, mapViewIsActive, clearAllFilters }
+
+}
\ No newline at end of file