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 23 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
7 mesi fa
​
11 mesi fa
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
7 mesi fa
​
​
​
​
​
​
​
7 mesi fa
​
​
​
​
​
​
1 anno fa
​
​
​
​
​
​
​
​
​
​
7 mesi fa
​
​
​
​
​
1 anno fa
​
​
​
​
11 mesi fa
​
​
​
​
​
​
​
​
​
​
​
​
11 mesi fa
​
7 mesi fa
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
7 mesi fa
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
7 mesi fa
​
​
​
​
​
​
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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 ExpandMore from "@mui/icons-material/ExpandMore";
  18. import ExpandLess from "@mui/icons-material/ExpandLess";
  19. import dayjs, { Dayjs } from "dayjs";
  20. import "dayjs/locale/zh-hk";
  21. import { DatePicker } from "@mui/x-date-pickers/DatePicker";
  22. import { TimePicker } from "@mui/x-date-pickers/TimePicker";
  23. import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
  24. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  25. import {
  26. Autocomplete,
  27. Box,
  28. Checkbox,
  29. Chip,
  30. ListSubheader,
  31. } from "@mui/material";
  32. import MultiSelect from "@/components/SearchBox/MultiSelect";
  33. import { intersectionWith } from "lodash";
  34. import { INPUT_DATE_FORMAT, INPUT_TIME_FORMAT, OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dayjsToDateTimeString } from "@/app/utils/formatUtil";
  35. import { DateTimePicker } from "@mui/x-date-pickers";
  36. interface BaseCriterion<T extends string> {
  37. label: string;
  38. label2?: string;
  39. paramName: T;
  40. paramName2?: T;
  41. // options?: T[] | string[];
  42. defaultValue?: string;
  43. /** 與 `defaultValue` 配對,用於 dateRange / datetimeRange 重置時的結束值 */
  44. defaultValueTo?: string;
  45. preFilledValue?: string | { from?: string; to?: string };
  46. filterObj?: T;
  47. handleSelectionChange?: (selectedOptions: T[]) => void;
  48. /** Hidden until the user clicks more criteria. */
  49. advanced?: boolean;
  50. }
  51. interface OptionWithLabel<T extends string> {
  52. label: string;
  53. value: any;
  54. }
  55. interface TextCriterion<T extends string> extends BaseCriterion<T> {
  56. type: "text";
  57. placeholder?: string;
  58. }
  59. interface SelectCriterion<T extends string> extends BaseCriterion<T> {
  60. type: "select";
  61. options: string[];
  62. }
  63. interface SelectWithLabelCriterion<T extends string> extends BaseCriterion<T> {
  64. type: "select-labelled";
  65. options: OptionWithLabel<T>[];
  66. }
  67. interface MultiSelectCriterion<T extends string> extends BaseCriterion<T> {
  68. type: "multi-select";
  69. options: T[];
  70. selectedOptions: T[];
  71. handleSelectionChange: (selectedOptions: T[]) => void;
  72. }
  73. interface AutocompleteOptions {
  74. value: string | number;
  75. label: string;
  76. group?: string;
  77. }
  78. interface AutocompleteCriterion<T extends string> extends BaseCriterion<T> {
  79. type: "autocomplete";
  80. options: AutocompleteOptions[];
  81. multiple?: boolean;
  82. noOptionsText?: string;
  83. needAll?: boolean;
  84. }
  85. interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
  86. type: "dateRange";
  87. }
  88. interface DatetimeRangeCriterion<T extends string> extends BaseCriterion<T> {
  89. type: "datetimeRange";
  90. }
  91. interface DateCriterion<T extends string> extends BaseCriterion<T> {
  92. type: "date";
  93. }
  94. interface TimeCriterion<T extends string> extends BaseCriterion<T> {
  95. type: "time";
  96. }
  97. export type Criterion<T extends string> =
  98. | TextCriterion<T>
  99. | SelectCriterion<T>
  100. | SelectWithLabelCriterion<T>
  101. | DateRangeCriterion<T>
  102. | DatetimeRangeCriterion<T>
  103. | DateCriterion<T>
  104. | TimeCriterion<T>
  105. | MultiSelectCriterion<T>
  106. | AutocompleteCriterion<T>;
  107. interface Props<T extends string> {
  108. criteria: Criterion<T>[];
  109. // TODO: may need to check the type is "autocomplete" and "multiple" = true, then allow string[].
  110. // TODO: may need to check the type is "dateRange", then add T and `${T}To` in the same time.
  111. // onSearch: (inputs: Record<T | (Criterion<T>["type"] extends "dateRange" ? `${T}To` : never), string>) => void;
  112. onSearch: (inputs: Record<T | `${T}To`, string>) => void;
  113. onReset?: () => void;
  114. /** Optional actions rendered in the same row as Reset/Search (e.g. Download, Upload buttons) */
  115. extraActions?: React.ReactNode;
  116. /** Optional filters rendered above the standard criteria fields */
  117. extraCriteria?: React.ReactNode;
  118. /** Disable inputs/actions while external task is running */
  119. disabled?: boolean;
  120. }
  121. /** FP-MTMS Version Checklist | Functions Ref. No. 79 | v1.0.1 | 2026-09-21 */
  122. function SearchBox<T extends string>({
  123. criteria,
  124. onSearch,
  125. onReset,
  126. extraActions,
  127. extraCriteria,
  128. disabled = false,
  129. }: Props<T>) {
  130. const { t } = useTranslation("common");
  131. const defaultAll: AutocompleteOptions = {
  132. value: "All",
  133. label: t("All"),
  134. group: t("All"),
  135. };
  136. const defaultInputs = useMemo(
  137. () =>
  138. criteria.reduce<Record<T | `${T}To`, string>>(
  139. (acc, c) => {
  140. let tempCriteria = {
  141. ...acc,
  142. [c.paramName]:
  143. c.type === "select" ||
  144. c.type === "select-labelled" ||
  145. (c.type === "autocomplete" && !Boolean(c.multiple))
  146. ? "All"
  147. : c.type === "autocomplete" && Boolean(c.multiple)
  148. ? [defaultAll.value]
  149. : "",
  150. };
  151. if (c.type === "dateRange" || c.type === "datetimeRange") {
  152. tempCriteria = {
  153. ...tempCriteria,
  154. [c.paramName]: c.defaultValue ?? "",
  155. [`${c.paramName}To`]: c.defaultValueTo ?? "",
  156. };
  157. }
  158. if (c.type === "date") {
  159. tempCriteria = {
  160. ...tempCriteria,
  161. [c.paramName]: c.defaultValue ?? "",
  162. };
  163. }
  164. return tempCriteria;
  165. },
  166. {} as Record<T | `${T}To`, string>,
  167. ),
  168. [criteria],
  169. );
  170. const preFilledInputs = useMemo(() => {
  171. const preFilledCriteria = criteria.reduce<Record<T | `${T}To`, string>>(
  172. (acc, c) => {
  173. if (c.type === "dateRange" || c.type === "datetimeRange") {
  174. if (typeof c.preFilledValue === "object" && c.preFilledValue !== null) {
  175. return {
  176. ...acc,
  177. ...(c.preFilledValue.from ? { [c.paramName]: c.preFilledValue.from } : {}),
  178. ...(c.preFilledValue.to ? { [`${c.paramName}To`]: c.preFilledValue.to } : {}),
  179. };
  180. }
  181. }
  182. if (typeof c.preFilledValue === "string") {
  183. return { ...acc, [c.paramName]: c.preFilledValue };
  184. }
  185. return acc;
  186. },
  187. {} as Record<T | `${T}To`, string>,
  188. );
  189. return {...defaultInputs, ...preFilledCriteria}
  190. }, [defaultInputs, criteria])
  191. const [inputs, setInputs] = useState(preFilledInputs);
  192. const [isReset, setIsReset] = useState(false);
  193. const [showAdvanced, setShowAdvanced] = useState(false);
  194. const hasAdvanced = criteria.some((c) => c.advanced);
  195. const visibleCriteria = showAdvanced ? criteria : criteria.filter((c) => !c.advanced);
  196. const makeInputChangeHandler = useCallback(
  197. (paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
  198. return (e) => {
  199. setInputs((i) => ({ ...i, [paramName]: e.target.value }));
  200. };
  201. },
  202. [],
  203. );
  204. const makeSelectChangeHandler = useCallback((paramName: T) => {
  205. return (e: SelectChangeEvent) => {
  206. setInputs((i) => ({ ...i, [paramName]: e.target.value }));
  207. };
  208. }, []);
  209. const makeAutocompleteChangeHandler = useCallback(
  210. (paramName: T, multiple: boolean) => {
  211. return (
  212. e: SyntheticEvent,
  213. newValue: AutocompleteOptions | AutocompleteOptions[],
  214. ) => {
  215. if (multiple) {
  216. const multiNewValue = newValue as AutocompleteOptions[];
  217. setInputs((i) => ({
  218. ...i,
  219. [paramName]: multiNewValue.map(({ value }) => value),
  220. }));
  221. } else {
  222. const singleNewValue = newValue as AutocompleteOptions;
  223. setInputs((i) => ({ ...i, [paramName]: singleNewValue.value }));
  224. }
  225. };
  226. },
  227. [],
  228. );
  229. const makeDateChangeHandler = useCallback((paramName: T) => {
  230. return (e: any) => {
  231. setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") }));
  232. };
  233. }, []);
  234. const makeDateToChangeHandler = useCallback((paramName: T) => {
  235. return (e: any) => {
  236. setInputs((i) => ({
  237. ...i,
  238. [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"),
  239. }));
  240. };
  241. }, []);
  242. const makeDatetimeChangeHandler = useCallback((paramName: T) => {
  243. return (value: Dayjs | null) => {
  244. setInputs((i) => ({
  245. ...i,
  246. [paramName]: value ? dayjsToDateTimeString(value) : null
  247. }));
  248. };
  249. }, []);
  250. const makeDatetimeToChangeHandler = useCallback((paramName: T) => {
  251. return (value: Dayjs | null) => {
  252. setInputs((i) => ({
  253. ...i,
  254. [paramName + "To"]: value ? dayjsToDateTimeString(value) : null
  255. }));
  256. };
  257. }, []);
  258. const makeTimeChangeHandler = useCallback((paramName: T) => {
  259. return (value: Dayjs | null) => {
  260. setInputs((i) => ({
  261. ...i,
  262. [paramName]: value ? value.format("HH:mm") : ""
  263. }));
  264. };
  265. }, []);
  266. const handleReset = () => {
  267. setInputs(preFilledInputs);
  268. onReset?.();
  269. setIsReset(!isReset);
  270. };
  271. const handleSearch = () => {
  272. onSearch(inputs);
  273. };
  274. return (
  275. <Card className="app-search-criteria" elevation={0}>
  276. <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
  277. <Typography className="app-search-criteria-label" variant="overline" sx={{ display: "block", mb: 0.5 }}>
  278. {t("Search Criteria")}
  279. </Typography>
  280. {extraCriteria}
  281. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  282. {visibleCriteria.map((c) => {
  283. return (
  284. <Grid key={c.paramName} item xs={6}>
  285. {c.type === "text" && (
  286. <TextField
  287. label={c.label}
  288. fullWidth
  289. placeholder={c.placeholder}
  290. onChange={makeInputChangeHandler(c.paramName)}
  291. value={inputs[c.paramName]}
  292. disabled={disabled}
  293. />
  294. )}
  295. {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
  296. {/* {c.type === "multi-select" && (
  297. <MultiSelect
  298. label={t(c.label)}
  299. options={c?.options}
  300. selectedValues={c.filterObj?.[c.paramName] ?? []}
  301. onChange={c.handleSelectionChange}
  302. isReset={isReset}
  303. />
  304. )} */}
  305. {c.type === "select" && (
  306. <FormControl fullWidth>
  307. <InputLabel>{c.label}</InputLabel>
  308. <Select
  309. label={c.label}
  310. onChange={makeSelectChangeHandler(c.paramName)}
  311. value={inputs[c.paramName] ?? "All"}
  312. disabled={disabled}
  313. >
  314. <MenuItem value={"All"}>{t("All")}</MenuItem>
  315. {c.options.map((option) => (
  316. <MenuItem key={option} value={option}>
  317. {t(option)}
  318. </MenuItem>
  319. ))}
  320. </Select>
  321. </FormControl>
  322. )}
  323. {c.type === "select-labelled" && (
  324. <FormControl fullWidth>
  325. <InputLabel>{c.label}</InputLabel>
  326. <Select
  327. label={c.label}
  328. onChange={makeSelectChangeHandler(c.paramName)}
  329. value={inputs[c.paramName] ?? "All"}
  330. disabled={disabled}
  331. >
  332. <MenuItem value={"All"}>{t("All")}</MenuItem>
  333. {c.options.map((option) => (
  334. <MenuItem key={option.value} value={option.value}>
  335. {option.label}
  336. </MenuItem>
  337. ))}
  338. </Select>
  339. </FormControl>
  340. )}
  341. {c.type === "autocomplete" && (
  342. <Autocomplete
  343. groupBy={
  344. c.options.filter((option) => option.group !== "All")
  345. .length > 0 && c.options.every((option) => option.group)
  346. ? (option) =>
  347. option.group && option.group.trim() !== ""
  348. ? option.group
  349. : "Ungrouped"
  350. : undefined
  351. }
  352. multiple={Boolean(c.multiple)}
  353. noOptionsText={c.noOptionsText ?? t("No options")}
  354. disableClearable
  355. fullWidth
  356. disabled={disabled}
  357. value={
  358. c.multiple
  359. ? intersectionWith(
  360. [defaultAll, ...c.options],
  361. inputs[c.paramName],
  362. (option, v) => {
  363. return option.value === (v ?? "");
  364. },
  365. )
  366. : c.options.find(
  367. (option) => option.value === inputs[c.paramName],
  368. ) ?? defaultAll
  369. }
  370. onChange={makeAutocompleteChangeHandler(
  371. c.paramName,
  372. Boolean(c.multiple),
  373. )}
  374. getOptionLabel={(option) => option.label}
  375. options={[defaultAll, ...c.options]}
  376. disableCloseOnSelect={Boolean(c.multiple)}
  377. renderGroup={
  378. c.options.every((option) => option.group)
  379. ? (params) => (
  380. <React.Fragment
  381. key={`${params.key}-${params.group}`}
  382. >
  383. <ListSubheader>{params.group}</ListSubheader>
  384. {params.children}
  385. </React.Fragment>
  386. )
  387. : undefined
  388. }
  389. renderTags={
  390. c.multiple
  391. ? (value, getTagProps) =>
  392. value.map((option, index) => {
  393. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  394. const { key, ...chipProps } = getTagProps({
  395. index,
  396. });
  397. return (
  398. <Chip
  399. {...chipProps}
  400. key={`${option.value}-${option.label}`}
  401. label={option.label}
  402. />
  403. );
  404. })
  405. : undefined
  406. }
  407. renderOption={(
  408. params: React.HTMLAttributes<HTMLLIElement> & {
  409. key?: React.Key;
  410. },
  411. option,
  412. { selected },
  413. ) => {
  414. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  415. const { key, ...rest } = params;
  416. return (
  417. <MenuItem
  418. {...rest}
  419. disableRipple
  420. value={option.value}
  421. key={`${option.value}--${option.label}`}
  422. >
  423. {c.multiple && (
  424. <Checkbox
  425. disableRipple
  426. key={`checkbox-${option.value}`}
  427. checked={selected}
  428. sx={{ transform: "translate(0)" }}
  429. />
  430. )}
  431. {option.label}
  432. </MenuItem>
  433. );
  434. }}
  435. renderInput={(params) => (
  436. <TextField
  437. {...params}
  438. variant="outlined"
  439. label={c.label}
  440. />
  441. )}
  442. />
  443. )}
  444. {c.type === "dateRange" && (
  445. <LocalizationProvider
  446. dateAdapter={AdapterDayjs}
  447. // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
  448. adapterLocale="zh-hk"
  449. >
  450. <Box display="flex">
  451. <FormControl fullWidth>
  452. <DatePicker
  453. format={`${OUTPUT_DATE_FORMAT}`}
  454. label={c.label}
  455. onChange={makeDateChangeHandler(c.paramName)}
  456. disabled={disabled}
  457. value={
  458. dayjs(inputs[c.paramName]).isValid()
  459. ? dayjs(inputs[c.paramName])
  460. : null
  461. }
  462. />
  463. </FormControl>
  464. <Box
  465. display="flex"
  466. alignItems="center"
  467. justifyContent="center"
  468. marginInline={2}
  469. >
  470. {"-"}
  471. </Box>
  472. <FormControl fullWidth>
  473. <DatePicker
  474. format={`${OUTPUT_DATE_FORMAT}`}
  475. label={c.label2 ? c.label2 : null}
  476. onChange={makeDateToChangeHandler(c.paramName)}
  477. disabled={disabled}
  478. value={
  479. dayjs(inputs[`${c.paramName}To`]).isValid()
  480. ? dayjs(inputs[`${c.paramName}To`])
  481. : null
  482. }
  483. />
  484. </FormControl>
  485. </Box>
  486. </LocalizationProvider>
  487. )}
  488. {c.type === "datetimeRange" && (
  489. <LocalizationProvider
  490. dateAdapter={AdapterDayjs}
  491. // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
  492. adapterLocale="zh-hk"
  493. >
  494. <Box display="flex">
  495. <FormControl fullWidth>
  496. <DateTimePicker
  497. views={['year','month','day','hours', 'minutes', 'seconds']}
  498. format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
  499. label={c.label}
  500. onChange={makeDatetimeChangeHandler(c.paramName)}
  501. disabled={disabled}
  502. value={
  503. dayjs(inputs[c.paramName]).isValid()
  504. ? dayjs(inputs[c.paramName])
  505. : null
  506. }
  507. />
  508. </FormControl>
  509. <Box
  510. display="flex"
  511. alignItems="center"
  512. justifyContent="center"
  513. marginInline={2}
  514. >
  515. {"-"}
  516. </Box>
  517. <FormControl fullWidth>
  518. <DateTimePicker
  519. views={['year','month','day','hours', 'minutes', 'seconds']}
  520. format={`${OUTPUT_DATE_FORMAT} ${OUTPUT_TIME_FORMAT}`}
  521. label={c.label2 ? c.label2 : null}
  522. onChange={makeDatetimeToChangeHandler(c.paramName)}
  523. disabled={disabled}
  524. value={
  525. dayjs(inputs[`${c.paramName}To`]).isValid()
  526. ? dayjs(inputs[`${c.paramName}To`])
  527. : null
  528. }
  529. />
  530. </FormControl>
  531. </Box>
  532. </LocalizationProvider>
  533. )}
  534. {c.type === "date" && (
  535. <LocalizationProvider
  536. dateAdapter={AdapterDayjs}
  537. // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
  538. adapterLocale="zh-hk"
  539. >
  540. <Box display="flex">
  541. <FormControl fullWidth>
  542. <DatePicker
  543. format={`${OUTPUT_DATE_FORMAT}`}
  544. label={c.label}
  545. onChange={makeDateChangeHandler(c.paramName)}
  546. disabled={disabled}
  547. value={
  548. inputs[c.paramName] && dayjs(inputs[c.paramName]).isValid()
  549. ? dayjs(inputs[c.paramName])
  550. : null
  551. }
  552. />
  553. </FormControl>
  554. </Box>
  555. </LocalizationProvider>
  556. )}
  557. {c.type === "time" && (
  558. <LocalizationProvider
  559. dateAdapter={AdapterDayjs}
  560. adapterLocale="zh-hk"
  561. >
  562. <FormControl fullWidth>
  563. <TimePicker
  564. format="HH:mm"
  565. label={c.label}
  566. onChange={makeTimeChangeHandler(c.paramName)}
  567. disabled={disabled}
  568. value={
  569. inputs[c.paramName] && dayjs(inputs[c.paramName], "HH:mm").isValid()
  570. ? dayjs(inputs[c.paramName], "HH:mm")
  571. : null
  572. }
  573. />
  574. </FormControl>
  575. </LocalizationProvider>
  576. )}
  577. </Grid>
  578. );
  579. })}
  580. </Grid>
  581. <CardActions sx={{ justifyContent: "flex-start", gap: 1, pt: 2, flexWrap: "wrap" }}>
  582. <Button
  583. variant="outlined"
  584. startIcon={<RestartAlt />}
  585. onClick={handleReset}
  586. disabled={disabled}
  587. sx={{ borderColor: "#e2e8f0", color: "#334155" }}
  588. >
  589. {t("Reset")}
  590. </Button>
  591. <Button
  592. variant="contained"
  593. color="primary"
  594. startIcon={<Search />}
  595. onClick={handleSearch}
  596. disabled={disabled}
  597. >
  598. {t("Search")}
  599. </Button>
  600. {hasAdvanced && (
  601. <Button
  602. variant="text"
  603. startIcon={showAdvanced ? <ExpandLess /> : <ExpandMore />}
  604. onClick={() => setShowAdvanced((open) => !open)}
  605. disabled={disabled}
  606. >
  607. {showAdvanced ? t("Fewer criteria") : t("More criteria")}
  608. </Button>
  609. )}
  610. {extraActions}
  611. </CardActions>
  612. </CardContent>
  613. </Card>
  614. );
  615. }
  616. export default SearchBox;