(emptyState);
- const debouncedSearch = debounce((value) => setFilters(value), 300);
-
const { t } = useTranslation();
+ /* TODO
+ initialFiltering={{
+ begin: toUIDate(new Date()) ?? "",
+ }}
+ */
return (
<>
@@ -23,14 +23,9 @@ function AllReservations(): JSX.Element {
{t("Reservations.allReservationListHeading")}
{t("Reservations.allReservationListDescription")}
-
+
-
+
>
);
diff --git a/apps/admin-ui/src/component/reservations/Filters.tsx b/apps/admin-ui/src/component/reservations/Filters.tsx
index 68eda8600..cf6d1975a 100644
--- a/apps/admin-ui/src/component/reservations/Filters.tsx
+++ b/apps/admin-ui/src/component/reservations/Filters.tsx
@@ -1,44 +1,20 @@
-import React, { useEffect, useReducer } from "react";
-import { DateInput, NumberInput, TextInput } from "hds-react";
+import React from "react";
+import { DateInput } from "hds-react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
-import i18next from "i18next";
import ShowAllContainer from "common/src/components/ShowAllContainer";
-import type { OptionType } from "@/common/types";
-import ReservationUnitTypeFilter from "../filters/ReservationUnitTypeFilter";
-import Tags, { type Action, getReducer, toTags } from "../lists/Tags";
-import { UnitFilter } from "../filters/UnitFilter";
-import ReservationUnitFilter from "../filters/ReservationUnitFilter";
-import ReservationStateFilter from "../filters/ReservationStateFilter";
-import PaymentStatusFilter from "./PaymentStatusFilter";
+import { useReservationUnitTypes } from "../filters/ReservationUnitTypeFilter";
+import { useUnitFilterOptions } from "../filters/UnitFilter";
+import { useReservationUnitOptions } from "../filters/ReservationUnitFilter";
import { AutoGrid } from "@/styles/layout";
-
-export type FilterArguments = {
- reservationUnitType: Array<{ label: string; value: number }>;
- unit: Array<{ label: string; value: number }>;
- reservationUnit: Array<{ label: string; value: number }>;
- reservationState: OptionType[];
- paymentStatuses: OptionType[];
- textSearch: string;
- begin: string;
- end: string;
- minPrice: string;
- maxPrice: string;
-};
-
-const multivaluedFields = [
- "unit",
- "reservationUnit",
- "reservationUnitType",
- "reservationUnitStates",
- "reservationState",
- "paymentStatuses",
-];
-
-type Props = {
- onSearch: (args: FilterArguments) => void;
- initialFiltering?: Partial;
-};
+import {
+ MultiSelectFilter,
+ RangeNumberFilter,
+ SearchFilter,
+} from "../QueryParamFilters";
+import { useSearchParams } from "react-router-dom";
+import { SearchTags } from "../SearchTags";
+import { OrderStatus, State } from "common/types/gql-types";
const Wrapper = styled.div`
display: flex;
@@ -52,121 +28,86 @@ const MoreWrapper = styled(ShowAllContainer)`
}
`;
-export const emptyState: FilterArguments = {
- reservationUnitType: [],
- unit: [],
- reservationUnit: [],
- paymentStatuses: [],
- reservationState: [],
- textSearch: "",
- begin: "",
- end: "",
- minPrice: "",
- maxPrice: "",
-};
+function DateInputFilter({ name }: { name: string }) {
+ const { t } = useTranslation();
+ const [searchParams, setParams] = useSearchParams();
-const MyTextInput = ({
- id,
- value,
- dispatch,
-}: {
- id: keyof FilterArguments;
- value: string;
- dispatch: React.Dispatch>;
-}) => (
- {
- if (e.target.value.length > 0) {
- dispatch({
- type: "set",
- value: { [id]: e.target.value },
- });
- } else {
- dispatch({
- type: "deleteTag",
- field: id,
- });
- }
- }}
- value={value || ""}
- placeholder={i18next.t("ReservationsSearch.textSearchPlaceholder")}
- />
-);
+ const filter = searchParams.get(name);
-function Filters({ onSearch, initialFiltering }: Props): JSX.Element {
- const { t } = useTranslation();
- const initialEmptyState = { ...emptyState, ...initialFiltering };
+ const handleChange = (val: string) => {
+ const params = new URLSearchParams(searchParams);
+ if (val.length > 0) {
+ params.set(name, val);
+ setParams(params, { replace: true });
+ } else {
+ setParams(params, { replace: true });
+ }
+ };
- const [state, dispatch] = useReducer(
- getReducer(initialEmptyState),
- initialEmptyState
+ const label = t(`filters.label.${name}`);
+ // TODO make the translation empty. no placeholder on purpose
+ const placeholder = t(`filters.placeholder.${name}`);
+ return (
+ handleChange(val)}
+ value={filter ?? ""}
+ />
);
+}
- useEffect(() => {
- onSearch(state);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
+export function Filters(): JSX.Element {
+ const { t } = useTranslation();
- const tags = toTags(
- state,
- t,
- multivaluedFields,
- ["textSearch"],
- "ReservationsSearch"
- );
+ const { options: reservationUnitTypeOptions } = useReservationUnitTypes();
+ const reservationStateOptions = Object.values(State).map((s) => ({
+ value: s,
+ label: t(`RequestedReservation.state.${s}`),
+ }));
+ const paymentStatusOptions = Object.values(OrderStatus).map((s) => ({
+ value: s,
+ label: t(`Payment.status.${s}`),
+ }));
+
+ const { options: unitOptions } = useUnitFilterOptions();
+ const { options: reservationUnitOptions } = useReservationUnitOptions();
+
+ // TODO implement
+ function translateTag(tag: string, val: string): string {
+ switch (tag) {
+ default:
+ return val;
+ }
+ }
return (
-
- dispatch({ type: "set", value: { reservationUnitType } })
- }
- value={state.reservationUnitType}
- />
-
- dispatch({ type: "set", value: { reservationState } })
- }
- value={state.reservationState}
- />
- dispatch({ type: "set", value: { unit } })}
- value={state.unit}
+
-
-
- dispatch({ type: "set", value: { paymentStatuses } })
- }
- value={state.paymentStatuses || []}
+
+
+
-
- dispatch({ type: "set", value: { reservationUnit } })
- }
- value={state.reservationUnit}
- />
- dispatch({ type: "set", value: { begin } })}
- value={state.begin}
- />
- dispatch({ type: "set", value: { end } })}
- value={state.end}
+
+
+
-
- dispatch({
- type: "set",
- value: { minPrice: e.target.value },
- })
- }
- />
- {
- dispatch({
- type: "set",
- value: {
- maxPrice: e.target.value,
- },
- });
- }}
+
-
+
);
}
-
-export default Filters;
diff --git a/apps/admin-ui/src/component/reservations/PaymentStatusFilter.tsx b/apps/admin-ui/src/component/reservations/PaymentStatusFilter.tsx
deleted file mode 100644
index ac8ad04b2..000000000
--- a/apps/admin-ui/src/component/reservations/PaymentStatusFilter.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import { SortedSelect } from "@/component/SortedSelect";
-import { OptionType } from "@/common/types";
-
-type Props = {
- onChange: (units: OptionType[]) => void;
- value: OptionType[];
-};
-
-const PaymentStatuses = [
- "DRAFT",
- "EXPIRED",
- "CANCELLED",
- "PAID",
- "PAID_MANUALLY",
- "REFUNDED",
-];
-
-const PaymentStatusFilter = ({ onChange, value }: Props): JSX.Element => {
- const { t } = useTranslation();
-
- const opts: OptionType[] = PaymentStatuses.map((s) => ({
- value: s,
- label: t(`Payment.status.${s}`),
- }));
-
- return (
-
- );
-};
-
-export default PaymentStatusFilter;
diff --git a/apps/admin-ui/src/component/reservations/RequestedReservations.tsx b/apps/admin-ui/src/component/reservations/RequestedReservations.tsx
index d442e218c..53c59e9fd 100644
--- a/apps/admin-ui/src/component/reservations/RequestedReservations.tsx
+++ b/apps/admin-ui/src/component/reservations/RequestedReservations.tsx
@@ -1,20 +1,21 @@
-import { debounce } from "lodash";
-import React, { useState } from "react";
+import React from "react";
import { useTranslation } from "react-i18next";
import { H1 } from "common/src/common/typography";
-import Filters, { type FilterArguments, emptyState } from "./Filters";
+import { Filters } from "./Filters";
import { ReservationsDataLoader } from "./ReservationsDataLoader";
import BreadcrumbWrapper from "../BreadcrumbWrapper";
import { HR } from "@/component/Table";
import { Container } from "@/styles/layout";
-import { toUIDate } from "common/src/common/util";
function Reservations(): JSX.Element {
- const [search, setSearch] = useState(emptyState);
- const debouncedSearch = debounce((value) => setSearch(value), 300);
-
const { t } = useTranslation();
+ /* TODO add default filters
+ defaultFiltering={{
+ state: ["DENIED", "CONFIRMED", "REQUIRES_HANDLING"],
+ }}
+ */
+
return (
<>
@@ -23,19 +24,9 @@ function Reservations(): JSX.Element {
{t("Reservations.reservationListHeading")}
{t("Reservations.reservationListDescription")}
-
+
-
+
>
);
diff --git a/apps/admin-ui/src/component/reservations/ReservationsDataLoader.tsx b/apps/admin-ui/src/component/reservations/ReservationsDataLoader.tsx
index fab99cf38..f209b4607 100644
--- a/apps/admin-ui/src/component/reservations/ReservationsDataLoader.tsx
+++ b/apps/admin-ui/src/component/reservations/ReservationsDataLoader.tsx
@@ -1,6 +1,5 @@
import React, { useState } from "react";
import { type ApolloError, useQuery } from "@apollo/client";
-import { values } from "lodash";
import {
type Query,
type QueryReservationsArgs,
@@ -10,65 +9,75 @@ import { More } from "@/component/More";
import { LIST_PAGE_SIZE } from "@/common/const";
import { useNotification } from "@/context/NotificationContext";
import Loader from "../Loader";
-import { FilterArguments } from "./Filters";
import { RESERVATIONS_QUERY } from "./queries";
import { ReservationsTable } from "./ReservationsTable";
import { fromUIDate, toApiDate } from "common/src/common/util";
-import { filterNonNullable } from "common/src/helpers";
-
-type Props = {
- filters: FilterArguments;
- defaultFiltering: QueryReservationsArgs;
-};
-
-function mapFilterParams(
- params: FilterArguments,
- defaultParams: QueryReservationsArgs
-): QueryReservationsArgs {
- const emptySearch =
- values(params).filter((v) => !(v === "" || v.length === 0)).length === 0;
-
- // only use defaults if search is "empty"
- const defaults = emptySearch ? defaultParams : {};
-
- const states = filterNonNullable(
- params.reservationState.map((ru) => ru.value?.toString())
- );
- const state = states.length > 0 ? states : defaults.state;
-
- const begin = fromUIDate(params.begin);
- const end = fromUIDate(params.end);
- const beginDate = begin ? toApiDate(begin) : defaults.beginDate;
- const endDate = end ? toApiDate(end) : defaults.endDate;
+import { filterNonNullable, toNumber } from "common/src/helpers";
+import { useSearchParams } from "react-router-dom";
+
+// TODO add defaultParams (using query params on page load)
+function mapFilterParams(searchParams: URLSearchParams): QueryReservationsArgs {
+ const reservationUnitTypes = searchParams
+ .getAll("reservationUnitType")
+ .map(Number)
+ .filter(Number.isInteger);
+
+ const unit = searchParams.getAll("unit").map(Number).filter(Number.isInteger);
+ const paymentStatus = searchParams
+ .getAll("paymentStatus")
+ .map(Number)
+ .filter(Number.isInteger);
+ const reservationUnit = searchParams
+ .getAll("reservationUnit")
+ .map(Number)
+ .filter(Number.isInteger);
+ const reservationState = searchParams
+ .getAll("reservationState")
+ .map(Number)
+ .filter(Number.isInteger);
+ const textSearch = searchParams.get("search");
+
+ const uiBegin = searchParams.get("begin");
+ const uiEnd = searchParams.get("end");
+
+ const minPrice = searchParams.get("minPrice");
+ const maxPrice = searchParams.get("maxPrice");
+
+ const begin = uiBegin ? fromUIDate(uiBegin) : undefined;
+ const end = uiEnd ? fromUIDate(uiEnd) : undefined;
+ const beginDate = begin ? toApiDate(begin) : undefined;
+ const endDate = end ? toApiDate(end) : undefined;
return {
- unit: filterNonNullable(params.unit?.map((u) => u.value?.toString())),
- reservationUnitType: filterNonNullable(
- params.reservationUnitType?.map((u) => u.value?.toString())
- ),
- reservationUnit: filterNonNullable(
- params.reservationUnit?.map((ru) => ru.value?.toString())
- ),
- state,
- textSearch: params.textSearch || undefined,
+ unit: unit.map((u) => u.toString()),
+ reservationUnitType: reservationUnitTypes.map((u) => u.toString()),
+ reservationUnit: reservationUnit.map((ru) => ru.toString()),
+ orderStatus: paymentStatus.map((status) => status.toString()),
+ state: reservationState.map((status) => status.toString()),
+ textSearch,
beginDate,
endDate,
- priceGte: params.minPrice !== "" ? params.minPrice : undefined,
- priceLte: params.maxPrice !== "" ? params.maxPrice : undefined,
- orderStatus: filterNonNullable(
- params.paymentStatuses?.map((status) => status.value?.toString())
- ),
+ priceGte: minPrice ? toNumber(minPrice)?.toString() : undefined,
+ priceLte: maxPrice ? toNumber(maxPrice)?.toString() : undefined,
};
}
-function useReservations(
- filters: FilterArguments,
- defaultFiltering: QueryReservationsArgs,
- sort: string
-) {
+export function ReservationsDataLoader(): JSX.Element {
+ const [sort, setSort] = useState("-state");
+ const onSortChanged = (sortField: string) => {
+ if (sort === sortField) {
+ setSort(`-${sortField}`);
+ } else {
+ setSort(sortField);
+ }
+ };
+
const { notifyError } = useNotification();
+ // TODO the sort string should be in the url
const orderBy = transformSortString(sort);
+ const [searchParams] = useSearchParams();
+
const { fetchMore, loading, data, previousData } = useQuery<
Query,
QueryReservationsArgs
@@ -79,7 +88,7 @@ function useReservations(
variables: {
orderBy,
first: LIST_PAGE_SIZE,
- ...mapFilterParams(filters, defaultFiltering),
+ ...mapFilterParams(searchParams),
},
onError: (err: ApolloError) => {
notifyError(err.message);
@@ -87,53 +96,28 @@ function useReservations(
});
const currData = data ?? previousData;
+
const reservations = filterNonNullable(
currData?.reservations?.edges.map((edge) => edge?.node)
);
+ const totalCount = currData?.reservations?.totalCount;
+ const offset = currData?.reservations?.edges.length;
- return {
- fetchMore,
- loading,
- data: reservations,
- totalCount: data?.reservations?.totalCount,
- offset: data?.reservations?.edges?.length,
- };
-}
-
-export function ReservationsDataLoader({
- filters,
- defaultFiltering,
-}: Props): JSX.Element {
- const [sort, setSort] = useState("-state");
- const onSortChanged = (sortField: string) => {
- if (sort === sortField) {
- setSort(`-${sortField}`);
- } else {
- setSort(sortField);
- }
- };
-
- const { fetchMore, loading, data, totalCount, offset } = useReservations(
- filters,
- defaultFiltering,
- sort
- );
-
- if (loading && data.length === 0) {
+ if (loading && reservations.length === 0) {
return ;
}
return (
<>
fetchMore({ variables: { offset } })}
/>
>
diff --git a/apps/admin-ui/src/i18n/messages.ts b/apps/admin-ui/src/i18n/messages.ts
index 69708fd88..91132aebd 100644
--- a/apps/admin-ui/src/i18n/messages.ts
+++ b/apps/admin-ui/src/i18n/messages.ts
@@ -1737,13 +1737,7 @@ const translations: ITranslations = {
showInCalendar: ["Näytä kalenterissa"],
},
ReservationsSearch: {
- textSearch: ["Hae varausta"],
- textSearchPlaceholder: ["Hae nimellä tai idllä"],
- minPrice: ["Hinta vähintään"],
- maxPrice: ["Hinta enintään"],
- begin: ["Alkaen"],
- end: ["Asti"],
- paymentStatus: ["Maksutila"],
+ // textSearchPlaceholder: ["Hae nimellä tai idllä"],
filters: {
minPriceTag: ["Hinta vähintään: {{value}}"],
maxPriceTag: ["Hinta enintään: {{value}}"],
@@ -1796,6 +1790,7 @@ const translations: ITranslations = {
selectUnits: ["Valitse toimipisteet"],
priority: ["Aikatoive"],
search: ["Hae hakemusta"],
+ searchReservation: ["Hae varausta"],
ageGroup: ["Ikäryhmä"],
purpose: ["Käyttötarkoitus"],
homeCity: ["Kotikunta"],
@@ -1803,6 +1798,14 @@ const translations: ITranslations = {
order: ["Varausyksiköiden toivejärjestys"],
reservationUnitType: ["Varausyksikön tyyppi"],
reservationUnitState: ["Varausyksikön tila"],
+ price: ["Hinta"],
+ /*
+ minPrice: ["Hinta vähintään"],
+ maxPrice: ["Hinta enintään"],
+ */
+ begin: ["Alkaen"],
+ end: ["Asti"],
+ paymentStatus: ["Maksutila"],
},
// weird values that don't fit under placeholder or label (custom options in this case)
reservationUnitApplication: ["Tilatoive"],