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.
 
 

218 lignes
6.5 KiB

  1. "use client";
  2. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  3. import RestartAlt from "@mui/icons-material/RestartAlt";
  4. import Search from "@mui/icons-material/Search";
  5. import {
  6. Box,
  7. Button,
  8. Card,
  9. CardActions,
  10. CardContent,
  11. FormControl,
  12. Grid,
  13. InputLabel,
  14. MenuItem,
  15. Select,
  16. SelectChangeEvent,
  17. TextField,
  18. Typography,
  19. } from "@mui/material";
  20. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  21. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  22. import dayjs from "dayjs";
  23. import "dayjs/locale/zh-hk";
  24. import { useCallback, useMemo, useState } from "react";
  25. import { useTranslation } from "react-i18next";
  26. export type StockIssueSearchFieldType = "text" | "select" | "date" | "number";
  27. export interface StockIssueSearchField<K extends string> {
  28. name: K;
  29. label: string;
  30. type: StockIssueSearchFieldType;
  31. options?: string[];
  32. /** Optional label for select option values (defaults to i18n `t(option)`). */
  33. getOptionLabel?: (value: string) => string;
  34. /** When this date is picked, copy the same value to `mirrorTo`. */
  35. mirrorTo?: K;
  36. defaultValue?: string;
  37. min?: number;
  38. max?: number;
  39. }
  40. interface Props<K extends string> {
  41. fields: StockIssueSearchField<K>[];
  42. onSearch: (values: Record<K, string>) => void;
  43. onReset?: () => void;
  44. extraActions?: React.ReactNode;
  45. disabled?: boolean;
  46. }
  47. /** FP-MTMS Version Checklist | Functions Ref. No. 74 | v1.0.2 | 2026-09-07 */
  48. function StockIssueSearchPanel<K extends string>({
  49. fields,
  50. onSearch,
  51. onReset,
  52. extraActions,
  53. disabled = false,
  54. }: Props<K>) {
  55. const { t } = useTranslation("stockIssue");
  56. const { t: tCommon } = useTranslation("common"); // All
  57. const emptyValues = useMemo(() => {
  58. return fields.reduce(
  59. (acc, field) => {
  60. acc[field.name] =
  61. field.defaultValue ??
  62. (field.type === "select" ? "All" : "");
  63. return acc;
  64. },
  65. {} as Record<K, string>,
  66. );
  67. }, [fields]);
  68. const [values, setValues] = useState<Record<K, string>>(emptyValues);
  69. const handleTextChange = useCallback(
  70. (name: K) => (e: React.ChangeEvent<HTMLInputElement>) => {
  71. setValues((prev) => ({ ...prev, [name]: e.target.value }));
  72. },
  73. [],
  74. );
  75. const handleSelectChange = useCallback(
  76. (name: K) => (e: SelectChangeEvent) => {
  77. setValues((prev) => ({ ...prev, [name]: e.target.value }));
  78. },
  79. [],
  80. );
  81. const handleDateChange = useCallback(
  82. (name: K, mirrorTo?: K) => (date: dayjs.Dayjs | null) => {
  83. const formatted =
  84. date && dayjs(date).isValid() ? dayjs(date).format("YYYY-MM-DD") : "";
  85. setValues((prev) => ({
  86. ...prev,
  87. [name]: formatted,
  88. ...(mirrorTo ? { [mirrorTo]: formatted } : {}),
  89. }));
  90. },
  91. [],
  92. );
  93. const handleReset = () => {
  94. setValues(emptyValues);
  95. onReset?.();
  96. };
  97. const handleSearchClick = () => {
  98. onSearch(values);
  99. };
  100. return (
  101. <Card className="app-search-criteria" elevation={0} sx={{ mb: 2 }}>
  102. <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
  103. <Typography
  104. className="app-search-criteria-label"
  105. variant="overline"
  106. sx={{ display: "block", mb: 0.5 }}
  107. >
  108. {t("Search Criteria")}
  109. </Typography>
  110. <Grid container spacing={2} columns={{ xs: 12, sm: 12, md: 12, lg: 12 }}>
  111. {fields.map((field) => (
  112. <Grid key={field.name} item xs={12} sm={6} md={4} lg={3}>
  113. {field.type === "text" && (
  114. <TextField
  115. label={field.label}
  116. fullWidth
  117. value={values[field.name] ?? ""}
  118. onChange={handleTextChange(field.name)}
  119. disabled={disabled}
  120. />
  121. )}
  122. {field.type === "number" && (
  123. <TextField
  124. label={field.label}
  125. type="number"
  126. fullWidth
  127. value={values[field.name] ?? ""}
  128. onChange={handleTextChange(field.name)}
  129. disabled={disabled}
  130. inputProps={{
  131. min: field.min,
  132. max: field.max,
  133. step: 1,
  134. }}
  135. />
  136. )}
  137. {field.type === "select" && (
  138. <FormControl fullWidth disabled={disabled}>
  139. <InputLabel>{field.label}</InputLabel>
  140. <Select
  141. label={field.label}
  142. value={values[field.name] ?? "All"}
  143. onChange={handleSelectChange(field.name)}
  144. >
  145. <MenuItem value="All">{tCommon("All")}</MenuItem>
  146. {(field.options ?? []).map((option) => (
  147. <MenuItem key={option} value={option}>
  148. {field.getOptionLabel?.(option) ?? t(option)}
  149. </MenuItem>
  150. ))}
  151. </Select>
  152. </FormControl>
  153. )}
  154. {field.type === "date" && (
  155. <LocalizationProvider
  156. dateAdapter={AdapterDayjs}
  157. adapterLocale="zh-hk"
  158. >
  159. <DatePicker
  160. label={field.label}
  161. format={OUTPUT_DATE_FORMAT}
  162. disabled={disabled}
  163. value={
  164. values[field.name] && dayjs(values[field.name]).isValid()
  165. ? dayjs(values[field.name])
  166. : null
  167. }
  168. onChange={handleDateChange(field.name, field.mirrorTo)}
  169. slotProps={{ textField: { fullWidth: true } }}
  170. />
  171. </LocalizationProvider>
  172. )}
  173. </Grid>
  174. ))}
  175. </Grid>
  176. <CardActions
  177. sx={{ justifyContent: "flex-start", gap: 1, pt: 2, flexWrap: "wrap", px: 0 }}
  178. >
  179. <Button
  180. variant="outlined"
  181. startIcon={<RestartAlt />}
  182. onClick={handleReset}
  183. disabled={disabled}
  184. sx={{ borderColor: "#e2e8f0", color: "#334155" }}
  185. >
  186. {t("Reset")}
  187. </Button>
  188. <Button
  189. variant="contained"
  190. color="primary"
  191. startIcon={<Search />}
  192. onClick={handleSearchClick}
  193. disabled={disabled}
  194. >
  195. {t("Search")}
  196. </Button>
  197. {extraActions}
  198. </CardActions>
  199. </CardContent>
  200. </Card>
  201. );
  202. }
  203. export default StockIssueSearchPanel;