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

286 строки
10 KiB

  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Card,
  6. CardContent,
  7. CardActions,
  8. Stack,
  9. Typography,
  10. Chip,
  11. CircularProgress,
  12. TablePagination,
  13. Grid,
  14. LinearProgress,
  15. } from "@mui/material";
  16. import { useState, useCallback, useEffect } from "react";
  17. import { useTranslation } from "react-i18next";
  18. import duration from "dayjs/plugin/duration";
  19. import {
  20. getStockTakeRecords,
  21. AllPickedStockTakeListReponse,
  22. createStockTakeForSections,
  23. } from "@/app/api/stockTake/actions";
  24. import dayjs from "dayjs";
  25. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  26. const PER_PAGE = 6;
  27. interface PickerCardListProps {
  28. onCardClick: (session: AllPickedStockTakeListReponse) => void;
  29. onReStockTakeClick: (session: AllPickedStockTakeListReponse) => void;
  30. }
  31. const PickerCardList: React.FC<PickerCardListProps> = ({ onCardClick, onReStockTakeClick }) => {
  32. const { t } = useTranslation(["inventory", "common"]);
  33. dayjs.extend(duration);
  34. const PER_PAGE = 6;
  35. const [loading, setLoading] = useState(false);
  36. const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]);
  37. const [page, setPage] = useState(0);
  38. const [creating, setCreating] = useState(false);
  39. const fetchStockTakeSessions = useCallback(async () => {
  40. setLoading(true);
  41. try {
  42. const data = await getStockTakeRecords();
  43. setStockTakeSessions(Array.isArray(data) ? data : []);
  44. setPage(0);
  45. } catch (e) {
  46. console.error(e);
  47. setStockTakeSessions([]);
  48. } finally {
  49. setLoading(false);
  50. }
  51. }, []);
  52. useEffect(() => {
  53. fetchStockTakeSessions();
  54. }, [fetchStockTakeSessions]);
  55. const startIdx = page * PER_PAGE;
  56. const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE);
  57. const handleCreateStockTake = useCallback(async () => {
  58. setCreating(true);
  59. try {
  60. const result = await createStockTakeForSections();
  61. const createdCount = Object.values(result).filter(msg => msg.startsWith("Created:")).length;
  62. const skippedCount = Object.values(result).filter(msg => msg.startsWith("Skipped:")).length;
  63. const errorCount = Object.values(result).filter(msg => msg.startsWith("Error:")).length;
  64. let message = `${t("Created")}: ${createdCount}, ${t("Skipped")}: ${skippedCount}`;
  65. if (errorCount > 0) {
  66. message += `, ${t("Errors")}: ${errorCount}`;
  67. }
  68. console.log(message);
  69. await fetchStockTakeSessions();
  70. } catch (e) {
  71. console.error(e);
  72. } finally {
  73. setCreating(false);
  74. }
  75. }, [fetchStockTakeSessions, t]);
  76. const getStatusColor = (status: string) => {
  77. const statusLower = status.toLowerCase();
  78. if (statusLower === "completed") return "success";
  79. if (statusLower === "in_progress" || statusLower === "processing") return "primary";
  80. if (statusLower === "approving") return "info";
  81. if (statusLower === "stockTaking") return "primary";
  82. if (statusLower === "no_cycle") return "default";
  83. return "warning";
  84. };
  85. const TimeDisplay: React.FC<{ startTime: string | null; endTime: string | null }> = ({ startTime, endTime }) => {
  86. const [currentTime, setCurrentTime] = useState(dayjs());
  87. useEffect(() => {
  88. if (!endTime && startTime) {
  89. const interval = setInterval(() => {
  90. setCurrentTime(dayjs());
  91. }, 1000); // 每秒更新一次
  92. return () => clearInterval(interval);
  93. }
  94. }, [startTime, endTime]);
  95. if (endTime && startTime) {
  96. // 当有结束时间时,计算从开始到结束的持续时间
  97. const start = dayjs(startTime);
  98. const end = dayjs(endTime);
  99. const duration = dayjs.duration(end.diff(start));
  100. const hours = Math.floor(duration.asHours());
  101. const minutes = duration.minutes();
  102. const seconds = duration.seconds();
  103. return (
  104. <>
  105. {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
  106. </>
  107. );
  108. } else if (startTime) {
  109. // 当没有结束时间时,显示实时计时器
  110. const start = dayjs(startTime);
  111. const duration = dayjs.duration(currentTime.diff(start));
  112. const hours = Math.floor(duration.asHours());
  113. const minutes = duration.minutes();
  114. const seconds = duration.seconds();
  115. return (
  116. <>
  117. {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
  118. </>
  119. );
  120. } else {
  121. return <>-</>;
  122. }
  123. };
  124. const startTimeDisplay = (startTime: string | null) => {
  125. if (startTime) {
  126. const start = dayjs(startTime);
  127. return start.format("HH:mm");
  128. } else {
  129. return "-";
  130. }
  131. };
  132. const endTimeDisplay = (endTime: string | null) => {
  133. if (endTime) {
  134. const end = dayjs(endTime);
  135. return end.format("HH:mm");
  136. } else {
  137. return "-";
  138. }
  139. };
  140. const getCompletionRate = (session: AllPickedStockTakeListReponse): number => {
  141. if (session.totalInventoryLotNumber === 0) return 0;
  142. return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100);
  143. };
  144. if (loading) {
  145. return (
  146. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  147. <CircularProgress />
  148. </Box>
  149. );
  150. }
  151. return (
  152. <Box>
  153. <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
  154. <Typography variant="body2" color="text.secondary">
  155. {t("Total Sections")}: {stockTakeSessions.length}
  156. </Typography>
  157. <Button
  158. variant="contained"
  159. color="primary"
  160. onClick={handleCreateStockTake}
  161. disabled={creating}
  162. >
  163. {creating ? <CircularProgress size={20} /> : t("Create Stock Take for All Sections")}
  164. </Button>
  165. </Box>
  166. <Grid container spacing={2}>
  167. {paged.map((session) => {
  168. const statusColor = getStatusColor(session.status || "");
  169. const lastStockTakeDate = session.lastStockTakeDate
  170. ? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT)
  171. : "-";
  172. const completionRate = getCompletionRate(session);
  173. return (
  174. <Grid key={session.id} item xs={12} sm={6} md={4}>
  175. <Card
  176. sx={{
  177. minHeight: 200,
  178. display: "flex",
  179. flexDirection: "column",
  180. border: "1px solid",
  181. borderColor: statusColor === "success" ? "success.main" : "primary.main",
  182. }}
  183. >
  184. <CardContent sx={{ pb: 1, flexGrow: 1 }}>
  185. <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
  186. <Typography variant="subtitle1" fontWeight={600}>
  187. {t("Section")}: {session.stockTakeSession}
  188. </Typography>
  189. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  190. {t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
  191. </Typography>
  192. </Stack>
  193. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName}</Typography>
  194. <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
  195. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("start time")}: {startTimeDisplay(session.startTime) || "-"}</Typography>
  196. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("end time")}: {endTimeDisplay(session.endTime) || "-"}</Typography>
  197. </Stack>
  198. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
  199. {t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} />
  200. </Typography>
  201. <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Total Item Number")}: {session.totalItemNumber}</Typography>
  202. {session.totalInventoryLotNumber > 0 && (
  203. <Box sx={{ mt: 2 }}>
  204. <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.5 }}>
  205. <Typography variant="body2" fontWeight={600}>
  206. {t("Progress")}
  207. </Typography>
  208. <Typography variant="body2" fontWeight={600}>
  209. {completionRate}%
  210. </Typography>
  211. </Stack>
  212. <LinearProgress
  213. variant="determinate"
  214. value={completionRate}
  215. sx={{ height: 8, borderRadius: 1 }}
  216. />
  217. </Box>
  218. )}
  219. </CardContent>
  220. <CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}>
  221. <Stack direction="row" spacing={1}>
  222. <Button
  223. variant="contained"
  224. size="small"
  225. onClick={() => onCardClick(session)}
  226. >
  227. {t("View Details")}
  228. </Button>
  229. <Button
  230. variant="contained"
  231. size="small"
  232. onClick={() => onReStockTakeClick(session)}
  233. disabled={!session.reStockTakeTrueFalse}
  234. >
  235. {t("View ReStockTake")}
  236. </Button>
  237. </Stack>
  238. <Chip size="small" label={t(session.status || "")} color={statusColor as any} />
  239. </CardActions>
  240. </Card>
  241. </Grid>
  242. );
  243. })}
  244. </Grid>
  245. {stockTakeSessions.length > 0 && (
  246. <TablePagination
  247. component="div"
  248. count={stockTakeSessions.length}
  249. page={page}
  250. rowsPerPage={PER_PAGE}
  251. onPageChange={(e, p) => setPage(p)}
  252. rowsPerPageOptions={[PER_PAGE]}
  253. />
  254. )}
  255. </Box>
  256. );
  257. };
  258. export default PickerCardList;