|
|
@@ -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, { useCallback, useMemo, useState } from "react"; |
|
|
|
import React, { SyntheticEvent, useCallback, useMemo, useState } from "react"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
import TextField from "@mui/material/TextField"; |
|
|
|
import FormControl from "@mui/material/FormControl"; |
|
|
@@ -20,15 +20,16 @@ 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 } from "@mui/material"; |
|
|
|
import { Autocomplete, Box, Checkbox, Chip, ListSubheader } from "@mui/material"; |
|
|
|
import MultiSelect from "@/components/SearchBox/MultiSelect"; |
|
|
|
import { intersectionWith } from "lodash"; |
|
|
|
|
|
|
|
interface BaseCriterion<T extends string> { |
|
|
|
label: string; |
|
|
|
label2?: string; |
|
|
|
paramName: T; |
|
|
|
paramName2?: T; |
|
|
|
options?: T[] | string[]; |
|
|
|
// options?: T[] | string[]; |
|
|
|
filterObj?: T; |
|
|
|
handleSelectionChange?: (selectedOptions: T[]) => void; |
|
|
|
} |
|
|
@@ -49,15 +50,36 @@ interface MultiSelectCriterion<T extends string> extends BaseCriterion<T> { |
|
|
|
handleSelectionChange: (selectedOptions: T[]) => void; |
|
|
|
} |
|
|
|
|
|
|
|
// TODO: Add group |
|
|
|
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"; |
|
|
|
} |
|
|
|
|
|
|
|
interface DateCriterion<T extends string> extends BaseCriterion<T> { |
|
|
|
type: "date"; |
|
|
|
} |
|
|
|
|
|
|
|
export type Criterion<T extends string> = |
|
|
|
| TextCriterion<T> |
|
|
|
| SelectCriterion<T> |
|
|
|
| DateRangeCriterion<T> |
|
|
|
| MultiSelectCriterion<T>; |
|
|
|
| DateCriterion<T> |
|
|
|
| MultiSelectCriterion<T> |
|
|
|
| AutocompleteCriterion<T>; |
|
|
|
|
|
|
|
interface Props<T extends string> { |
|
|
|
criteria: Criterion<T>[]; |
|
|
@@ -71,11 +93,15 @@ function SearchBox<T extends string>({ |
|
|
|
onReset, |
|
|
|
}: Props<T>) { |
|
|
|
const { t } = useTranslation("common"); |
|
|
|
const defaultAll = { value: "All", label: t("All"), group: t("All") } |
|
|
|
const defaultInputs = useMemo( |
|
|
|
() => |
|
|
|
criteria.reduce<Record<T, string>>( |
|
|
|
(acc, c) => { |
|
|
|
return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; |
|
|
|
return { |
|
|
|
...acc, |
|
|
|
[c.paramName]: (c.type === "select" || c.type === "autocomplete" ? "All" : "") |
|
|
|
}; |
|
|
|
}, |
|
|
|
{} as Record<T, string>, |
|
|
|
), |
|
|
@@ -99,6 +125,18 @@ 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 singleNewValue = newValue as AutocompleteOptions; |
|
|
|
setInputs((i) => ({ ...i, [paramName]: singleNewValue.value })); |
|
|
|
} |
|
|
|
}; |
|
|
|
}, []); |
|
|
|
|
|
|
|
const makeDateChangeHandler = useCallback((paramName: T) => { |
|
|
|
return (e: any) => { |
|
|
|
setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") })); |
|
|
@@ -130,6 +168,7 @@ function SearchBox<T extends string>({ |
|
|
|
<Typography variant="overline">{t("Search Criteria")}</Typography> |
|
|
|
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> |
|
|
|
{criteria.map((c) => { |
|
|
|
console.log(c.options) |
|
|
|
return ( |
|
|
|
<Grid key={c.paramName} item xs={6}> |
|
|
|
{c.type === "text" && ( |
|
|
@@ -141,13 +180,13 @@ function SearchBox<T extends string>({ |
|
|
|
/> |
|
|
|
)} |
|
|
|
{c.type === "multi-select" && ( |
|
|
|
<MultiSelect |
|
|
|
label={c.label} |
|
|
|
options={c?.options} |
|
|
|
selectedValues={c.filterObj?.[c.paramName] ?? []} |
|
|
|
onChange={c.handleSelectionChange} |
|
|
|
isReset={isReset} |
|
|
|
/> |
|
|
|
<MultiSelect |
|
|
|
label={c.label} |
|
|
|
options={c?.options} |
|
|
|
selectedValues={c.filterObj?.[c.paramName] ?? []} |
|
|
|
onChange={c.handleSelectionChange} |
|
|
|
isReset={isReset} |
|
|
|
/> |
|
|
|
)} |
|
|
|
{c.type === "select" && ( |
|
|
|
<FormControl fullWidth> |
|
|
@@ -166,6 +205,76 @@ function SearchBox<T extends string>({ |
|
|
|
</Select> |
|
|
|
</FormControl> |
|
|
|
)} |
|
|
|
{c.type === "autocomplete" && ( |
|
|
|
<Autocomplete |
|
|
|
groupBy={ |
|
|
|
c.options.every((option) => option.group) |
|
|
|
? (option) => (option.group && option.group.trim() !== '' ? option.group : 'Ungrouped') |
|
|
|
: undefined |
|
|
|
} |
|
|
|
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={[defaultAll, ...c.options]} |
|
|
|
disableCloseOnSelect={Boolean(c.multiple)} |
|
|
|
renderGroup={c.options.every((option) => option.group) ? (params) => ( |
|
|
|
<React.Fragment key={`${params.key}-${params.group}`}> |
|
|
|
<ListSubheader>{params.group}</ListSubheader> |
|
|
|
{params.children} |
|
|
|
</React.Fragment> |
|
|
|
) : undefined} |
|
|
|
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} variant="outlined" label={c.label} />} |
|
|
|
/> |
|
|
|
)} |
|
|
|
{c.type === "dateRange" && ( |
|
|
|
<LocalizationProvider |
|
|
|
dateAdapter={AdapterDayjs} |
|
|
@@ -197,20 +306,20 @@ function SearchBox<T extends string>({ |
|
|
|
</LocalizationProvider> |
|
|
|
)} |
|
|
|
{c.type === "date" && ( |
|
|
|
<LocalizationProvider |
|
|
|
dateAdapter={AdapterDayjs} |
|
|
|
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD |
|
|
|
adapterLocale="zh-hk" |
|
|
|
> |
|
|
|
<Box display="flex"> |
|
|
|
<FormControl fullWidth> |
|
|
|
<DatePicker |
|
|
|
label={c.label} |
|
|
|
onChange={makeDateChangeHandler(c.paramName)} |
|
|
|
/> |
|
|
|
</FormControl> |
|
|
|
</Box> |
|
|
|
</LocalizationProvider> |
|
|
|
<LocalizationProvider |
|
|
|
dateAdapter={AdapterDayjs} |
|
|
|
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD |
|
|
|
adapterLocale="zh-hk" |
|
|
|
> |
|
|
|
<Box display="flex"> |
|
|
|
<FormControl fullWidth> |
|
|
|
<DatePicker |
|
|
|
label={c.label} |
|
|
|
onChange={makeDateChangeHandler(c.paramName)} |
|
|
|
/> |
|
|
|
</FormControl> |
|
|
|
</Box> |
|
|
|
</LocalizationProvider> |
|
|
|
)} |
|
|
|
</Grid> |
|
|
|
); |
|
|
|