FPSMS-frontend
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 

287 satır
10 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. // Sort rows by supplier code alphabetically (A -> Z)
  61. const sortedData = useMemo(() => {
  62. return [...data].sort((a, b) => {
  63. const codeA = (a.supplierCode || '').toUpperCase();
  64. const codeB = (b.supplierCode || '').toUpperCase();
  65. if (codeA < codeB) return -1;
  66. if (codeA > codeB) return 1;
  67. return 0;
  68. });
  69. }, [data]);
  70. const totalStatistics = useMemo(() => {
  71. // Overall statistics should count ALL POs, including those hidden from the table
  72. const totalReceived = sortedData.reduce((sum, row) => sum + (row.noOfOrdersReceivedAtDock || 0), 0);
  73. const totalExpected = sortedData.reduce((sum, row) => sum + (row.expectedNoOfDelivery || 0), 0);
  74. return { received: totalReceived, expected: totalExpected };
  75. }, [sortedData]);
  76. type StatusKey = 'pending' | 'receiving' | 'accepted';
  77. const getStatusKey = useCallback((row: GoodsReceiptStatusRow): StatusKey => {
  78. // Only when the whole PO is processed (all items finished IQC and PO completed)
  79. // should we treat it as "accepted" (已收貨).
  80. if (row.noOfOrdersReceivedAtDock === 1) {
  81. return 'accepted';
  82. }
  83. // If some items have been inspected or put away but the order is not fully processed,
  84. // treat as "receiving" / "processing".
  85. if ((row.noOfItemsInspected ?? 0) > 0 || (row.noOfItemsCompletedPutAwayAtStore ?? 0) > 0) {
  86. return 'receiving';
  87. }
  88. // Otherwise, nothing has started yet -> "pending".
  89. return 'pending';
  90. }, []);
  91. const renderStatusChip = useCallback((row: GoodsReceiptStatusRow) => {
  92. const statusKey = getStatusKey(row);
  93. const label = t(statusKey);
  94. // Color mapping: pending -> red, receiving -> yellow, accepted -> default/green-ish
  95. const color =
  96. statusKey === 'pending'
  97. ? 'error'
  98. : statusKey === 'receiving'
  99. ? 'warning'
  100. : 'success';
  101. return (
  102. <Chip
  103. label={label}
  104. color={color}
  105. size="small"
  106. sx={{
  107. minWidth: 64,
  108. fontWeight: 500,
  109. ...(statusKey === 'pending'
  110. ? {
  111. bgcolor: 'error.light',
  112. color: 'common.white',
  113. }
  114. : {}),
  115. }}
  116. />
  117. );
  118. }, [getStatusKey, t]);
  119. if (screenCleared) {
  120. return (
  121. <Card sx={{ mb: 2 }}>
  122. <CardContent>
  123. <Stack direction="row" spacing={2} justifyContent="space-between" alignItems="center">
  124. <Typography variant="body2" color="text.secondary">
  125. {t("Screen cleared")}
  126. </Typography>
  127. <Button variant="contained" onClick={() => setScreenCleared(false)}>
  128. {t("Restore Screen")}
  129. </Button>
  130. </Stack>
  131. </CardContent>
  132. </Card>
  133. );
  134. }
  135. return (
  136. <Card sx={{ mb: 2 }}>
  137. <CardContent>
  138. {/* Header */}
  139. <Stack direction="row" spacing={2} sx={{ mb: 2 }} alignItems="center" flexWrap="wrap">
  140. <Stack direction="row" spacing={1} alignItems="center">
  141. <Typography variant="body2" sx={{ fontWeight: 600 }}>
  142. {t("Date")}:
  143. </Typography>
  144. <LocalizationProvider dateAdapter={AdapterDayjs}>
  145. <DatePicker
  146. value={selectedDate}
  147. onChange={(value) => {
  148. if (!value) return;
  149. setSelectedDate(value);
  150. }}
  151. slotProps={{
  152. textField: {
  153. size: "small",
  154. sx: { minWidth: 160 }
  155. }
  156. }}
  157. />
  158. </LocalizationProvider>
  159. <Typography variant="body2" sx={{ ml: 1 }}>
  160. 訂單已處理: {totalStatistics.received}/{totalStatistics.expected}
  161. </Typography>
  162. </Stack>
  163. <Box sx={{ flexGrow: 1 }} />
  164. <Typography variant="body2" sx={{ color: 'text.secondary' }}>
  165. {t("Auto-refresh every 15 minutes")} | {t("Last updated")}: {lastUpdated ? lastUpdated.format('HH:mm:ss') : '--:--:--'}
  166. </Typography>
  167. <Button variant="outlined" color="inherit" onClick={() => setScreenCleared(true)}>
  168. {t("Exit Screen")}
  169. </Button>
  170. </Stack>
  171. {/* Table */}
  172. <Box sx={{ mt: 2 }}>
  173. {loading ? (
  174. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  175. <CircularProgress />
  176. </Box>
  177. ) : (
  178. <TableContainer component={Paper}>
  179. <Table
  180. size="small"
  181. sx={{
  182. minWidth: 560,
  183. tableLayout: 'fixed',
  184. }}
  185. >
  186. <TableHead>
  187. <TableRow sx={{ backgroundColor: 'grey.100' }}>
  188. <TableCell sx={{ fontWeight: 600, width: '32%', padding: '4px 6px' }}>{t("Supplier")}</TableCell>
  189. <TableCell sx={{ fontWeight: 600, width: '30%', padding: '4px 6px' }}>{t("Purchase Order Code")}</TableCell>
  190. <TableCell sx={{ fontWeight: 600, width: '14%', padding: '4px 4px' }}>{t("Status")}</TableCell>
  191. <TableCell sx={{ fontWeight: 600, width: '24%', padding: '4px 4px' }} align="right">{t("No. of Items with IQC Issue")}</TableCell>
  192. </TableRow>
  193. </TableHead>
  194. <TableBody>
  195. {sortedData.length === 0 ? (
  196. <TableRow>
  197. <TableCell colSpan={4} align="center">
  198. <Typography variant="body2" color="text.secondary">
  199. {t("No data available")} ({selectedDateLabel})
  200. </Typography>
  201. </TableCell>
  202. </TableRow>
  203. ) : (
  204. sortedData
  205. .filter((row) => !row.hideFromDashboard) // hide completed/rejected POs from table only
  206. .map((row, index) => (
  207. <TableRow
  208. key={`${row.supplierId ?? 'na'}-${index}`}
  209. sx={{
  210. '&:hover': { backgroundColor: 'grey.50' }
  211. }}
  212. >
  213. <TableCell sx={{ padding: '4px 6px' }}>
  214. <Box sx={{ display: 'flex', alignItems: 'center' }}>
  215. <Typography
  216. component="span"
  217. variant="body2"
  218. sx={{
  219. fontWeight: 500,
  220. minWidth: '60px'
  221. }}
  222. >
  223. {row.supplierCode || '-'}
  224. </Typography>
  225. <Typography
  226. component="span"
  227. variant="body2"
  228. sx={{ color: 'text.secondary',ml: 0.5, mr: 1 }}
  229. >
  230. -
  231. </Typography>
  232. <Typography
  233. component="span"
  234. variant="body2"
  235. >
  236. {row.supplierName || '-'}
  237. </Typography>
  238. </Box>
  239. </TableCell>
  240. <TableCell sx={{ padding: '4px 6px' }}>
  241. {row.purchaseOrderCode || '-'}
  242. </TableCell>
  243. <TableCell sx={{ padding: '4px 4px' }}>
  244. {renderStatusChip(row)}
  245. </TableCell>
  246. <TableCell sx={{ padding: '4px 6px' }} align="right">
  247. {row.noOfItemsWithIqcIssue ?? 0}
  248. </TableCell>
  249. </TableRow>
  250. ))
  251. )}
  252. </TableBody>
  253. </Table>
  254. </TableContainer>
  255. )}
  256. </Box>
  257. </CardContent>
  258. </Card>
  259. );
  260. };
  261. export default GoodsReceiptStatusNew;
  262. 4