FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 

407 строки
15 KiB

  1. "use client";
  2. import React, { useState, useEffect, useCallback, useMemo, 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. TablePagination,
  17. Stack
  18. } from '@mui/material';
  19. import { useTranslation } from 'react-i18next';
  20. import dayjs from 'dayjs';
  21. import { arrayToDayjs } from '@/app/utils/formatUtil';
  22. import { fetchMaterialPickStatus, MaterialPickStatusItem } from '@/app/api/jo/actions';
  23. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  24. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  25. const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes in milliseconds
  26. const MaterialPickStatusTable: React.FC = () => {
  27. const { t } = useTranslation("jo");
  28. const [data, setData] = useState<MaterialPickStatusItem[]>([]);
  29. const [loading, setLoading] = useState<boolean>(true);
  30. const refreshCountRef = useRef<number>(0);
  31. const [selectedDate, setSelectedDate] = useState(dayjs().format("YYYY-MM-DD"));
  32. const [paginationController, setPaginationController] = useState({
  33. pageNum: 0,
  34. pageSize: 10,
  35. });
  36. const [now, setNow] = useState(dayjs());
  37. const [lastDataRefreshTime, setLastDataRefreshTime] = useState<dayjs.Dayjs | null>(null);
  38. const loadData = useCallback(async () => {
  39. setLoading(true);
  40. try {
  41. const result = await fetchMaterialPickStatus(selectedDate);
  42. if (refreshCountRef.current >= 1) {
  43. setData(result);
  44. } else {
  45. setData(result || []);
  46. }
  47. refreshCountRef.current += 1;
  48. setLastDataRefreshTime(dayjs());
  49. } catch (error) {
  50. console.error('Error fetching material pick status:', error);
  51. setData([]);
  52. } finally {
  53. setLoading(false);
  54. }
  55. }, [selectedDate]);
  56. useEffect(() => {
  57. loadData();
  58. const interval = setInterval(() => {
  59. loadData();
  60. }, REFRESH_INTERVAL);
  61. return () => clearInterval(interval);
  62. }, [loadData]);
  63. useEffect(() => {
  64. const timer = setInterval(() => setNow(dayjs()), 60 * 1000);
  65. return () => clearInterval(timer);
  66. }, []);
  67. const formatTime = (timeData: any): string => {
  68. if (!timeData) return '';
  69. // Handle LocalDateTime ISO string format (e.g., "2026-01-09T18:01:54")
  70. if (typeof timeData === 'string') {
  71. // Try parsing as ISO string first (most common format from LocalDateTime)
  72. const parsed = dayjs(timeData);
  73. if (parsed.isValid()) {
  74. return parsed.format('HH:mm');
  75. }
  76. // Try parsing as custom format YYYYMMDDHHmmss
  77. const customParsed = dayjs(timeData, 'YYYYMMDDHHmmss');
  78. if (customParsed.isValid()) {
  79. return customParsed.format('HH:mm');
  80. }
  81. // Try parsing as time string (HH:mm or HH:mm:ss)
  82. const parts = timeData.split(':');
  83. if (parts.length >= 2) {
  84. const hour = parseInt(parts[0], 10);
  85. const minute = parseInt(parts[1] || '0', 10);
  86. if (!isNaN(hour) && !isNaN(minute)) {
  87. return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
  88. }
  89. }
  90. } else if (Array.isArray(timeData)) {
  91. // Handle array format [year, month, day, hour, minute, second]
  92. const hour = timeData[3] ?? timeData[0] ?? 0;
  93. const minute = timeData[4] ?? timeData[1] ?? 0;
  94. return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
  95. }
  96. return '';
  97. };
  98. const calculatePickTime = (startTime: any, endTime: any): number => {
  99. if (!startTime || !endTime) return 0;
  100. let start: dayjs.Dayjs;
  101. let end: dayjs.Dayjs;
  102. // Parse start time
  103. if (Array.isArray(startTime)) {
  104. // Array format: [year, month, day, hour, minute, second]
  105. if (startTime.length >= 5) {
  106. const year = startTime[0] || 0;
  107. const month = (startTime[1] || 1) - 1; // month is 0-indexed in JS Date
  108. const day = startTime[2] || 1;
  109. const hour = startTime[3] || 0;
  110. const minute = startTime[4] || 0;
  111. const second = startTime[5] || 0;
  112. // Create Date object and convert to dayjs
  113. const date = new Date(year, month, day, hour, minute, second);
  114. start = dayjs(date);
  115. console.log('Parsed start time:', {
  116. array: startTime,
  117. date: date.toISOString(),
  118. dayjs: start.format('YYYY-MM-DD HH:mm:ss'),
  119. isValid: start.isValid()
  120. });
  121. } else {
  122. // Fallback to arrayToDayjs for shorter arrays
  123. start = arrayToDayjs(startTime, true);
  124. }
  125. } else if (typeof startTime === 'string') {
  126. // Try ISO format first
  127. start = dayjs(startTime);
  128. if (!start.isValid()) {
  129. // Try custom format
  130. start = dayjs(startTime, 'YYYYMMDDHHmmss');
  131. }
  132. } else {
  133. start = dayjs(startTime);
  134. }
  135. // Parse end time
  136. if (Array.isArray(endTime)) {
  137. // Array format: [year, month, day, hour, minute, second]
  138. if (endTime.length >= 5) {
  139. const year = endTime[0] || 0;
  140. const month = (endTime[1] || 1) - 1; // month is 0-indexed in JS Date
  141. const day = endTime[2] || 1;
  142. const hour = endTime[3] || 0;
  143. const minute = endTime[4] || 0;
  144. const second = endTime[5] || 0;
  145. // Create Date object and convert to dayjs
  146. const date = new Date(year, month, day, hour, minute, second);
  147. end = dayjs(date);
  148. console.log('Parsed end time:', {
  149. array: endTime,
  150. date: date.toISOString(),
  151. dayjs: end.format('YYYY-MM-DD HH:mm:ss'),
  152. isValid: end.isValid()
  153. });
  154. } else {
  155. // Fallback to arrayToDayjs for shorter arrays
  156. end = arrayToDayjs(endTime, true);
  157. }
  158. } else if (typeof endTime === 'string') {
  159. // Try ISO format first
  160. end = dayjs(endTime);
  161. if (!end.isValid()) {
  162. // Try custom format
  163. end = dayjs(endTime, 'YYYYMMDDHHmmss');
  164. }
  165. } else {
  166. end = dayjs(endTime);
  167. }
  168. if (!start.isValid() || !end.isValid()) {
  169. console.warn('Invalid time values:', {
  170. startTime,
  171. endTime,
  172. startValid: start.isValid(),
  173. endValid: end.isValid(),
  174. startFormat: start.isValid() ? start.format() : 'invalid',
  175. endFormat: end.isValid() ? end.format() : 'invalid'
  176. });
  177. return 0;
  178. }
  179. // Calculate difference in seconds first, then convert to minutes
  180. // This handles sub-minute differences correctly
  181. const diffSeconds = end.diff(start, 'second');
  182. const diffMinutes = Math.ceil(diffSeconds / 60); // Round up to nearest minute
  183. console.log('Time calculation:', {
  184. start: start.format('YYYY-MM-DD HH:mm:ss'),
  185. end: end.format('YYYY-MM-DD HH:mm:ss'),
  186. diffSeconds,
  187. diffMinutes
  188. });
  189. return diffMinutes > 0 ? diffMinutes : 0;
  190. };
  191. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  192. setPaginationController(prev => ({
  193. ...prev,
  194. pageNum: newPage,
  195. }));
  196. }, []);
  197. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  198. const newPageSize = parseInt(event.target.value, 10);
  199. setPaginationController({
  200. pageNum: 0,
  201. pageSize: newPageSize,
  202. });
  203. }, []);
  204. const paginatedData = useMemo(() => {
  205. const startIndex = paginationController.pageNum * paginationController.pageSize;
  206. const endIndex = startIndex + paginationController.pageSize;
  207. return data.slice(startIndex, endIndex);
  208. }, [data, paginationController]);
  209. return (
  210. <Card sx={{ mb: 2 }}>
  211. <CardContent>
  212. {/* Title */}
  213. <Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}>
  214. {t("Material Pick Status")}
  215. </Typography>
  216. {/* Filters */}
  217. <Stack direction="row" spacing={2} sx={{ mb: 3 }}>
  218. <LocalizationProvider dateAdapter={AdapterDayjs}>
  219. <DatePicker
  220. label={t("Date")}
  221. value={dayjs(selectedDate)}
  222. onChange={(newValue) => {
  223. setSelectedDate(newValue ? newValue.format("YYYY-MM-DD") : selectedDate);
  224. }}
  225. format="YYYY-MM-DD"
  226. slotProps={{
  227. textField: { size: "small", sx: { minWidth: 160 } },
  228. }}
  229. />
  230. </LocalizationProvider>
  231. <Box sx={{ flexGrow: 1 }} />
  232. <Stack direction="row" spacing={2} sx={{ alignSelf: 'center' }}>
  233. <Typography variant="body2" sx={{ color: 'text.secondary' }}>
  234. {t("Now")}: {now.format('HH:mm')}
  235. </Typography>
  236. <Typography variant="body2" sx={{ color: 'text.secondary' }}>
  237. {t("Auto-refresh every 10 minutes")} | {t("Last updated")}: {lastDataRefreshTime ? lastDataRefreshTime.format('HH:mm:ss') : '--:--:--'}
  238. </Typography>
  239. </Stack>
  240. </Stack>
  241. <Box sx={{ mt: 2 }}>
  242. {loading ? (
  243. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  244. <CircularProgress />
  245. </Box>
  246. ) : (
  247. <>
  248. <TableContainer component={Paper} sx={{ maxHeight: 440, overflow: 'auto' }}>
  249. <Table size="small" sx={{ minWidth: 650 }}>
  250. <TableHead sx={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'grey.100' }}>
  251. <TableRow>
  252. <TableCell>
  253. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  254. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  255. {t("Pick Order No.- Job Order No.- Item")}
  256. </Typography>
  257. </Box>
  258. </TableCell>
  259. <TableCell align="right">
  260. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  261. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  262. {t("Job Order Qty")}
  263. </Typography>
  264. </Box>
  265. </TableCell>
  266. <TableCell align="right">
  267. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  268. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  269. {t("No. of Items to be Picked")}
  270. </Typography>
  271. </Box>
  272. </TableCell>
  273. <TableCell align="right">
  274. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  275. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  276. {t("No. of Items with Issue During Pick")}
  277. </Typography>
  278. </Box>
  279. </TableCell>
  280. <TableCell>
  281. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  282. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  283. {t("Pick Start Time")}
  284. </Typography>
  285. </Box>
  286. </TableCell>
  287. <TableCell>
  288. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  289. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  290. {t("Pick End Time")}
  291. </Typography>
  292. </Box>
  293. </TableCell>
  294. <TableCell align="right" sx={{
  295. }}>
  296. <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
  297. <Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
  298. {t("Pick Time Taken (minutes)")}
  299. </Typography>
  300. </Box>
  301. </TableCell>
  302. </TableRow>
  303. </TableHead>
  304. <TableBody>
  305. {paginatedData.length === 0 ? (
  306. <TableRow>
  307. <TableCell colSpan={9} align="center">
  308. {t("No data available")}
  309. </TableCell>
  310. </TableRow>
  311. ) : (
  312. paginatedData.map((row) => {
  313. const pickTimeTaken = calculatePickTime(row.pickStartTime, row.pickEndTime);
  314. return (
  315. <TableRow key={row.id}>
  316. <TableCell>
  317. <Box> {row.pickOrderCode || '-'}</Box>
  318. <br />
  319. <Box>{row.jobOrderCode || '-'}</Box>
  320. <br />
  321. <Box>{row.itemCode || '-'} {row.itemName || '-'}</Box>
  322. </TableCell>
  323. <TableCell align="right">
  324. {row.jobOrderQty !== null && row.jobOrderQty !== undefined
  325. ? `${row.jobOrderQty} ${row.uom || ''}`
  326. : '-'}
  327. </TableCell>
  328. <TableCell align="right">{row.numberOfItemsToPick ?? 0}</TableCell>
  329. <TableCell align="right">{row.numberOfItemsWithIssue ?? 0}</TableCell>
  330. <TableCell>{formatTime(row.pickStartTime) || '-'}</TableCell>
  331. <TableCell>{formatTime(row.pickEndTime) || '-'}</TableCell>
  332. <TableCell align="right" sx={{
  333. backgroundColor: 'rgba(76, 175, 80, 0.1)',
  334. fontWeight: 600
  335. }}>
  336. {pickTimeTaken > 0 ? `${pickTimeTaken} ${t("minutes")}` : '-'}
  337. </TableCell>
  338. </TableRow>
  339. );
  340. })
  341. )}
  342. </TableBody>
  343. </Table>
  344. </TableContainer>
  345. {data.length > 0 && (
  346. <TablePagination
  347. component="div"
  348. count={data.length}
  349. page={paginationController.pageNum}
  350. rowsPerPage={paginationController.pageSize}
  351. onPageChange={handlePageChange}
  352. onRowsPerPageChange={handlePageSizeChange}
  353. rowsPerPageOptions={[5, 10, 15, 25]}
  354. labelRowsPerPage={t("Rows per page")}
  355. />
  356. )}
  357. </>
  358. )}
  359. </Box>
  360. </CardContent>
  361. </Card>
  362. );
  363. };
  364. export default MaterialPickStatusTable;