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.
 
 

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