FPSMS-frontend
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.

SearchBox.tsx 16 KiB

10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
6 mesi fa
10 mesi fa
10 mesi fa
6 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
10 mesi fa
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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, { SyntheticEvent, 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 {
  23. Autocomplete,
  24. Box,
  25. Checkbox,
  26. Chip,
  27. ListSubheader,
  28. } from "@mui/material";
  29. import MultiSelect from "@/components/SearchBox/MultiSelect";
  30. import { intersectionWith } from "lodash";
  31. interface BaseCriterion<T extends string> {
  32. label: string;
  33. label2?: string;
  34. paramName: T;
  35. paramName2?: T;
  36. // options?: T[] | string[];
  37. filterObj?: T;
  38. handleSelectionChange?: (selectedOptions: T[]) => void;
  39. }
  40. interface OptionWithLabel<T extends string> {
  41. label: string;
  42. value: any;
  43. }
  44. interface TextCriterion<T extends string> extends BaseCriterion<T> {
  45. type: "text";
  46. }
  47. interface SelectCriterion<T extends string> extends BaseCriterion<T> {
  48. type: "select";
  49. options: string[];
  50. }
  51. interface SelectWithLabelCriterion<T extends string> extends BaseCriterion<T> {
  52. type: "select-labelled";
  53. options: OptionWithLabel<T>[];
  54. }
  55. interface MultiSelectCriterion<T extends string> extends BaseCriterion<T> {
  56. type: "multi-select";
  57. options: T[];
  58. selectedOptions: T[];
  59. handleSelectionChange: (selectedOptions: T[]) => void;
  60. }
  61. interface AutocompleteOptions {
  62. value: string | number;
  63. label: string;
  64. group?: string;
  65. }
  66. interface AutocompleteCriterion<T extends string> extends BaseCriterion<T> {
  67. type: "autocomplete";
  68. options: AutocompleteOptions[];
  69. multiple?: boolean;
  70. noOptionsText?: string;
  71. needAll?: boolean;
  72. }
  73. interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
  74. type: "dateRange";
  75. }
  76. interface DateCriterion<T extends string> extends BaseCriterion<T> {
  77. type: "date";
  78. }
  79. export type Criterion<T extends string> =
  80. | TextCriterion<T>
  81. | SelectCriterion<T>
  82. | SelectWithLabelCriterion<T>
  83. | DateRangeCriterion<T>
  84. | DateCriterion<T>
  85. | MultiSelectCriterion<T>
  86. | AutocompleteCriterion<T>;
  87. interface Props<T extends string> {
  88. criteria: Criterion<T>[];
  89. // TODO: may need to check the type is "autocomplete" and "multiple" = true, then allow string[].
  90. // TODO: may need to check the type is "dateRange", then add T and `${T}To` in the same time.
  91. // onSearch: (inputs: Record<T | (Criterion<T>["type"] extends "dateRange" ? `${T}To` : never), string>) => void;
  92. onSearch: (inputs: Record<T | `${T}To`, string>) => void;
  93. onReset?: () => void;
  94. }
  95. function SearchBox<T extends string>({
  96. criteria,
  97. onSearch,
  98. onReset,
  99. }: Props<T>) {
  100. const { t } = useTranslation("common");
  101. const defaultAll: AutocompleteOptions = {
  102. value: "All",
  103. label: t("All"),
  104. group: t("All"),
  105. };
  106. const defaultInputs = useMemo(
  107. () =>
  108. criteria.reduce<Record<T | `${T}To`, string>>(
  109. (acc, c) => {
  110. let tempCriteria = {
  111. ...acc,
  112. [c.paramName]:
  113. c.type === "select" ||
  114. c.type === "select-labelled" ||
  115. (c.type === "autocomplete" && !Boolean(c.multiple))
  116. ? "All"
  117. : c.type === "autocomplete" && Boolean(c.multiple)
  118. ? [defaultAll.value]
  119. : "",
  120. };
  121. if (c.type === "dateRange") {
  122. tempCriteria = {
  123. ...tempCriteria,
  124. [c.paramName]: "",
  125. [`${c.paramName}To`]: "",
  126. };
  127. }
  128. return tempCriteria;
  129. },
  130. {} as Record<T | `${T}To`, string>,
  131. ),
  132. [criteria],
  133. );
  134. const [inputs, setInputs] = useState(defaultInputs);
  135. const [isReset, setIsReset] = useState(false);
  136. const makeInputChangeHandler = useCallback(
  137. (paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
  138. return (e) => {
  139. setInputs((i) => ({ ...i, [paramName]: e.target.value }));
  140. };
  141. },
  142. [],
  143. );
  144. const makeSelectChangeHandler = useCallback((paramName: T) => {
  145. return (e: SelectChangeEvent) => {
  146. setInputs((i) => ({ ...i, [paramName]: e.target.value }));
  147. };
  148. }, []);
  149. const makeAutocompleteChangeHandler = useCallback(
  150. (paramName: T, multiple: boolean) => {
  151. return (
  152. e: SyntheticEvent,
  153. newValue: AutocompleteOptions | AutocompleteOptions[],
  154. ) => {
  155. if (multiple) {
  156. const multiNewValue = newValue as AutocompleteOptions[];
  157. setInputs((i) => ({
  158. ...i,
  159. [paramName]: multiNewValue.map(({ value }) => value),
  160. }));
  161. } else {
  162. const singleNewValue = newValue as AutocompleteOptions;
  163. setInputs((i) => ({ ...i, [paramName]: singleNewValue.value }));
  164. }
  165. };
  166. },
  167. [],
  168. );
  169. const makeDateChangeHandler = useCallback((paramName: T) => {
  170. return (e: any) => {
  171. setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") }));
  172. };
  173. }, []);
  174. const makeDateToChangeHandler = useCallback((paramName: T) => {
  175. return (e: any) => {
  176. setInputs((i) => ({
  177. ...i,
  178. [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"),
  179. }));
  180. };
  181. }, []);
  182. const handleReset = () => {
  183. setInputs(defaultInputs);
  184. onReset?.();
  185. setIsReset(!isReset);
  186. };
  187. const handleSearch = () => {
  188. onSearch(inputs);
  189. };
  190. return (
  191. <Card>
  192. <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
  193. <Typography variant="overline">{t("Search Criteria")}</Typography>
  194. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  195. {criteria.map((c) => {
  196. return (
  197. <Grid key={c.paramName} item xs={6}>
  198. {c.type === "text" && (
  199. <TextField
  200. label={t(c.label)}
  201. fullWidth
  202. onChange={makeInputChangeHandler(c.paramName)}
  203. value={inputs[c.paramName]}
  204. />
  205. )}
  206. {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
  207. {/* {c.type === "multi-select" && (
  208. <MultiSelect
  209. label={t(c.label)}
  210. options={c?.options}
  211. selectedValues={c.filterObj?.[c.paramName] ?? []}
  212. onChange={c.handleSelectionChange}
  213. isReset={isReset}
  214. />
  215. )} */}
  216. {c.type === "select" && (
  217. <FormControl fullWidth>
  218. <InputLabel>{t(c.label)}</InputLabel>
  219. <Select
  220. label={t(c.label)}
  221. onChange={makeSelectChangeHandler(c.paramName)}
  222. value={inputs[c.paramName]}
  223. >
  224. <MenuItem value={"All"}>{t("All")}</MenuItem>
  225. {c.options.map((option) => (
  226. <MenuItem key={option} value={option}>
  227. {option}
  228. </MenuItem>
  229. ))}
  230. </Select>
  231. </FormControl>
  232. )}
  233. {c.type === "select-labelled" && (
  234. <FormControl fullWidth>
  235. <InputLabel>{t(c.label)}</InputLabel>
  236. <Select
  237. label={t(c.label)}
  238. onChange={makeSelectChangeHandler(c.paramName)}
  239. value={inputs[c.paramName]}
  240. >
  241. <MenuItem value={"All"}>{t("All")}</MenuItem>
  242. {c.options.map((option) => (
  243. <MenuItem key={option.value} value={option.value}>
  244. {option.label}
  245. </MenuItem>
  246. ))}
  247. </Select>
  248. </FormControl>
  249. )}
  250. {c.type === "autocomplete" && (
  251. <Autocomplete
  252. groupBy={
  253. c.options.filter((option) => option.group !== "All")
  254. .length > 0 && c.options.every((option) => option.group)
  255. ? (option) =>
  256. option.group && option.group.trim() !== ""
  257. ? option.group
  258. : "Ungrouped"
  259. : undefined
  260. }
  261. multiple={Boolean(c.multiple)}
  262. noOptionsText={c.noOptionsText ?? t("No options")}
  263. disableClearable
  264. fullWidth
  265. value={
  266. c.multiple
  267. ? intersectionWith(
  268. [defaultAll, ...c.options],
  269. inputs[c.paramName],
  270. (option, v) => {
  271. return option.value === (v ?? "");
  272. },
  273. )
  274. : c.options.find(
  275. (option) => option.value === inputs[c.paramName],
  276. ) ?? defaultAll
  277. }
  278. onChange={makeAutocompleteChangeHandler(
  279. c.paramName,
  280. Boolean(c.multiple),
  281. )}
  282. getOptionLabel={(option) => option.label}
  283. options={[defaultAll, ...c.options]}
  284. disableCloseOnSelect={Boolean(c.multiple)}
  285. renderGroup={
  286. c.options.every((option) => option.group)
  287. ? (params) => (
  288. <React.Fragment
  289. key={`${params.key}-${params.group}`}
  290. >
  291. <ListSubheader>{params.group}</ListSubheader>
  292. {params.children}
  293. </React.Fragment>
  294. )
  295. : undefined
  296. }
  297. renderTags={
  298. c.multiple
  299. ? (value, getTagProps) =>
  300. value.map((option, index) => {
  301. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  302. const { key, ...chipProps } = getTagProps({
  303. index,
  304. });
  305. return (
  306. <Chip
  307. {...chipProps}
  308. key={`${option.value}-${option.label}`}
  309. label={option.label}
  310. />
  311. );
  312. })
  313. : undefined
  314. }
  315. renderOption={(
  316. params: React.HTMLAttributes<HTMLLIElement> & {
  317. key?: React.Key;
  318. },
  319. option,
  320. { selected },
  321. ) => {
  322. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  323. const { key, ...rest } = params;
  324. return (
  325. <MenuItem
  326. {...rest}
  327. disableRipple
  328. value={option.value}
  329. key={`${option.value}--${option.label}`}
  330. >
  331. {c.multiple && (
  332. <Checkbox
  333. disableRipple
  334. key={`checkbox-${option.value}`}
  335. checked={selected}
  336. sx={{ transform: "translate(0)" }}
  337. />
  338. )}
  339. {option.label}
  340. </MenuItem>
  341. );
  342. }}
  343. renderInput={(params) => (
  344. <TextField
  345. {...params}
  346. variant="outlined"
  347. label={t(c.label)}
  348. />
  349. )}
  350. />
  351. )}
  352. {c.type === "dateRange" && (
  353. <LocalizationProvider
  354. dateAdapter={AdapterDayjs}
  355. // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
  356. adapterLocale="zh-hk"
  357. >
  358. <Box display="flex">
  359. <FormControl fullWidth>
  360. <DatePicker
  361. label={t(c.label)}
  362. onChange={makeDateChangeHandler(c.paramName)}
  363. value={
  364. dayjs(inputs[c.paramName]).isValid()
  365. ? dayjs(inputs[c.paramName])
  366. : null
  367. }
  368. />
  369. </FormControl>
  370. <Box
  371. display="flex"
  372. alignItems="center"
  373. justifyContent="center"
  374. marginInline={2}
  375. >
  376. {"-"}
  377. </Box>
  378. <FormControl fullWidth>
  379. <DatePicker
  380. label={c.label2 ? t(c.label2) : null}
  381. onChange={makeDateToChangeHandler(c.paramName)}
  382. value={
  383. dayjs(inputs[`${c.paramName}To`]).isValid()
  384. ? dayjs(inputs[`${c.paramName}To`])
  385. : null
  386. }
  387. />
  388. </FormControl>
  389. </Box>
  390. </LocalizationProvider>
  391. )}
  392. {c.type === "date" && (
  393. <LocalizationProvider
  394. dateAdapter={AdapterDayjs}
  395. // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
  396. adapterLocale="zh-hk"
  397. >
  398. <Box display="flex">
  399. <FormControl fullWidth>
  400. <DatePicker
  401. label={t(c.label)}
  402. onChange={makeDateChangeHandler(c.paramName)}
  403. />
  404. </FormControl>
  405. </Box>
  406. </LocalizationProvider>
  407. )}
  408. </Grid>
  409. );
  410. })}
  411. </Grid>
  412. <CardActions sx={{ justifyContent: "flex-end" }}>
  413. <Button
  414. variant="text"
  415. startIcon={<RestartAlt />}
  416. onClick={handleReset}
  417. >
  418. {t("Reset")}
  419. </Button>
  420. <Button
  421. variant="outlined"
  422. startIcon={<Search />}
  423. onClick={handleSearch}
  424. >
  425. {t("Search")}
  426. </Button>
  427. </CardActions>
  428. </CardContent>
  429. </Card>
  430. );
  431. }
  432. export default SearchBox;