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

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