Repository: City-of-Helsinki/tilavarauspalvelu-core
Pull request #1623: Replace min/maxPersons with personsAllowed
Author: login: vincit-matu
Labels: name: improvement
description: Improves an existing feature
Created: 2025-03-05T11:25:39Z | Merged: 2025-03-21T08:41:43Z

## 🛠️ Changelog
[//]: # "Describe the changes in this pull request here."

- replaces all `minPersons`/`maxPersons` with the single `personsAllowed` field
- replaces the double (min/max) input pairs with a ControlledNumberInput in forms
- removes `participantCountOptions` from const as they aren't used anywhere anymore
- fixes `count` value in `FilterTagList` to show one/other-values correctly in translations

## 🧪 Test plan
[//]: # "Help your fellow reviewer and write a short description of the fastest way to test your changes."

- Automated tests

## 🚧 Dependencies
[//]: # "Is this PR dependent on other changes outside this repository? Describe the changes, or add links to them."
[//]: # "e.g. This PR breaks X and is waiting for frontend changes before merging."

- None

## 🎫 Tickets
[//]: # "This pull request resolves all or part of the following ticket(s)."

- [TILA-3739](https://helsinkisolutionoffice.atlassian.net/browse/TILA-3739)


[TILA-3739]: https://helsinkisolutionoffice.atlassian.net/browse/TILA-3739?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/apps/ui/components/FilterTagList.tsx b/apps/ui/components/FilterTagList.tsx
index ca92524587..0f3debfcd1 100644
--- a/apps/ui/components/FilterTagList.tsx
+++ b/apps/ui/components/FilterTagList.tsx
@@ -45,7 +45,7 @@ export function FilterTagList({
   multiSelectFilters,
   hideList,
   translateTag,
-}: FilterTagProps) {
+}: Readonly<FilterTagProps>) {
   const { t } = useTranslation();
 
   const { handleRemoveTag, handleResetTags } = useSearchModify();
@@ -86,7 +86,7 @@ export function FilterTagList({
         const label = t(`searchForm:filters.${key}`, {
           label: key,
           value,
-          count: value.length,
+          count: Number(value),
         });
         // Still have the old string encoded values (key=v1,v2,v3) for backwards compatibility
         // but support the better array version (key=v1&key=v2&key=v3) for new code
diff --git a/apps/ui/components/SortingComponent.tsx b/apps/ui/components/SortingComponent.tsx
index 3d59313616..e027999f4e 100644
--- a/apps/ui/components/SortingComponent.tsx
+++ b/apps/ui/components/SortingComponent.tsx
@@ -48,8 +48,6 @@ const Wrapper = styled(Flex).attrs({
   $justifyContent: "flex-end",
   $wrap: "wrap",
 })`
-  flex-grow: 1;
-
   padding: var(--spacing-xs);
   background-color: var(--color-black-5);
 
diff --git a/apps/ui/components/application/ApplicationEvent.tsx b/apps/ui/components/application/ApplicationEvent.tsx
index fd9de5c48e..ca23a1003e 100644
--- a/apps/ui/components/application/ApplicationEvent.tsx
+++ b/apps/ui/components/application/ApplicationEvent.tsx
@@ -61,7 +61,6 @@ function ApplicationEventInner({
     ageGroupOptions,
     purposeOptions,
     reservationUnitTypeOptions,
-    participantCountOptions,
     unitOptions,
   } = optionTypes;
 
@@ -154,7 +153,6 @@ function ApplicationEventInner({
         options={{
           purposeOptions,
           reservationUnitTypeOptions,
-          participantCountOptions,
           unitOptions,
         }}
       />
diff --git a/apps/ui/components/application/ReservationUnitList.tsx b/apps/ui/components/application/ReservationUnitList.tsx
index b811c7f909..7653014a1d 100644
--- a/apps/ui/components/application/ReservationUnitList.tsx
+++ b/apps/ui/components/application/ReservationUnitList.tsx
@@ -31,7 +31,6 @@ export type OptionTypes = {
   ageGroupOptions?: OptionType[];
   purposeOptions: OptionType[];
   reservationUnitTypeOptions: OptionType[];
-  participantCountOptions: OptionType[];
   unitOptions: OptionType[];
 };
 
diff --git a/apps/ui/components/application/ReservationUnitModalContent.tsx b/apps/ui/components/application/ReservationUnitModalContent.tsx
index 3862231990..226e1ca5fd 100644
--- a/apps/ui/components/application/ReservationUnitModalContent.tsx
+++ b/apps/ui/components/application/ReservationUnitModalContent.tsx
@@ -32,7 +32,10 @@ import { getReservationUnitPath } from "@/modules/urls";
 import Card from "common/src/components/Card";
 import { ButtonLikeLink } from "@/components/common/ButtonLikeLink";
 import { useForm } from "react-hook-form";
-import { ControlledSelect } from "common/src/components/form";
+import {
+  ControlledNumberInput,
+  ControlledSelect,
+} from "common/src/components/form";
 
 const ImageSizeWrapper = styled.div`
   @media (min-width: ${breakpoints.m}) {
@@ -138,7 +141,6 @@ type OptionType = {
 type OptionsType = {
   purposeOptions: OptionType[];
   reservationUnitTypeOptions: OptionType[];
-  participantCountOptions: OptionType[];
   unitOptions: OptionType[];
 };
 
@@ -146,7 +148,7 @@ type SearchFormValues = {
   searchTerm?: string;
   reservationUnitType?: number;
   unit?: number;
-  minPersons?: number;
+  personsAllowed?: number;
 };
 
 export function ReservationUnitModalContent({
@@ -155,18 +157,17 @@ export function ReservationUnitModalContent({
   handleRemove,
   currentReservationUnits,
   options,
-}: {
+}: Readonly<{
   applicationRound: AppRoundNode;
   handleAdd: (ru: ReservationUnitType) => void;
   handleRemove: (ru: ReservationUnitType) => void;
   currentReservationUnits: Pick<ReservationUnitType, "pk">[];
   options: OptionsType;
-}): JSX.Element {
+}>): JSX.Element {
   const { t } = useTranslation();
   const form = useForm<SearchFormValues>();
   const { control, watch, setValue } = form;
-  const { unitOptions, participantCountOptions, reservationUnitTypeOptions } =
-    options;
+  const { unitOptions, reservationUnitTypeOptions } = options;
 
   const reservationUnitType = watch("reservationUnitType");
   const unit = watch("unit");
@@ -175,7 +176,7 @@ export function ReservationUnitModalContent({
     variables: {
       applicationRound: [applicationRound.pk ?? 0],
       textSearch: watch("searchTerm"),
-      minPersons: watch("minPersons")?.toString(),
+      personsAllowed: watch("personsAllowed")?.toString(),
       reservationUnitType:
         reservationUnitType != null ? [reservationUnitType] : [],
       unit: unit != null ? [unit] : [],
@@ -209,12 +210,11 @@ export function ReservationUnitModalContent({
           options={reservationUnitTypeOptions}
           label={t("reservationUnitModal:searchReservationUnitTypeLabel")}
         />
-        <ControlledSelect
-          name="minPersons"
+        <ControlledNumberInput
+          name="personsAllowed"
           control={control}
-          clearable
-          label={t("searchForm:participantCountLabel")}
-          options={participantCountOptions}
+          min={1}
+          label={t("reservationUnitModal:searchPersonsAllowedLabel")}
         />
         <ControlledSelect
           name="unit"
diff --git a/apps/ui/components/recurring/SeasonalSearchForm.tsx b/apps/ui/components/recurring/SeasonalSearchForm.tsx
index b1ce977f44..cd230b6a36 100644
--- a/apps/ui/components/recurring/SeasonalSearchForm.tsx
+++ b/apps/ui/components/recurring/SeasonalSearchForm.tsx
@@ -7,22 +7,20 @@ import {
   ButtonVariant,
 } from "hds-react";
 import { type SubmitHandler, useForm } from "react-hook-form";
-import { participantCountOptions } from "@/modules/const";
 import { useSearchModify } from "@/hooks/useSearchValues";
 import { FilterTagList } from "../FilterTagList";
 import { ControlledSelect } from "common/src/components/form/ControlledSelect";
 import { BottomContainer, Filters, StyledSubmitButton } from "../search/styled";
 import { mapParamToNumber } from "@/modules/search";
-import SingleLabelInputGroup from "@/components/common/SingleLabelInputGroup";
 import { type ReadonlyURLSearchParams, useSearchParams } from "next/navigation";
 import { AccessType } from "@gql/gql-types";
+import { ControlledNumberInput } from "common/src/components/form";
 import { toNumber } from "common/src/helpers";
 
 const filterOrder = [
   "applicationRound",
   "textSearch",
-  "minPersons",
-  "maxPersons",
+  "personsAllowed",
   "reservationUnitTypes",
   "unit",
   "purposes",
@@ -30,8 +28,7 @@ const filterOrder = [
 ] as const;
 
 type FormValues = {
-  minPersons: number | null;
-  maxPersons: number | null;
+  personsAllowed: number | null;
   unit: number[];
   reservationUnitTypes: number[];
   purposes: number[];
@@ -48,8 +45,7 @@ function mapQueryToForm(params: ReadonlyURLSearchParams): FormValues {
       params.getAll("reservationUnitTypes"),
       1
     ),
-    minPersons: toNumber(params.get("minPersons")),
-    maxPersons: toNumber(params.get("maxPersons")),
+    personsAllowed: toNumber(params.get("personsAllowed")),
     textSearch: params.get("textSearch") ?? "",
     accessType: params.getAll("accessType"),
   };
@@ -130,27 +126,12 @@ export function SeasonalSearchForm({
             }
           }}
         />
-        {/* TODO this could be combined as a common search option */}
-        <SingleLabelInputGroup label={t("searchForm:participantCountCombined")}>
-          <ControlledSelect
-            name="minPersons"
-            control={control}
-            clearable
-            options={participantCountOptions}
-            label={`${t("searchForm:participantCountCombined")} ${t("common:minimum")}`}
-            placeholder={t("common:minimum")}
-            className="inputSm inputGroupStart"
-          />
-          <ControlledSelect
-            name="maxPersons"
-            control={control}
-            clearable
-            options={participantCountOptions}
-            label={`${t("searchForm:participantCountCombined")} ${t("common:maximum")}`}
-            placeholder={t("common:maximum")}
-            className="inputSm inputGroupEnd"
-          />
-        </SingleLabelInputGroup>
+        <ControlledNumberInput
+          label={t("searchForm:participantCountCombined")}
+          name="personsAllowed"
+          control={control}
+          min={1}
+        />
         <ControlledSelect
           multiselect
           enableSearch
diff --git a/apps/ui/components/search/SingleSearchForm.tsx b/apps/ui/components/search/SingleSearchForm.tsx
index 8850311060..4ce8152cdb 100644
--- a/apps/ui/components/search/SingleSearchForm.tsx
+++ b/apps/ui/components/search/SingleSearchForm.tsx
@@ -4,10 +4,13 @@ import { Checkbox, IconSearch, LoadingSpinner, TextInput } from "hds-react";
 import { type SubmitHandler, useForm, Controller } from "react-hook-form";
 import styled from "styled-components";
 import { addYears, startOfDay } from "date-fns";
-import { TimeRangePicker } from "common/src/components/form";
+import {
+  ControlledNumberInput,
+  TimeRangePicker,
+} from "common/src/components/form";
 import { toUIDate } from "common/src/common/util";
 import { fromUIDate } from "@/modules/util";
-import { getDurationOptions, participantCountOptions } from "@/modules/const";
+import { getDurationOptions } from "@/modules/const";
 import { DateRangePicker } from "@/components/form";
 import { FilterTagList } from "../FilterTagList";
 import SingleLabelInputGroup from "@/components/common/SingleLabelInputGroup";
@@ -45,8 +48,7 @@ type FormValues = {
   startDate: string | null;
   endDate: string | null;
   duration: number | null;
-  minPersons: number | null;
-  maxPersons: number | null;
+  personsAllowed: number | null;
   showOnlyReservable?: boolean;
   textSearch: string;
 };
@@ -71,8 +73,7 @@ function mapQueryToForm(params: ReadonlyURLSearchParams): FormValues {
     startDate: params.get("startDate"),
     endDate: params.get("endDate"),
     duration,
-    minPersons: toNumber(params.get("minPersons")),
-    maxPersons: toNumber(params.get("maxPersons")),
+    personsAllowed: toNumber(params.get("personsAllowed")),
     showOnlyReservable,
     textSearch: params.get("textSearch") ?? "",
   };
@@ -86,8 +87,7 @@ const filterOrder = [
   "startDate",
   "endDate",
   "duration",
-  "minPersons",
-  "maxPersons",
+  "personsAllowed",
   "reservationUnitTypes",
   "unit",
   "purposes",
@@ -169,8 +169,7 @@ export function SingleSearchForm({
 
   const showOptionalFilters =
     formValues.reservationUnitTypes.length !== 0 ||
-    formValues.minPersons != null ||
-    formValues.maxPersons != null ||
+    formValues.personsAllowed != null ||
     formValues.textSearch !== "";
 
   return (
@@ -258,28 +257,12 @@ export function SingleSearchForm({
           data-testid="search-form__filters--optional"
           initiallyOpen={showOptionalFilters}
         >
-          <SingleLabelInputGroup
+          <ControlledNumberInput
             label={t("searchForm:participantCountCombined")}
-          >
-            <ControlledSelect
-              name="minPersons"
-              control={control}
-              options={participantCountOptions}
-              clearable
-              label={`${t("searchForm:participantCountCombined")} ${t("common:minimum")}`}
-              placeholder={t("common:minimum")}
-              className="inputSm inputGroupStart"
-            />
-            <ControlledSelect
-              name="maxPersons"
-              control={control}
-              options={participantCountOptions}
-              clearable
-              label={`${t("searchForm:participantCountCombined")} ${t("common:maximum")}`}
-              placeholder={t("common:maximum")}
-              className="inputSm inputGroupEnd"
-            />
-          </SingleLabelInputGroup>
+            name="personsAllowed"
+            control={control}
+            min={1}
+          />
           <ControlledSelect
             name="reservationUnitTypes"
             multiselect
diff --git a/apps/ui/gql/gql-types.ts b/apps/ui/gql/gql-types.ts
index 3a90c12e8d..22b323ddb1 100644
--- a/apps/ui/gql/gql-types.ts
+++ b/apps/ui/gql/gql-types.ts
@@ -6888,8 +6888,7 @@ export type SearchReservationUnitsQueryVariables = Exact<{
     | Array<InputMaybe<Scalars["Int"]["input"]>>
     | InputMaybe<Scalars["Int"]["input"]>
   >;
-  minPersons?: InputMaybe<Scalars["Decimal"]["input"]>;
-  maxPersons?: InputMaybe<Scalars["Decimal"]["input"]>;
+  personsAllowed?: InputMaybe<Scalars["Decimal"]["input"]>;
   unit?: InputMaybe<
     | Array<InputMaybe<Scalars["Int"]["input"]>>
     | InputMaybe<Scalars["Int"]["input"]>
@@ -11364,8 +11363,7 @@ export const SearchReservationUnitsDocument = gql`
     $textSearch: String
     $pk: [Int]
     $applicationRound: [Int]
-    $minPersons: Decimal
-    $maxPersons: Decimal
+    $personsAllowed: Decimal
     $unit: [Int]
     $reservationUnitType: [Int]
     $purposes: [Int]
@@ -11391,10 +11389,7 @@ export const SearchReservationUnitsDocument = gql`
       textSearch: $textSearch
       pk: $pk
       applicationRound: $applicationRound
-      maxPersonsGte: $minPersons
-      minPersonsGte: $minPersons
-      maxPersonsLte: $maxPersons
-      minPersonsLte: $maxPersons
+      personsAllowed: $personsAllowed
       unit: $unit
       reservationUnitType: $reservationUnitType
       purposes: $purposes
@@ -11456,8 +11451,7 @@ export const SearchReservationUnitsDocument = gql`
  *      textSearch: // value for 'textSearch'
  *      pk: // value for 'pk'
  *      applicationRound: // value for 'applicationRound'
- *      minPersons: // value for 'minPersons'
- *      maxPersons: // value for 'maxPersons'
+ *      personsAllowed: // value for 'personsAllowed'
  *      unit: // value for 'unit'
  *      reservationUnitType: // value for 'reservationUnitType'
  *      purposes: // value for 'purposes'
diff --git a/apps/ui/hooks/useOptions.ts b/apps/ui/hooks/useOptions.ts
index 1e89639ac9..81e9400733 100644
--- a/apps/ui/hooks/useOptions.ts
+++ b/apps/ui/hooks/useOptions.ts
@@ -7,7 +7,6 @@ import {
   ReservationUnitTypeOrderingChoices,
   useOptionsQuery,
 } from "@gql/gql-types";
-import { participantCountOptions } from "@/modules/const";
 import { filterNonNullable, getLocalizationLang } from "common/src/helpers";
 import { getParameterLabel } from "@/modules/util";
 
@@ -190,7 +189,6 @@ export function useOptions() {
     cityOptions,
     purposeOptions,
     reservationUnitTypeOptions,
-    participantCountOptions,
   };
 
   const params = {
diff --git a/apps/ui/modules/const.ts b/apps/ui/modules/const.ts
index d1823b4bce..d763ee7cd7 100644
--- a/apps/ui/modules/const.ts
+++ b/apps/ui/modules/const.ts
@@ -9,10 +9,6 @@ export const ANALYTICS_COOKIE_GROUP_NAME = "statistics";
 
 export const SEARCH_PAGING_LIMIT = 36;
 
-export const participantCountOptions = [
-  1, 2, 5, 10, 15, 20, 25, 30, 40, 50, 60, 80, 100, 150, 200,
-].map((v) => ({ label: `${v}`, value: v }));
-
 type DurationOption = { label: string; value: number };
 function durationMinuteOptions(t: TFunction) {
   const durations: DurationOption[] = [];
diff --git a/apps/ui/modules/queries/reservationUnit.tsx b/apps/ui/modules/queries/reservationUnit.tsx
index dec40d556e..bbfece0d33 100644
--- a/apps/ui/modules/queries/reservationUnit.tsx
+++ b/apps/ui/modules/queries/reservationUnit.tsx
@@ -196,8 +196,7 @@ export const SEARCH_RESERVATION_UNITS = gql`
     $textSearch: String
     $pk: [Int]
     $applicationRound: [Int]
-    $minPersons: Decimal
-    $maxPersons: Decimal
+    $personsAllowed: Decimal
     $unit: [Int]
     $reservationUnitType: [Int]
     $purposes: [Int]
@@ -223,10 +222,7 @@ export const SEARCH_RESERVATION_UNITS = gql`
       textSearch: $textSearch
       pk: $pk
       applicationRound: $applicationRound
-      maxPersonsGte: $minPersons
-      minPersonsGte: $minPersons
-      maxPersonsLte: $maxPersons
-      minPersonsLte: $maxPersons
+      personsAllowed: $personsAllowed
       unit: $unit
       reservationUnitType: $reservationUnitType
       purposes: $purposes
diff --git a/apps/ui/modules/search.ts b/apps/ui/modules/search.ts
index cce96a99e8..e15edfb774 100644
--- a/apps/ui/modules/search.ts
+++ b/apps/ui/modules/search.ts
@@ -5,7 +5,6 @@ import {
   getLocalizationLang,
   ignoreMaybeArray,
   type LocalizationLanguages,
-  toNumber,
 } from "common/src/helpers";
 import {
   AccessType,
@@ -186,8 +185,7 @@ export function processVariables({
   const duration = dur != null && dur > 0 ? dur : null;
   const isSeasonal = kind === ReservationKind.Season;
   const textSearch = ignoreMaybeArray(values.getAll("textSearch"));
-  const minPersons = toNumber(ignoreMaybeArray(values.getAll("minPersons")));
-  const maxPersons = toNumber(ignoreMaybeArray(values.getAll("maxPersons")));
+  const personsAllowed = ignoreMaybeArray(values.getAll("personsAllowed"));
   const purposes = paramToIntegers(values.getAll("purposes"));
   const unit = paramToIntegers(values.getAll("unit"));
   const reservationUnitTypes = paramToIntegers(
@@ -215,16 +213,7 @@ export function processVariables({
           textSearch,
         }
       : {}),
-    ...(minPersons != null && minPersons >= 0
-      ? {
-          minPersons,
-        }
-      : {}),
-    ...(maxPersons != null && maxPersons >= 0
-      ? {
-          maxPersons,
-        }
-      : {}),
+    personsAllowed,
     purposes,
     unit,
     reservationUnitType: reservationUnitTypes,
diff --git a/apps/ui/public/locales/en/searchForm.json b/apps/ui/public/locales/en/searchForm.json
index 511aa5513d..93e53d1bb8 100644
--- a/apps/ui/public/locales/en/searchForm.json
+++ b/apps/ui/public/locales/en/searchForm.json
@@ -20,10 +20,8 @@
     "applicationRound": "{{label}}",
     "textSearch": "{{value}}",
     "equipments": "{{value}}",
-    "minPersons_one": "Min. {{value}} person",
-    "minPersons_other": "Min. {{value}} persons",
-    "maxPersons_one": "Max. {{value}} person",
-    "maxPersons_other": "Max. {{value}} persons",
+    "personsAllowed_one": "{{value}} person",
+    "personsAllowed_other": "{{value}} persons",
     "startDate": "From {{value}}",
     "endDate": "Until {{value}}",
     "timeBegin": "After {{value}}",
diff --git a/apps/ui/public/locales/fi/searchForm.json b/apps/ui/public/locales/fi/searchForm.json
index ae84e6c04b..4c05b51114 100644
--- a/apps/ui/public/locales/fi/searchForm.json
+++ b/apps/ui/public/locales/fi/searchForm.json
@@ -21,8 +21,8 @@
     "textSearch": "{{value}}",
     "equipments": "{{value}}",
     "unit": "{{value}}",
-    "minPersons": "Vähintään {{value}} hlö",
-    "maxPersons": "Enintään {{value}} hlö",
+    "personsAllowed_one": "{{value}} henkilö",
+    "personsAllowed_other": "{{value}} henkilöä",
     "startDate": "Alkaen {{value}}",
     "endDate": "{{value}} asti",
     "timeBegin": "Aikaisintaan {{value}}",
diff --git a/apps/ui/public/locales/sv/searchForm.json b/apps/ui/public/locales/sv/searchForm.json
index fc43844daa..8ad851622d 100644
--- a/apps/ui/public/locales/sv/searchForm.json
+++ b/apps/ui/public/locales/sv/searchForm.json
@@ -20,10 +20,8 @@
     "applicationRound": "{{label}}",
     "textSearch": "{{value}}",
     "equipments": "{{value}}",
-    "minPersons_one": "Minst {{value}} person",
-    "minPersons_other": "Minst {{value}} personer",
-    "maxPersons_one": "Upp till {{value}} person",
-    "maxPersons_other": "Upp till {{value}} personer",
+    "personsAllowed_one": "{{value}} person",
+    "personsAllowed_other": "{{value}} personer",
     "startDate": "Från {{value}}",
     "endDate": "Till {{value}}",
     "timeBegin": "Tidigast kl. {{value}}",


Reviews:
user: login: joonatank
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T14:44:51Z
user: login: vincit-matu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T14:46:24Z

Review comments:
diff_hunk: @@ -247,28 +246,12 @@ export function SingleSearchForm({
           data-testid="search-form__filters--optional"
           initiallyOpen={showOptionalFilters}
         >
-          <SingleLabelInputGroup
+          <ControlledNumberInput
             label={t("searchForm:participantCountCombined")}
-          >
-            <ControlledSelect
-              name="minPersons"
-              control={control}
-              options={participantCountOptions}
-              clearable
-              label={`${t("searchForm:participantCountCombined")} ${t("common:minimum")}`}
-              placeholder={t("common:minimum")}
-              className="inputSm inputGroupStart"
-            />
-            <ControlledSelect
-              name="maxPersons"
-              control={control}
-              options={participantCountOptions}
-              clearable
-              label={`${t("searchForm:participantCountCombined")} ${t("common:maximum")}`}
-              placeholder={t("common:maximum")}
-              className="inputSm inputGroupEnd"
-            />
-          </SingleLabelInputGroup>
+            name="personsAllowed"
+            control={control}
+            min={0}
path: apps/ui/components/search/SingleSearchForm.tsx
user: login: joonatank
body: min=1?
created_at: 2025-03-05T14:42:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -120,27 +116,12 @@ export function SeasonalSearchForm({
             }
           }}
         />
-        {/* TODO this could be combined as a common search option */}
-        <SingleLabelInputGroup label={t("searchForm:participantCountCombined")}>
-          <ControlledSelect
-            name="minPersons"
-            control={control}
-            clearable
-            options={participantCountOptions}
-            label={`${t("searchForm:participantCountCombined")} ${t("common:minimum")}`}
-            placeholder={t("common:minimum")}
-            className="inputSm inputGroupStart"
-          />
-          <ControlledSelect
-            name="maxPersons"
-            control={control}
-            clearable
-            options={participantCountOptions}
-            label={`${t("searchForm:participantCountCombined")} ${t("common:maximum")}`}
-            placeholder={t("common:maximum")}
-            className="inputSm inputGroupEnd"
-          />
-        </SingleLabelInputGroup>
+        <ControlledNumberInput
+          label={t("searchForm:participantCountCombined")}
+          name="personsAllowed"
+          control={control}
+          min={0}
path: apps/ui/components/search/SeasonalSearchForm.tsx
user: login: joonatank
body: min=1?
created_at: 2025-03-05T14:43:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -209,12 +210,11 @@ export function ReservationUnitModalContent({
           options={reservationUnitTypeOptions}
           label={t("reservationUnitModal:searchReservationUnitTypeLabel")}
         />
-        <ControlledSelect
-          name="minPersons"
+        <ControlledNumberInput
+          name="personsAllowed"
           control={control}
-          clearable
-          label={t("searchForm:participantCountLabel")}
-          options={participantCountOptions}
+          min={0}
path: apps/ui/components/application/ReservationUnitModalContent.tsx
user: login: joonatank
body: min=1?
created_at: 2025-03-05T14:44:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -247,28 +246,12 @@ export function SingleSearchForm({
           data-testid="search-form__filters--optional"
           initiallyOpen={showOptionalFilters}
         >
-          <SingleLabelInputGroup
+          <ControlledNumberInput
             label={t("searchForm:participantCountCombined")}
-          >
-            <ControlledSelect
-              name="minPersons"
-              control={control}
-              options={participantCountOptions}
-              clearable
-              label={`${t("searchForm:participantCountCombined")} ${t("common:minimum")}`}
-              placeholder={t("common:minimum")}
-              className="inputSm inputGroupStart"
-            />
-            <ControlledSelect
-              name="maxPersons"
-              control={control}
-              options={participantCountOptions}
-              clearable
-              label={`${t("searchForm:participantCountCombined")} ${t("common:maximum")}`}
-              placeholder={t("common:maximum")}
-              className="inputSm inputGroupEnd"
-            />
-          </SingleLabelInputGroup>
+            name="personsAllowed"
+            control={control}
+            min={0}
path: apps/ui/components/search/SingleSearchForm.tsx
user: login: vincit-matu
body: Right, forgot about our conversation. Will change!
created_at: 2025-03-05T14:46:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-21T08:44:50Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=City-of-Helsinki_tilavarauspalvelu-core&pullRequest=1623) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=City-of-Helsinki_tilavarauspalvelu-core&pullRequest=1623&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=City-of-Helsinki_tilavarauspalvelu-core&pullRequest=1623&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=City-of-Helsinki_tilavarauspalvelu-core&pullRequest=1623&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=City-of-Helsinki_tilavarauspalvelu-core&pullRequest=1623&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=City-of-Helsinki_tilavarauspalvelu-core&pullRequest=1623&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=City-of-Helsinki_tilavarauspalvelu-core&pullRequest=1623)


author_association: NONE