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

276 строки
9.6 KiB

  1. "use client";
  2. import React, { useState, useEffect, useCallback, useMemo } from 'react';
  3. import {
  4. Box,
  5. Typography,
  6. Card,
  7. CardContent,
  8. Stack,
  9. Table,
  10. TableBody,
  11. TableCell,
  12. TableContainer,
  13. TableHead,
  14. TableRow,
  15. Paper,
  16. CircularProgress,
  17. Button,
  18. Chip
  19. } from '@mui/material';
  20. import { useTranslation } from 'react-i18next';
  21. import dayjs from 'dayjs';
  22. import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
  23. import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
  24. import { DatePicker } from '@mui/x-date-pickers/DatePicker';
  25. import { fetchGoodsReceiptStatusClient, type GoodsReceiptStatusRow } from '@/app/api/dashboard/client';
  26. const REFRESH_MS = 15 * 60 * 1000;
  27. const GoodsReceiptStatusNew: React.FC = () => {
  28. const { t } = useTranslation("dashboard");
  29. const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs());
  30. const [data, setData] = useState<GoodsReceiptStatusRow[]>([]);
  31. const [loading, setLoading] = useState<boolean>(true);
  32. const [lastUpdated, setLastUpdated] = useState<dayjs.Dayjs | null>(null);
  33. const [screenCleared, setScreenCleared] = useState<boolean>(false);
  34. const loadData = useCallback(async () => {
  35. if (screenCleared) return;
  36. try {
  37. setLoading(true);
  38. const dateParam = selectedDate.format('YYYY-MM-DD');
  39. const result = await fetchGoodsReceiptStatusClient(dateParam);
  40. setData(result ?? []);
  41. setLastUpdated(dayjs());
  42. } catch (error) {
  43. console.error('Error fetching goods receipt status:', error);
  44. setData([]);
  45. } finally {
  46. setLoading(false);
  47. }
  48. }, [selectedDate, screenCleared]);
  49. useEffect(() => {
  50. if (screenCleared) return;
  51. loadData();
  52. const refreshInterval = setInterval(() => {
  53. loadData();
  54. }, REFRESH_MS);
  55. return () => clearInterval(refreshInterval);
  56. }, [loadData, screenCleared]);
  57. const selectedDateLabel = useMemo(() => {
  58. return selectedDate.format('YYYY-MM-DD');
  59. }, [selectedDate]);
  60. const totalStatistics = useMemo(() => {
  61. // Overall statistics should count ALL POs, including those hidden from the table
  62. const totalReceived = data.reduce((sum, row) => sum + (row.noOfOrdersReceivedAtDock || 0), 0);
  63. const totalExpected = data.reduce((sum, row) => sum + (row.expectedNoOfDelivery || 0), 0);
  64. return { received: totalReceived, expected: totalExpected };
  65. }, [data]);
  66. type StatusKey = 'pending' | 'receiving' | 'accepted';
  67. const getStatusKey = useCallback((row: GoodsReceiptStatusRow): StatusKey => {
  68. // Only when the whole PO is processed (all items finished IQC and PO completed)
  69. // should we treat it as "accepted" (已收貨).
  70. if (row.noOfOrdersReceivedAtDock === 1) {
  71. return 'accepted';
  72. }
  73. // If some items have been inspected or put away but the order is not fully processed,
  74. // treat as "receiving" / "processing".
  75. if ((row.noOfItemsInspected ?? 0) > 0 || (row.noOfItemsCompletedPutAwayAtStore ?? 0) > 0) {
  76. return 'receiving';
  77. }
  78. // Otherwise, nothing has started yet -> "pending".
  79. return 'pending';
  80. }, []);
  81. const renderStatusChip = useCallback((row: GoodsReceiptStatusRow) => {
  82. const statusKey = getStatusKey(row);
  83. const label = t(statusKey);
  84. // Color mapping: pending -> red, receiving -> yellow, accepted -> default/green-ish
  85. const color =
  86. statusKey === 'pending'
  87. ? 'error'
  88. : statusKey === 'receiving'
  89. ? 'warning'
  90. : 'success';
  91. return (
  92. <Chip
  93. label={label}
  94. color={color}
  95. size="small"
  96. sx={{
  97. minWidth: 64,
  98. fontWeight: 500,
  99. ...(statusKey === 'pending'
  100. ? {
  101. bgcolor: 'error.light',
  102. color: 'common.white',
  103. }
  104. : {}),
  105. }}
  106. />
  107. );
  108. }, [getStatusKey, t]);
  109. if (screenCleared) {
  110. return (
  111. <Card sx={{ mb: 2 }}>
  112. <CardContent>
  113. <Stack direction="row" spacing={2} justifyContent="space-between" alignItems="center">
  114. <Typography variant="body2" color="text.secondary">
  115. {t("Screen cleared")}
  116. </Typography>
  117. <Button variant="contained" onClick={() => setScreenCleared(false)}>
  118. {t("Restore Screen")}
  119. </Button>
  120. </Stack>
  121. </CardContent>
  122. </Card>
  123. );
  124. }
  125. return (
  126. <Card sx={{ mb: 2 }}>
  127. <CardContent>
  128. {/* Header */}
  129. <Stack direction="row" spacing={2} sx={{ mb: 2 }} alignItems="center" flexWrap="wrap">
  130. <Stack direction="row" spacing={1} alignItems="center">
  131. <Typography variant="body2" sx={{ fontWeight: 600 }}>
  132. {t("Date")}:
  133. </Typography>
  134. <LocalizationProvider dateAdapter={AdapterDayjs}>
  135. <DatePicker
  136. value={selectedDate}
  137. onChange={(value) => {
  138. if (!value) return;
  139. setSelectedDate(value);
  140. }}
  141. slotProps={{
  142. textField: {
  143. size: "small",
  144. sx: { minWidth: 160 }
  145. }
  146. }}
  147. />
  148. </LocalizationProvider>
  149. <Typography variant="body2" sx={{ ml: 1 }}>
  150. 訂單已處理: {totalStatistics.received}/{totalStatistics.expected}
  151. </Typography>
  152. </Stack>
  153. <Box sx={{ flexGrow: 1 }} />
  154. <Typography variant="body2" sx={{ color: 'text.secondary' }}>
  155. {t("Auto-refresh every 15 minutes")} | {t("Last updated")}: {lastUpdated ? lastUpdated.format('HH:mm:ss') : '--:--:--'}
  156. </Typography>
  157. <Button variant="outlined" color="inherit" onClick={() => setScreenCleared(true)}>
  158. {t("Exit Screen")}
  159. </Button>
  160. </Stack>
  161. {/* Table */}
  162. <Box sx={{ mt: 2 }}>
  163. {loading ? (
  164. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  165. <CircularProgress />
  166. </Box>
  167. ) : (
  168. <TableContainer component={Paper}>
  169. <Table
  170. size="small"
  171. sx={{
  172. minWidth: 560,
  173. tableLayout: 'fixed',
  174. }}
  175. >
  176. <TableHead>
  177. <TableRow sx={{ backgroundColor: 'grey.100' }}>
  178. <TableCell sx={{ fontWeight: 600, width: '32%', padding: '4px 6px' }}>{t("Supplier")}</TableCell>
  179. <TableCell sx={{ fontWeight: 600, width: '30%', padding: '4px 6px' }}>{t("Purchase Order Code")}</TableCell>
  180. <TableCell sx={{ fontWeight: 600, width: '14%', padding: '4px 4px' }}>{t("Status")}</TableCell>
  181. <TableCell sx={{ fontWeight: 600, width: '24%', padding: '4px 4px' }} align="right">{t("No. of Items with IQC Issue")}</TableCell>
  182. </TableRow>
  183. </TableHead>
  184. <TableBody>
  185. {data.length === 0 ? (
  186. <TableRow>
  187. <TableCell colSpan={4} align="center">
  188. <Typography variant="body2" color="text.secondary">
  189. {t("No data available")} ({selectedDateLabel})
  190. </Typography>
  191. </TableCell>
  192. </TableRow>
  193. ) : (
  194. data
  195. .filter((row) => !row.hideFromDashboard) // hide completed/rejected POs from table only
  196. .map((row, index) => (
  197. <TableRow
  198. key={`${row.supplierId ?? 'na'}-${index}`}
  199. sx={{
  200. '&:hover': { backgroundColor: 'grey.50' }
  201. }}
  202. >
  203. <TableCell sx={{ padding: '4px 6px' }}>
  204. <Box sx={{ display: 'flex', alignItems: 'center' }}>
  205. <Typography
  206. component="span"
  207. variant="body2"
  208. sx={{
  209. fontWeight: 500,
  210. minWidth: '60px'
  211. }}
  212. >
  213. {row.supplierCode || '-'}
  214. </Typography>
  215. <Typography
  216. component="span"
  217. variant="body2"
  218. sx={{ color: 'text.secondary',ml: 0.5, mr: 1 }}
  219. >
  220. -
  221. </Typography>
  222. <Typography
  223. component="span"
  224. variant="body2"
  225. >
  226. {row.supplierName || '-'}
  227. </Typography>
  228. </Box>
  229. </TableCell>
  230. <TableCell sx={{ padding: '4px 6px' }}>
  231. {row.purchaseOrderCode || '-'}
  232. </TableCell>
  233. <TableCell sx={{ padding: '4px 4px' }}>
  234. {renderStatusChip(row)}
  235. </TableCell>
  236. <TableCell sx={{ padding: '4px 6px' }} align="right">
  237. {row.noOfItemsWithIqcIssue ?? 0}
  238. </TableCell>
  239. </TableRow>
  240. ))
  241. )}
  242. </TableBody>
  243. </Table>
  244. </TableContainer>
  245. )}
  246. </Box>
  247. </CardContent>
  248. </Card>
  249. );
  250. };
  251. export default GoodsReceiptStatusNew;
  252. 4