"use client"; 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, { SyntheticEvent, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import TextField from "@mui/material/TextField"; import FormControl from "@mui/material/FormControl"; import InputLabel from "@mui/material/InputLabel"; import Select, { SelectChangeEvent } from "@mui/material/Select"; import MenuItem from "@mui/material/MenuItem"; import CardActions from "@mui/material/CardActions"; import Button from "@mui/material/Button"; import RestartAlt from "@mui/icons-material/RestartAlt"; import Search from "@mui/icons-material/Search"; import ExpandMore from "@mui/icons-material/ExpandMore"; import ExpandLess from "@mui/icons-material/ExpandLess"; import dayjs, { Dayjs } from "dayjs"; import "dayjs/locale/zh-hk"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { TimePicker } from "@mui/x-date-pickers/TimePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { Autocomplete, Box, Checkbox, Chip, ListSubheader, } from "@mui/material"; import MultiSelect from "@/components/SearchBox/MultiSelect"; import { intersectionWith } from "lodash"; import { INPUT_DATE_FORMAT, INPUT_TIME_FORMAT, OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dayjsToDateTimeString } from "@/app/utils/formatUtil"; import { DateTimePicker } from "@mui/x-date-pickers"; interface BaseCriterion { label: string; label2?: string; paramName: T; paramName2?: T; // options?: T[] | string[]; defaultValue?: string; /** 與 `defaultValue` 配對,用於 dateRange / datetimeRange 重置時的結束值 */ defaultValueTo?: string; preFilledValue?: string | { from?: string; to?: string }; filterObj?: T; handleSelectionChange?: (selectedOptions: T[]) => void; /** Hidden until the user clicks more criteria. */ advanced?: boolean; /** Shown under the field in error color when set. */ errorText?: string; } interface OptionWithLabel { label: string; value: any; } interface TextCriterion extends BaseCriterion { type: "text"; placeholder?: string; } interface SelectCriterion extends BaseCriterion { type: "select"; options: string[]; } interface SelectWithLabelCriterion extends BaseCriterion { type: "select-labelled"; options: OptionWithLabel[]; } interface MultiSelectCriterion extends BaseCriterion { type: "multi-select"; options: T[]; selectedOptions: T[]; handleSelectionChange: (selectedOptions: T[]) => void; } interface AutocompleteOptions { value: string | number; label: string; group?: string; } interface AutocompleteCriterion extends BaseCriterion { type: "autocomplete"; options: AutocompleteOptions[]; multiple?: boolean; noOptionsText?: string; needAll?: boolean; } interface DateRangeCriterion extends BaseCriterion { type: "dateRange"; } interface DatetimeRangeCriterion extends BaseCriterion { type: "datetimeRange"; } interface DateCriterion extends BaseCriterion { type: "date"; } interface TimeCriterion extends BaseCriterion { type: "time"; } export type Criterion = | TextCriterion | SelectCriterion | SelectWithLabelCriterion | DateRangeCriterion | DatetimeRangeCriterion | DateCriterion | TimeCriterion | MultiSelectCriterion | AutocompleteCriterion; interface Props { criteria: Criterion[]; // TODO: may need to check the type is "autocomplete" and "multiple" = true, then allow string[]. // TODO: may need to check the type is "dateRange", then add T and `${T}To` in the same time. // onSearch: (inputs: Record["type"] extends "dateRange" ? `${T}To` : never), string>) => void; onSearch: (inputs: Record) => void; onReset?: () => void; /** Optional actions rendered in the same row as Reset/Search (e.g. Download, Upload buttons) */ extraActions?: React.ReactNode; /** Disable inputs/actions while external task is running */ disabled?: boolean; } /** FP-MTMS Version Checklist | Functions Ref. No. 79 | v1.0.1 | 2026-09-21 */ function SearchBox({ criteria, onSearch, onReset, extraActions, disabled = false, }: Props) { const { t } = useTranslation("common"); const defaultAll: AutocompleteOptions = { value: "All", label: t("All"), group: t("All"), }; const defaultInputs = useMemo( () => criteria.reduce>( (acc, c) => { let tempCriteria = { ...acc, [c.paramName]: c.type === "select" || c.type === "select-labelled" || (c.type === "autocomplete" && !Boolean(c.multiple)) ? "All" : c.type === "autocomplete" && Boolean(c.multiple) ? [defaultAll.value] : "", }; if (c.type === "dateRange" || c.type === "datetimeRange") { tempCriteria = { ...tempCriteria, [c.paramName]: c.defaultValue ?? "", [`${c.paramName}To`]: c.defaultValueTo ?? "", }; } if (c.type === "date") { tempCriteria = { ...tempCriteria, [c.paramName]: c.defaultValue ?? "", }; } return tempCriteria; }, {} as Record, ), [criteria], ); const preFilledInputs = useMemo(() => { const preFilledCriteria = criteria.reduce>( (acc, c) => { if (c.type === "dateRange" || c.type === "datetimeRange") { if (typeof c.preFilledValue === "object" && c.preFilledValue !== null) { return { ...acc, ...(c.preFilledValue.from ? { [c.paramName]: c.preFilledValue.from } : {}), ...(c.preFilledValue.to ? { [`${c.paramName}To`]: c.preFilledValue.to } : {}), }; } } if (typeof c.preFilledValue === "string") { return { ...acc, [c.paramName]: c.preFilledValue }; } return acc; }, {} as Record, ); return {...defaultInputs, ...preFilledCriteria} }, [defaultInputs, criteria]) const [inputs, setInputs] = useState(preFilledInputs); const [isReset, setIsReset] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const hasAdvanced = criteria.some((c) => c.advanced); const visibleCriteria = showAdvanced ? criteria : criteria.filter((c) => !c.advanced); const makeInputChangeHandler = useCallback( (paramName: T): React.ChangeEventHandler => { return (e) => { setInputs((i) => ({ ...i, [paramName]: e.target.value })); }; }, [], ); const makeSelectChangeHandler = useCallback((paramName: T) => { return (e: SelectChangeEvent) => { setInputs((i) => ({ ...i, [paramName]: e.target.value })); }; }, []); 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") })); }; }, []); const makeDateToChangeHandler = useCallback((paramName: T) => { return (e: any) => { setInputs((i) => ({ ...i, [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"), })); }; }, []); const makeDatetimeChangeHandler = useCallback((paramName: T) => { return (value: Dayjs | null) => { setInputs((i) => ({ ...i, [paramName]: value ? dayjsToDateTimeString(value) : null })); }; }, []); const makeDatetimeToChangeHandler = useCallback((paramName: T) => { return (value: Dayjs | null) => { setInputs((i) => ({ ...i, [paramName + "To"]: value ? dayjsToDateTimeString(value) : null })); }; }, []); const makeTimeChangeHandler = useCallback((paramName: T) => { return (value: Dayjs | null) => { setInputs((i) => ({ ...i, [paramName]: value ? value.format("HH:mm") : "" })); }; }, []); const handleReset = () => { setInputs(preFilledInputs); onReset?.(); setIsReset(!isReset); }; const handleSearch = () => { onSearch(inputs); }; return ( {t("Search Criteria")} {visibleCriteria.map((c) => { return ( {c.type === "text" && ( )} {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */} {/* {c.type === "multi-select" && ( )} */} {c.type === "select" && ( {c.label} )} {c.type === "select-labelled" && ( {c.label} )} {c.type === "autocomplete" && ( option.group !== "All") .length > 0 && 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 disabled={disabled} value={ c.multiple ? intersectionWith( [defaultAll, ...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) => ( {params.group} {params.children} ) : 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 ( ); }) : undefined } renderOption={( params: React.HTMLAttributes & { key?: React.Key; }, option, { selected }, ) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { key, ...rest } = params; return ( {c.multiple && ( )} {option.label} ); }} renderInput={(params) => ( )} /> )} {c.type === "dateRange" && ( {"-"} )} {c.type === "datetimeRange" && ( {"-"} )} {c.type === "date" && ( )} {c.type === "time" && ( )} ); })} {hasAdvanced && ( )} {extraActions} ); } export default SearchBox;