選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

317 行
11 KiB

  1. //src\components\ReportSearchBox\SearchBox.tsx
  2. "use client";
  3. import Grid from "@mui/material/Grid";
  4. import Card from "@mui/material/Card";
  5. import CardContent from "@mui/material/CardContent";
  6. import Typography from "@mui/material/Typography";
  7. import React, { useCallback, useMemo, useState } from "react";
  8. import { useTranslation } from "react-i18next";
  9. import TextField from "@mui/material/TextField";
  10. import FormControl from "@mui/material/FormControl";
  11. import InputLabel from "@mui/material/InputLabel";
  12. import Select, { SelectChangeEvent } from "@mui/material/Select";
  13. import MenuItem from "@mui/material/MenuItem";
  14. import CardActions from "@mui/material/CardActions";
  15. import Button from "@mui/material/Button";
  16. import RestartAlt from "@mui/icons-material/RestartAlt";
  17. import Search from "@mui/icons-material/Search";
  18. import dayjs from "dayjs";
  19. import "dayjs/locale/zh-hk";
  20. import { DatePicker } from "@mui/x-date-pickers/DatePicker";
  21. import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
  22. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  23. import { Box } from "@mui/material";
  24. import * as XLSX from 'xlsx-js-style';
  25. //import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton';
  26. interface BaseCriterion<T extends string> {
  27. label: string;
  28. label2?: string;
  29. paramName: T;
  30. paramName2?: T;
  31. }
  32. interface TextCriterion<T extends string> extends BaseCriterion<T> {
  33. type: "text";
  34. }
  35. interface SelectCriterion<T extends string> extends BaseCriterion<T> {
  36. type: "select";
  37. options: string[];
  38. }
  39. interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
  40. type: "dateRange";
  41. }
  42. export type Criterion<T extends string> =
  43. | TextCriterion<T>
  44. | SelectCriterion<T>
  45. | DateRangeCriterion<T>;
  46. interface Props<T extends string> {
  47. criteria: Criterion<T>[];
  48. onSearch: (inputs: Record<T, string>) => void;
  49. onReset?: () => void;
  50. }
  51. function SearchBox<T extends string>({
  52. criteria,
  53. onSearch,
  54. onReset,
  55. }: Props<T>) {
  56. const { t } = useTranslation("common");
  57. const defaultInputs = useMemo(
  58. () =>
  59. criteria.reduce<Record<T, string>>(
  60. (acc, c) => {
  61. return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" };
  62. },
  63. {} as Record<T, string>,
  64. ),
  65. [criteria],
  66. );
  67. const [inputs, setInputs] = useState(defaultInputs);
  68. const makeInputChangeHandler = useCallback(
  69. (paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
  70. return (e) => {
  71. setInputs((i) => ({ ...i, [paramName]: e.target.value }));
  72. };
  73. },
  74. [],
  75. );
  76. const makeSelectChangeHandler = useCallback((paramName: T) => {
  77. return (e: SelectChangeEvent) => {
  78. setInputs((i) => ({ ...i, [paramName]: e.target.value }));
  79. };
  80. }, []);
  81. const makeDateChangeHandler = useCallback((paramName: T) => {
  82. return (e: any) => {
  83. setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") }));
  84. };
  85. }, []);
  86. const makeDateToChangeHandler = useCallback((paramName: T) => {
  87. return (e: any) => {
  88. setInputs((i) => ({
  89. ...i,
  90. [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"),
  91. }));
  92. };
  93. }, []);
  94. const handleReset = () => {
  95. setInputs(defaultInputs);
  96. onReset?.();
  97. };
  98. const handleSearch = () => {
  99. onSearch(inputs);
  100. };
  101. const handleDownload = async () => {
  102. //setIsLoading(true);
  103. try {
  104. const response = await fetch('/temp/AR06_Project Completion Report with Outstanding Un-billed Hours.xlsx', {
  105. headers: {
  106. 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  107. },
  108. });
  109. if (!response.ok) throw new Error('Network response was not ok.');
  110. const data = await response.blob();
  111. const reader = new FileReader();
  112. reader.onload = (e) => {
  113. if (e.target && e.target.result) {
  114. const ab = e.target.result as ArrayBuffer;
  115. const workbook = XLSX.read(ab, { type: 'array' });
  116. const firstSheetName = workbook.SheetNames[0];
  117. const worksheet = workbook.Sheets[firstSheetName];
  118. // Add the current date to cell C2
  119. const cellAddress = 'C2';
  120. const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD
  121. const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD
  122. XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress });
  123. // Calculate the maximum length of content in each column and set column width
  124. const colWidths: number[] = [];
  125. const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][];
  126. jsonData.forEach((row: (string | number)[]) => {
  127. row.forEach((cell: string | number, index: number) => {
  128. const valueLength = cell.toString().length;
  129. colWidths[index] = Math.max(colWidths[index] || 0, valueLength);
  130. });
  131. });
  132. // Apply calculated widths to each column, skipping column A
  133. worksheet['!cols'] = colWidths.map((width, index) => {
  134. if (index === 0) {
  135. return { wch: 8 }; // Set default or specific width for column A if needed
  136. }
  137. return { wch: width + 2 }; // Add padding to width
  138. });
  139. // Style for cell A1: Font size 16 and bold
  140. if (worksheet['A1']) {
  141. worksheet['A1'].s = {
  142. font: {
  143. bold: true,
  144. sz: 16, // Font size 16
  145. //name: 'Times New Roman' // Specify font
  146. }
  147. };
  148. }
  149. // Apply styles from A2 to A3 (bold)
  150. ['A2', 'A3'].forEach(cell => {
  151. if (worksheet[cell]) {
  152. worksheet[cell].s = { font: { bold: true } };
  153. }
  154. });
  155. // Formatting from A5 to G5
  156. // Apply styles from A5 to G5 (bold, bottom border, center alignment)
  157. for (let col = 0; col < 7; col++) { // Columns A to G
  158. const cellRef = XLSX.utils.encode_col(col) + '5';
  159. if (worksheet[cellRef]) {
  160. worksheet[cellRef].s = {
  161. font: { bold: true },
  162. alignment: { horizontal: 'center' },
  163. border: {
  164. bottom: { style: 'thin', color: { auto: 1 } }
  165. }
  166. };
  167. }
  168. }
  169. const firstTableData = [
  170. ['Column1', 'Column2', 'Column3'], // Row 1
  171. ['Data1', 'Data2', 'Data3'], // Row 2
  172. // ... more rows as needed
  173. ];
  174. // Find the last row of the first table
  175. let lastRowOfFirstTable = 5; // Starting row for data in the first table
  176. while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) {
  177. lastRowOfFirstTable++;
  178. }
  179. // Insert the first data form into the worksheet at the desired location
  180. XLSX.utils.sheet_add_aoa(worksheet, firstTableData, { origin: { c: 0, r: lastRowOfFirstTable } });
  181. // Format filename with date
  182. const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD
  183. const filename = `AR06_Project_Completion_Report_with_Outstanding_Un-billed_Hours_${today}.xlsx`; // Append formatted date to the filename
  184. // Convert workbook back to XLSX file
  185. XLSX.writeFile(workbook, filename);
  186. } else {
  187. throw new Error('Failed to load file');
  188. }
  189. };
  190. reader.readAsArrayBuffer(data);
  191. } catch (error) {
  192. console.error('Error downloading the file: ', error);
  193. }
  194. //setIsLoading(false);
  195. };
  196. return (
  197. <Card>
  198. <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
  199. <Typography variant="overline">{t("Search Criteria")}</Typography>
  200. <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
  201. {criteria.map((c) => {
  202. return (
  203. <Grid key={c.paramName} item xs={6}>
  204. {c.type === "text" && (
  205. <TextField
  206. label={c.label}
  207. fullWidth
  208. onChange={makeInputChangeHandler(c.paramName)}
  209. value={inputs[c.paramName]}
  210. />
  211. )}
  212. {c.type === "select" && (
  213. <FormControl fullWidth>
  214. <InputLabel>{c.label}</InputLabel>
  215. <Select
  216. label={c.label}
  217. onChange={makeSelectChangeHandler(c.paramName)}
  218. value={inputs[c.paramName]}
  219. >
  220. <MenuItem value={"All"}>{t("All")}</MenuItem>
  221. {c.options.map((option, index) => (
  222. <MenuItem key={`${option}-${index}`} value={option}>
  223. {option}
  224. </MenuItem>
  225. ))}
  226. </Select>
  227. </FormControl>
  228. )}
  229. {c.type === "dateRange" && (
  230. <LocalizationProvider
  231. dateAdapter={AdapterDayjs}
  232. // TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
  233. adapterLocale="zh-hk"
  234. >
  235. <Box display="flex">
  236. <FormControl fullWidth>
  237. <DatePicker
  238. label={c.label}
  239. onChange={makeDateChangeHandler(c.paramName)}
  240. value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null}
  241. />
  242. </FormControl>
  243. <Box
  244. display="flex"
  245. alignItems="center"
  246. justifyContent="center"
  247. marginInline={2}
  248. >
  249. {"-"}
  250. </Box>
  251. <FormControl fullWidth>
  252. <DatePicker
  253. label={c.label2}
  254. onChange={makeDateToChangeHandler(c.paramName)}
  255. value={inputs[c.paramName.concat("To") as T] ? dayjs(inputs[c.paramName.concat("To") as T]) : null}
  256. />
  257. </FormControl>
  258. </Box>
  259. </LocalizationProvider>
  260. )}
  261. </Grid>
  262. );
  263. })}
  264. </Grid>
  265. <CardActions sx={{ justifyContent: "flex-end" }}>
  266. <Button
  267. variant="text"
  268. startIcon={<RestartAlt />}
  269. onClick={handleReset}
  270. >
  271. {t("Reset")}
  272. </Button>
  273. <Button
  274. variant="outlined"
  275. startIcon={<Search />}
  276. onClick={handleDownload}
  277. >
  278. {t("Download")}
  279. </Button>
  280. </CardActions>
  281. </CardContent>
  282. </Card>
  283. );
  284. }
  285. export default SearchBox;