@@ -4,7 +4,7 @@ import Grid from "@mui/material/Grid";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
import React, { FocusEvent, KeyboardEvent, PointerEvent, useCallback, useMemo, useState } from "react";
import React, { FocusEvent, KeyboardEvent, PointerEvent, SyntheticEvent, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import TextField from "@mui/material/TextField";
import FormControl from "@mui/material/FormControl";
@@ -21,8 +21,9 @@ import "dayjs/locale/zh-hk";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { Box, FormHelperText } from "@mui/material";
import { Autocomplete, Box, Checkbox, Chip, FormHelperText, ListSubheader } from "@mui/material";
import { InputAdornment, NumberInput } from "../utils/numberInput";
import { intersectionWith } from "lodash";
interface BaseCriterion<T extends string> {
label: string;
@@ -44,6 +45,20 @@ interface SelectCriterion<T extends string> extends BaseCriterion<T> {
needAll?: boolean;
}
interface AutocompleteOptions {
value: string | number;
label: string;
group?: string;
}
interface AutocompleteCriterion<T extends string> extends BaseCriterion<T> {
type: "autocomplete";
options: AutocompleteOptions[];
multiple?: boolean;
noOptionsText?: string;
needAll?: boolean;
}
interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
type: "dateRange";
needMonth?: boolean;
@@ -60,6 +75,7 @@ interface NumberCriterion<T extends string> extends BaseCriterion<T> {
export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
| AutocompleteCriterion<T>
| DateRangeCriterion<T>
| MonthYearCriterion<T>
| NumberCriterion<T>;
@@ -78,6 +94,7 @@ function SearchBox<T extends string>({
formType,
}: Props<T>) {
const { t } = useTranslation("common");
const defaultAll = { value: "All", label: t("All"), group: t("All") }
const defaultInputs = useMemo(
() =>
criteria.reduce<Record<T, string>>(
@@ -85,13 +102,13 @@ function SearchBox<T extends string>({
return {
...acc,
[c.paramName]:
c.type === "select"
c.type === "select" || c.type === "autocomplete"
? !(c.needAll === false)
? "All"
: c.options.length > 0
? c.options[0]
? c.type === "autocomplete" ? c.options[0].value : c. options[0]
: ""
: "",
: ""
};
},
{} as Record<T, string>
@@ -99,7 +116,7 @@ function SearchBox<T extends string>({
[criteria]
);
const [inputs, setInputs] = useState(defaultInputs);
const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
return (e) => {
@@ -108,7 +125,7 @@ function SearchBox<T extends string>({
},
[]
);
const makeNumberChangeHandler = useCallback(
(paramName: T): (event: FocusEvent<HTMLInputElement, Element> | PointerEvent<Element> | KeyboardEvent<Element>, value: number | null) => void => {
return (event, value) => {
@@ -124,14 +141,26 @@ function SearchBox<T extends string>({
};
}, []);
const makeAutocompleteChangeHandler = useCallback((paramName: T, multiple: boolean) => {
return (e: SyntheticEvent, newValue: AutocompleteOptions | AutocompleteOptions[]) => {
if (multiple) {
const multiNewValue = newValue as AutocompleteOptions[];
setInputs((i) => ({ ...i, [paramName]: multiNewValue.map(({ value }) => value) }));
} else {
const singleNewVal = newValue as AutocompleteOptions;
setInputs((i) => ({ ...i, [paramName]: singleNewVal.value }));
}
};
}, []);
const makeDateChangeHandler = useCallback((paramName: T, needMonth?: boolean) => {
return (e: any) => {
if(needMonth){
if (needMonth) {
setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM") }));
}else{
} else {
setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") }));
}
};
}, []);
@@ -144,17 +173,17 @@ function SearchBox<T extends string>({
const makeDateToChangeHandler = useCallback((paramName: T, needMonth?: boolean) => {
return (e: any) => {
if(needMonth){
setInputs((i) => ({
...i,
[paramName + "To"]: dayjs(e).format("YYYY-MM"),
}));
}else{
setInputs((i) => ({
...i,
[paramName + "To"]: dayjs(e).format("YYYY-MM-DD"),
}));
}
if (needMonth) {
setInputs((i) => ({
...i,
[paramName + "To"]: dayjs(e).format("YYYY-MM"),
}));
} else {
setInputs((i) => ({
...i,
[paramName + "To"]: dayjs(e).format("YYYY-MM-DD"),
}));
}
};
}, []);
@@ -205,9 +234,134 @@ function SearchBox<T extends string>({
</Select>
</FormControl>
)}
{c.type === "autocomplete" && c.options.some(option => Boolean(option.group)) && (
<Autocomplete
multiple={Boolean(c.multiple)}
noOptionsText={c.noOptionsText ?? t("No options")}
disableClearable
fullWidth
value={c.multiple ? intersectionWith(c.options, inputs[c.paramName], (option, v) => {
return option.value === (v ?? "")
})
: c.options.find((option) => option.value === inputs[c.paramName]) ?? defaultAll}
onChange={makeAutocompleteChangeHandler(c.paramName, Boolean(c.multiple))}
groupBy={(option) => option.group!!}
getOptionLabel={(option) => option.label}
options={!(c.needAll === false) ? [defaultAll, ...c.options] : c.options}
disableCloseOnSelect={Boolean(c.multiple)}
renderTags={
c.multiple
? (value, getTagProps) =>
value.map((option, index) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { key, ...chipProps } = getTagProps({ index });
return (
<Chip
{...chipProps}
key={`${option.value}-${option.label}`}
label={option.label}
/>
);
})
: undefined
}
renderGroup={(params) => (
<React.Fragment key={`${params.key}-${params.group}`}>
<ListSubheader>{params.group}</ListSubheader>
{params.children}
</React.Fragment>
)}
renderOption={(
params: React.HTMLAttributes<HTMLLIElement> & { key?: React.Key },
option,
{ selected },
) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { key, ...rest } = params;
return (
<MenuItem
{...rest}
disableRipple
value={option.value}
key={`${option.value}--${option.label}`}
>
{c.multiple && (
<Checkbox
disableRipple
key={`checkbox-${option.value}`}
checked={selected}
sx={{ transform: "translate(0)" }}
/>
)}
{option.label}
</MenuItem>
);
}}
renderInput={(params) => <TextField {...params} />}
/>
)}
{c.type === "autocomplete" && !c.options.some(option => Boolean(option.group)) && (
<Autocomplete
multiple={Boolean(c.multiple)}
noOptionsText={c.noOptionsText ?? t("No options")}
disableClearable
fullWidth
value={c.multiple ? intersectionWith(c.options, inputs[c.paramName], (option, v) => {
return option.value === (v ?? "")
})
: c.options.find((option) => option.value === inputs[c.paramName]) ?? defaultAll}
onChange={makeAutocompleteChangeHandler(c.paramName, Boolean(c.multiple))}
getOptionLabel={(option) => option.label}
options={!Boolean(c.multiple) && !(c.needAll === false) ? [defaultAll, ...c.options] : c.options}
disableCloseOnSelect={Boolean(c.multiple)}
renderTags={
c.multiple
? (value, getTagProps) =>
value.map((option, index) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { key, ...chipProps } = getTagProps({ index });
return (
<Chip
{...chipProps}
key={`${option.value}-${option.label}`}
label={option.label}
/>
);
})
: undefined
}
renderOption={(
params: React.HTMLAttributes<HTMLLIElement> & { key?: React.Key },
option,
{ selected },
) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { key, ...rest } = params;
return (
<MenuItem
{...rest}
disableRipple
value={option.value}
key={`${option.value}--${option.label}`}
>
{c.multiple && (
<Checkbox
disableRipple
key={`checkbox-${option.value}`}
checked={selected}
sx={{ transform: "translate(0)" }}
/>
)}
{option.label}
</MenuItem>
);
}}
renderInput={(params) => <TextField {...params} />}
/>
)}
{c.type === "number" && (
<NumberInput
placeholder={c.label}
<NumberInput
placeholder={c.label}
min={50}
max={99}
onChange={makeNumberChangeHandler(c.paramName)}
@@ -223,15 +377,15 @@ function SearchBox<T extends string>({
<Box display="flex">
<FormControl fullWidth>
<DatePicker
label={c.label}
onChange={makeMonthYearChangeHandler(c.paramName)}
value={
inputs[c.paramName]
? dayjs(inputs[c.paramName])
: dayjs()
}
views={["month","year"]}
/>
label={c.label}
onChange={makeMonthYearChangeHandler(c.paramName)}
value={
inputs[c.paramName]
? dayjs(inputs[c.paramName])
: dayjs()
}
views={["month", "year"]}
/>
</FormControl>
</Box>
</LocalizationProvider>