FPSMS-frontend
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

241 lignes
7.7 KiB

  1. "use client";
  2. import Grid from "@mui/material/Grid";
  3. import Card from "@mui/material/Card";
  4. import CardContent from "@mui/material/CardContent";
  5. import Typography from "@mui/material/Typography";
  6. import React, { useCallback, useMemo, useState } from "react";
  7. import { useTranslation } from "react-i18next";
  8. import TextField from "@mui/material/TextField";
  9. import FormControl from "@mui/material/FormControl";
  10. import InputLabel from "@mui/material/InputLabel";
  11. import Select, { SelectChangeEvent } from "@mui/material/Select";
  12. import MenuItem from "@mui/material/MenuItem";
  13. import CardActions from "@mui/material/CardActions";
  14. import Button from "@mui/material/Button";
  15. import RestartAlt from "@mui/icons-material/RestartAlt";
  16. import Search from "@mui/icons-material/Search";
  17. import dayjs from "dayjs";
  18. import "dayjs/locale/zh-hk";
  19. import { DatePicker } from "@mui/x-date-pickers/DatePicker";
  20. import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
  21. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  22. import { Box } from "@mui/material";
  23. import MultiSelect from "@/components/SearchBox/MultiSelect";
  24. interface BaseCriterion<T extends string> {
  25. label: string;
  26. label2?: string;
  27. paramName: T;
  28. paramName2?: T;
  29. options?: T[] | string[];
  30. filterObj?: T;
  31. handleSelectionChange?: (selectedOptions: T[]) => void;
  32. }
  33. interface TextCriterion<T extends string> extends BaseCriterion<T> {
  34. type: "text";
  35. }
  36. interface SelectCriterion<T extends string> extends BaseCriterion<T> {
  37. type: "select";
  38. options: string[];
  39. }
  40. interface MultiSelectCriterion<T extends string> extends BaseCriterion<T> {
  41. type: "multi-select";
  42. options: T[];
  43. selectedOptions: T[];
  44. handleSelectionChange: (selectedOptions: T[]) => void;
  45. }
  46. interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
  47. type: "dateRange";
  48. }
  49. export type Criterion<T extends string> =
  50. | TextCriterion<T>
  51. | SelectCriterion<T>
  52. | DateRangeCriterion<T>
  53. | MultiSelectCriterion<T>;
  54. interface Props<T extends string> {
  55. criteria: Criterion<T>[];
  56. onSearch: (inputs: Record<T, string>) => void;
  57. onReset?: () => void;
  58. }
  59. function SearchBox<T extends string>({
  60. criteria,
  61. onSearch,
  62. onReset,
  63. }: Props<T>) {
  64. const { t } = useTranslation("common");
  65. const defaultInputs = useMemo(
  66. () =>
  67. criteria.reduce<Record<T, string>>(
  68. (acc, c) => {
  69. return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" };
  70. },
  71. {} as Record<T, string>,
  72. ),
  73. [criteria],
  74. );
  75. const [inputs, setInputs] = useState(defaultInputs);
  76. const [isReset, setIsReset] = useState(false);
  77. const makeInputChangeHandler = useCallback(
  78. (paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
  79. return (e) => {
  80. setInputs((i) => ({ ...i, [paramName]: e.target.value }));
  81. };
  82. },
  83. [],
  84. );
  85. const makeSelectChangeHandler = useCallback((paramName: T) => {
  86. return (e: SelectChangeEvent) => {
  87. setInputs((i) => ({ ...i, [paramName]: e.target.value }));
  88. };
  89. }, []);
  90. const makeDateChangeHandler = useCallback((paramName: T) => {
  91. return (e: any) => {
  92. setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") }));
  93. };
  94. }, []);
  95. const makeDateToChangeHandler = useCallback((paramName: T) => {
  96. return (e: any) => {
  97. setInputs((i) => ({
  98. ...i,
  99. [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"),
  100. }));
  101. };
  102. }, []);
  103. const handleReset = () => {
  104. setInputs(defaultInputs);
  105. onReset?.();
  106. setIsReset(!isReset);
  107. };
  108. const handleSearch = () => {
  109. onSearch(inputs);
  110. };
  111. return (
  112. <Card>
  113. <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
  114. <Typography variant="overline">{t("Search Criteria")}</Typography>
  115. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  116. {criteria.map((c) => {
  117. return (
  118. <Grid key={c.paramName} item xs={6}>
  119. {c.type === "text" && (
  120. <TextField
  121. label={c.label}
  122. fullWidth
  123. onChange={makeInputChangeHandler(c.paramName)}
  124. value={inputs[c.paramName]}
  125. />
  126. )}
  127. {c.type === "multi-select" && (
  128. <MultiSelect
  129. label={c.label}
  130. options={c?.options}
  131. selectedValues={c.filterObj?.[c.paramName] ?? []}
  132. onChange={c.handleSelectionChange}
  133. isReset={isReset}
  134. />
  135. )}
  136. {c.type === "select" && (
  137. <FormControl fullWidth>
  138. <InputLabel>{c.label}</InputLabel>
  139. <Select
  140. label={c.label}
  141. onChange={makeSelectChangeHandler(c.paramName)}
  142. value={inputs[c.paramName]}
  143. >
  144. <MenuItem value={"All"}>{t("All")}</MenuItem>
  145. {c.options.map((option) => (
  146. <MenuItem key={option} value={option}>
  147. {option}
  148. </MenuItem>
  149. ))}
  150. </Select>
  151. </FormControl>
  152. )}
  153. {c.type === "dateRange" && (
  154. <LocalizationProvider
  155. dateAdapter={AdapterDayjs}
  156. // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
  157. adapterLocale="zh-hk"
  158. >
  159. <Box display="flex">
  160. <FormControl fullWidth>
  161. <DatePicker
  162. label={c.label}
  163. onChange={makeDateChangeHandler(c.paramName)}
  164. />
  165. </FormControl>
  166. <Box
  167. display="flex"
  168. alignItems="center"
  169. justifyContent="center"
  170. marginInline={2}
  171. >
  172. {"-"}
  173. </Box>
  174. <FormControl fullWidth>
  175. <DatePicker
  176. label={c.label2}
  177. onChange={makeDateToChangeHandler(c.paramName)}
  178. />
  179. </FormControl>
  180. </Box>
  181. </LocalizationProvider>
  182. )}
  183. {c.type === "date" && (
  184. <LocalizationProvider
  185. dateAdapter={AdapterDayjs}
  186. // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
  187. adapterLocale="zh-hk"
  188. >
  189. <Box display="flex">
  190. <FormControl fullWidth>
  191. <DatePicker
  192. label={c.label}
  193. onChange={makeDateChangeHandler(c.paramName)}
  194. />
  195. </FormControl>
  196. </Box>
  197. </LocalizationProvider>
  198. )}
  199. </Grid>
  200. );
  201. })}
  202. </Grid>
  203. <CardActions sx={{ justifyContent: "flex-end" }}>
  204. <Button
  205. variant="text"
  206. startIcon={<RestartAlt />}
  207. onClick={handleReset}
  208. >
  209. {t("Reset")}
  210. </Button>
  211. <Button
  212. variant="outlined"
  213. startIcon={<Search />}
  214. onClick={handleSearch}
  215. >
  216. {t("Search")}
  217. </Button>
  218. </CardActions>
  219. </CardContent>
  220. </Card>
  221. );
  222. }
  223. export default SearchBox;