FPSMS-frontend
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 

350 wiersze
13 KiB

  1. "use client";
  2. import React, { useState, useEffect, useCallback, useRef } from "react";
  3. import {
  4. Box,
  5. Typography,
  6. Card,
  7. CardContent,
  8. Table,
  9. TableBody,
  10. TableCell,
  11. TableContainer,
  12. TableHead,
  13. TableRow,
  14. Paper,
  15. CircularProgress,
  16. Stack,
  17. IconButton,
  18. Collapse,
  19. Link as MuiLink,
  20. } from "@mui/material";
  21. import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
  22. import ExpandLessIcon from "@mui/icons-material/ExpandLess";
  23. import NextLink from "next/link";
  24. import { useTranslation } from "react-i18next";
  25. import dayjs from "dayjs";
  26. import type { Dayjs } from "dayjs";
  27. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  28. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  29. import {
  30. fetchDrinkProductionQty,
  31. DrinkProductionQtyResponse,
  32. } from "@/app/api/jo/actions";
  33. const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 分鐘
  34. const formatQty = (qty: number | null | undefined): string => {
  35. if (qty === null || qty === undefined || Number.isNaN(qty)) return "-";
  36. return qty.toLocaleString(undefined, { maximumFractionDigits: 2 });
  37. };
  38. const formatProductionDate = (value: string | null | undefined): string => {
  39. if (!value) return "-";
  40. const parsed = dayjs(value);
  41. return parsed.isValid() ? parsed.format("YYYY-MM-DD") : value;
  42. };
  43. const getRowKey = (row: DrinkProductionQtyResponse, idx: number): string =>
  44. `${row.itemCode || "unknown"}-${idx}`;
  45. const DrinkProductionQtyDashboard: React.FC = () => {
  46. const { t } = useTranslation(["common", "jo", "productionProcess"]);
  47. const [data, setData] = useState<DrinkProductionQtyResponse[]>([]);
  48. const [loading, setLoading] = useState<boolean>(true);
  49. const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
  50. const [expandedRowKeys, setExpandedRowKeys] = useState<Set<string>>(
  51. new Set(),
  52. );
  53. const refreshCountRef = useRef<number>(0);
  54. const [lastDataRefreshTime, setLastDataRefreshTime] = useState<Dayjs | null>(
  55. null,
  56. );
  57. const loadData = useCallback(async () => {
  58. setLoading(true);
  59. try {
  60. const result = await fetchDrinkProductionQty(
  61. selectedDate.format("YYYY-MM-DD"),
  62. );
  63. setData(result || []);
  64. setExpandedRowKeys(new Set());
  65. setLastDataRefreshTime(dayjs());
  66. refreshCountRef.current += 1;
  67. } catch (error) {
  68. console.error("Error fetching drink production qty:", error);
  69. setData([]);
  70. setExpandedRowKeys(new Set());
  71. } finally {
  72. setLoading(false);
  73. }
  74. }, [selectedDate]);
  75. useEffect(() => {
  76. loadData();
  77. const interval = setInterval(() => {
  78. loadData();
  79. }, REFRESH_INTERVAL);
  80. return () => clearInterval(interval);
  81. }, [loadData]);
  82. const toggleRowExpanded = (rowKey: string) => {
  83. setExpandedRowKeys((prev) => {
  84. const next = new Set(prev);
  85. if (next.has(rowKey)) {
  86. next.delete(rowKey);
  87. } else {
  88. next.add(rowKey);
  89. }
  90. return next;
  91. });
  92. };
  93. return (
  94. <Card sx={{ mb: 2 }}>
  95. <CardContent>
  96. <Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}>
  97. {t("Drink Production Qty Dashboard")}
  98. </Typography>
  99. <Stack direction="row" spacing={2} sx={{ mb: 3, alignItems: "center" }}>
  100. <LocalizationProvider dateAdapter={AdapterDayjs}>
  101. <DatePicker
  102. label={t("Date")}
  103. value={selectedDate}
  104. onChange={(newValue) => {
  105. if (newValue) setSelectedDate(newValue);
  106. }}
  107. format="YYYY-MM-DD"
  108. slotProps={{
  109. textField: { size: "small", sx: { minWidth: 160 } },
  110. }}
  111. />
  112. </LocalizationProvider>
  113. <Box sx={{ flexGrow: 1 }} />
  114. <Typography
  115. variant="body2"
  116. sx={{ color: "text.secondary" }}
  117. suppressHydrationWarning
  118. >
  119. {t("Auto-refresh every 10 minutes")}&nbsp;|&nbsp;
  120. {t("Last updated")}:{" "}
  121. {lastDataRefreshTime
  122. ? lastDataRefreshTime.format("HH:mm:ss")
  123. : "--:--:--"}
  124. </Typography>
  125. </Stack>
  126. {loading ? (
  127. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  128. <CircularProgress />
  129. </Box>
  130. ) : (
  131. <TableContainer
  132. sx={{
  133. border: "3px solid #135fed",
  134. overflowX: "auto",
  135. maxHeight: 440,
  136. overflow: "auto",
  137. }}
  138. component={Paper}
  139. >
  140. <Table size="small" sx={{ minWidth: 650 }}>
  141. <TableHead>
  142. <TableRow
  143. sx={{
  144. bgcolor: "#424242",
  145. "& th": {
  146. borderBottom: "none",
  147. py: 1.5,
  148. position: "sticky",
  149. top: 0,
  150. zIndex: 1,
  151. },
  152. }}
  153. >
  154. <TableCell sx={{ width: 48 }} />
  155. <TableCell sx={{ width: 160 }}>
  156. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  157. {t("Item Code")}
  158. </Typography>
  159. </TableCell>
  160. <TableCell sx={{ width: 260 }}>
  161. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  162. {t("Goods Name")}
  163. </Typography>
  164. </TableCell>
  165. <TableCell sx={{ width: 120 }}>
  166. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  167. {t("Unit")}
  168. </Typography>
  169. </TableCell>
  170. <TableCell align="right" sx={{ width: 120 }}>
  171. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  172. {t("Stock Req. Qty")}
  173. </Typography>
  174. </TableCell>
  175. <TableCell align="right" sx={{ width: 140 }}>
  176. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  177. {t("Production Qty")}
  178. </Typography>
  179. </TableCell>
  180. </TableRow>
  181. </TableHead>
  182. <TableBody>
  183. {data.length === 0 ? (
  184. <TableRow>
  185. <TableCell colSpan={6} align="center">
  186. <Typography
  187. variant="body2"
  188. sx={{ py: 2, color: "text.secondary" }}
  189. >
  190. {t("No data available")}
  191. </Typography>
  192. </TableCell>
  193. </TableRow>
  194. ) : (
  195. data.map((row, idx) => {
  196. const rowKey = getRowKey(row, idx);
  197. const jobOrders = row.jobOrders ?? [];
  198. const isExpanded = expandedRowKeys.has(rowKey);
  199. const hasJobOrders = jobOrders.length > 0;
  200. return (
  201. <React.Fragment key={rowKey}>
  202. <TableRow hover={hasJobOrders}>
  203. <TableCell padding="checkbox">
  204. {hasJobOrders ? (
  205. <IconButton
  206. size="small"
  207. aria-label={
  208. isExpanded
  209. ? t("Collapse job order details")
  210. : t("Expand job order details")
  211. }
  212. onClick={() => toggleRowExpanded(rowKey)}
  213. >
  214. {isExpanded ? (
  215. <ExpandLessIcon fontSize="small" />
  216. ) : (
  217. <ExpandMoreIcon fontSize="small" />
  218. )}
  219. </IconButton>
  220. ) : null}
  221. </TableCell>
  222. <TableCell>
  223. <Typography variant="body2">
  224. {row.itemCode || "-"}
  225. </Typography>
  226. </TableCell>
  227. <TableCell>
  228. <Typography variant="body2">
  229. {row.itemName || "-"}
  230. </Typography>
  231. </TableCell>
  232. <TableCell>
  233. <Typography variant="body2">
  234. {row.uom || "-"}
  235. </Typography>
  236. </TableCell>
  237. <TableCell align="right">
  238. <Typography variant="body2">
  239. {formatQty(row.totalReqQty)}
  240. </Typography>
  241. </TableCell>
  242. <TableCell align="right">
  243. <Typography variant="body2">
  244. {formatQty(row.totalQty)}
  245. </Typography>
  246. </TableCell>
  247. </TableRow>
  248. {hasJobOrders && (
  249. <TableRow>
  250. <TableCell
  251. colSpan={6}
  252. sx={{ py: 0, borderBottom: 0 }}
  253. >
  254. <Collapse
  255. in={isExpanded}
  256. timeout="auto"
  257. unmountOnExit
  258. >
  259. <Box sx={{ py: 1.5, pl: 6, pr: 2 }}>
  260. <Table size="small">
  261. <TableHead>
  262. <TableRow>
  263. <TableCell sx={{ fontWeight: 600 }}>
  264. {t("Job Order Code")}
  265. </TableCell>
  266. <TableCell sx={{ fontWeight: 600 }}>
  267. {t("Production Date")}
  268. </TableCell>
  269. <TableCell
  270. align="right"
  271. sx={{ fontWeight: 600 }}
  272. >
  273. {t("Stock Req. Qty")}
  274. </TableCell>
  275. <TableCell
  276. align="right"
  277. sx={{ fontWeight: 600 }}
  278. >
  279. {t("Production Qty")}
  280. </TableCell>
  281. </TableRow>
  282. </TableHead>
  283. <TableBody>
  284. {jobOrders.map((jo) => (
  285. <TableRow
  286. key={`${rowKey}-jo-${jo.jobOrderId}`}
  287. >
  288. <TableCell>
  289. {jo.jobOrderId > 0 ? (
  290. <MuiLink
  291. component={NextLink}
  292. href={`/jo/edit?id=${jo.jobOrderId}`}
  293. underline="hover"
  294. >
  295. {jo.jobOrderCode ||
  296. `JO-${jo.jobOrderId}`}
  297. </MuiLink>
  298. ) : (
  299. jo.jobOrderCode || "-"
  300. )}
  301. </TableCell>
  302. <TableCell>
  303. {formatProductionDate(
  304. jo.productionDate,
  305. )}
  306. </TableCell>
  307. <TableCell align="right">
  308. {formatQty(jo.reqQty)}
  309. </TableCell>
  310. <TableCell align="right">
  311. {formatQty(jo.productionQty)}
  312. </TableCell>
  313. </TableRow>
  314. ))}
  315. </TableBody>
  316. </Table>
  317. </Box>
  318. </Collapse>
  319. </TableCell>
  320. </TableRow>
  321. )}
  322. </React.Fragment>
  323. );
  324. })
  325. )}
  326. </TableBody>
  327. </Table>
  328. </TableContainer>
  329. )}
  330. </CardContent>
  331. </Card>
  332. );
  333. };
  334. export default DrinkProductionQtyDashboard;