|
- "use client";
-
- import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
- import RestartAlt from "@mui/icons-material/RestartAlt";
- import Search from "@mui/icons-material/Search";
- import {
- Box,
- Button,
- Card,
- CardActions,
- CardContent,
- FormControl,
- Grid,
- InputLabel,
- MenuItem,
- Select,
- SelectChangeEvent,
- TextField,
- Typography,
- } from "@mui/material";
- import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
- import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
- import dayjs from "dayjs";
- import "dayjs/locale/zh-hk";
- import { useCallback, useMemo, useState } from "react";
- import { useTranslation } from "react-i18next";
-
- export type StockIssueSearchFieldType = "text" | "select" | "date" | "number";
-
- export interface StockIssueSearchField<K extends string> {
- name: K;
- label: string;
- type: StockIssueSearchFieldType;
- options?: string[];
- /** Optional label for select option values (defaults to i18n `t(option)`). */
- getOptionLabel?: (value: string) => string;
- /** When this date is picked, copy the same value to `mirrorTo`. */
- mirrorTo?: K;
- defaultValue?: string;
- min?: number;
- max?: number;
- }
-
- interface Props<K extends string> {
- fields: StockIssueSearchField<K>[];
- onSearch: (values: Record<K, string>) => void;
- onReset?: () => void;
- extraActions?: React.ReactNode;
- disabled?: boolean;
- }
-
- /** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.1 | 2026-09-07 */
- function StockIssueSearchPanel<K extends string>({
- fields,
- onSearch,
- onReset,
- extraActions,
- disabled = false,
- }: Props<K>) {
- const { t } = useTranslation("stockIssue");
- const { t: tCommon } = useTranslation("common"); // All
-
- const emptyValues = useMemo(() => {
- return fields.reduce(
- (acc, field) => {
- acc[field.name] =
- field.defaultValue ??
- (field.type === "select" ? "All" : "");
- return acc;
- },
- {} as Record<K, string>,
- );
- }, [fields]);
-
- const [values, setValues] = useState<Record<K, string>>(emptyValues);
-
- const handleTextChange = useCallback(
- (name: K) => (e: React.ChangeEvent<HTMLInputElement>) => {
- setValues((prev) => ({ ...prev, [name]: e.target.value }));
- },
- [],
- );
-
- const handleSelectChange = useCallback(
- (name: K) => (e: SelectChangeEvent) => {
- setValues((prev) => ({ ...prev, [name]: e.target.value }));
- },
- [],
- );
-
- const handleDateChange = useCallback(
- (name: K, mirrorTo?: K) => (date: dayjs.Dayjs | null) => {
- const formatted =
- date && dayjs(date).isValid() ? dayjs(date).format("YYYY-MM-DD") : "";
- setValues((prev) => ({
- ...prev,
- [name]: formatted,
- ...(mirrorTo ? { [mirrorTo]: formatted } : {}),
- }));
- },
- [],
- );
-
- const handleReset = () => {
- setValues(emptyValues);
- onReset?.();
- };
-
- const handleSearchClick = () => {
- onSearch(values);
- };
-
- return (
- <Card className="app-search-criteria" elevation={0} sx={{ mb: 2 }}>
- <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
- <Typography
- className="app-search-criteria-label"
- variant="overline"
- sx={{ display: "block", mb: 0.5 }}
- >
- {t("Search Criteria")}
- </Typography>
- <Grid container spacing={2} columns={{ xs: 12, sm: 12, md: 12, lg: 12 }}>
- {fields.map((field) => (
- <Grid key={field.name} item xs={12} sm={6} md={4} lg={3}>
- {field.type === "text" && (
- <TextField
- label={field.label}
- fullWidth
- value={values[field.name] ?? ""}
- onChange={handleTextChange(field.name)}
- disabled={disabled}
- />
- )}
- {field.type === "number" && (
- <TextField
- label={field.label}
- type="number"
- fullWidth
- value={values[field.name] ?? ""}
- onChange={handleTextChange(field.name)}
- disabled={disabled}
- inputProps={{
- min: field.min,
- max: field.max,
- step: 1,
- }}
- />
- )}
- {field.type === "select" && (
- <FormControl fullWidth disabled={disabled}>
- <InputLabel>{field.label}</InputLabel>
- <Select
- label={field.label}
- value={values[field.name] ?? "All"}
- onChange={handleSelectChange(field.name)}
- >
- <MenuItem value="All">{tCommon("All")}</MenuItem>
- {(field.options ?? []).map((option) => (
- <MenuItem key={option} value={option}>
- {field.getOptionLabel?.(option) ?? t(option)}
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- )}
- {field.type === "date" && (
- <LocalizationProvider
- dateAdapter={AdapterDayjs}
- adapterLocale="zh-hk"
- >
- <DatePicker
- label={field.label}
- format={OUTPUT_DATE_FORMAT}
- disabled={disabled}
- value={
- values[field.name] && dayjs(values[field.name]).isValid()
- ? dayjs(values[field.name])
- : null
- }
- onChange={handleDateChange(field.name, field.mirrorTo)}
- slotProps={{ textField: { fullWidth: true } }}
- />
- </LocalizationProvider>
- )}
- </Grid>
- ))}
- </Grid>
- <CardActions
- sx={{ justifyContent: "flex-start", gap: 1, pt: 2, flexWrap: "wrap", px: 0 }}
- >
- <Button
- variant="outlined"
- startIcon={<RestartAlt />}
- onClick={handleReset}
- disabled={disabled}
- sx={{ borderColor: "#e2e8f0", color: "#334155" }}
- >
- {t("Reset")}
- </Button>
- <Button
- variant="contained"
- color="primary"
- startIcon={<Search />}
- onClick={handleSearchClick}
- disabled={disabled}
- >
- {t("Search")}
- </Button>
- {extraActions}
- </CardActions>
- </CardContent>
- </Card>
- );
- }
-
- export default StockIssueSearchPanel;
|