FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

213 line
6.5 KiB

  1. "use client";
  2. import {
  3. Box,
  4. Card,
  5. CardContent,
  6. CardActions,
  7. Typography,
  8. CircularProgress,
  9. Grid,
  10. Chip,
  11. Button,
  12. TablePagination,
  13. } from "@mui/material";
  14. import { useCallback, useEffect, useMemo, useState } from "react";
  15. import { useTranslation } from "react-i18next";
  16. import {
  17. AllPickedStockTakeListReponse,
  18. getApproverStockTakeRecords,
  19. } from "@/app/api/stockTake/actions";
  20. import dayjs from "dayjs";
  21. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  22. const PER_PAGE = 6;
  23. interface ApproverAllCardListProps {
  24. onCardClick: (session: AllPickedStockTakeListReponse) => void;
  25. }
  26. const ApproverAllCardList: React.FC<ApproverAllCardListProps> = ({
  27. onCardClick,
  28. }) => {
  29. const { t } = useTranslation(["stockTake", "common"]);
  30. const [loading, setLoading] = useState(false);
  31. const [sessions, setSessions] = useState<AllPickedStockTakeListReponse[]>([]);
  32. const [page, setPage] = useState(0);
  33. const fetchSessions = useCallback(async () => {
  34. setLoading(true);
  35. try {
  36. const data = await getApproverStockTakeRecords();
  37. const list = Array.isArray(data) ? data : [];
  38. // 用 stockTakeRoundId 聚合輪次;舊資料 roundId 可能為 null,fallback 到 stockTakeId
  39. const roundGroups = new Map<number, AllPickedStockTakeListReponse[]>();
  40. list.forEach((s) => {
  41. const roundKey = s.stockTakeRoundId ?? s.stockTakeId;
  42. const existing = roundGroups.get(roundKey) ?? [];
  43. existing.push(s);
  44. roundGroups.set(roundKey, existing);
  45. });
  46. if (roundGroups.size === 0) {
  47. setSessions([]);
  48. setPage(0);
  49. return;
  50. }
  51. const allRounds = Array.from(roundGroups.values());
  52. const latestRoundSessions = allRounds
  53. .sort((a, b) => {
  54. const aLatest = Math.max(
  55. ...a.map((s) => dayjs(s.planStartDate ?? s.startTime ?? 0).valueOf())
  56. );
  57. const bLatest = Math.max(
  58. ...b.map((s) => dayjs(s.planStartDate ?? s.startTime ?? 0).valueOf())
  59. );
  60. return bLatest - aLatest;
  61. })[0];
  62. // 汇总这一轮的总 item / lot 数
  63. const totalItems = latestRoundSessions.reduce(
  64. (sum, s) => sum + (s.totalItemNumber || 0),
  65. 0
  66. );
  67. const totalLots = latestRoundSessions.reduce(
  68. (sum, s) => sum + (s.totalInventoryLotNumber || 0),
  69. 0
  70. );
  71. // 用这一轮里的第一条作为代表,覆盖汇总数字
  72. const representative = latestRoundSessions[0];
  73. const mergedRound: AllPickedStockTakeListReponse = {
  74. ...representative,
  75. totalItemNumber: totalItems,
  76. totalInventoryLotNumber: totalLots,
  77. };
  78. // UI 上只展示这一轮一张卡
  79. setSessions([mergedRound]);
  80. setPage(0);
  81. } catch (e) {
  82. console.error(e);
  83. setSessions([]);
  84. } finally {
  85. setLoading(false);
  86. }
  87. }, []);
  88. useEffect(() => {
  89. fetchSessions();
  90. }, [fetchSessions]);
  91. const getStatusColor = (status: string | null) => {
  92. if (!status) return "default";
  93. const statusLower = status.toLowerCase().replace(/_/g, "");
  94. if (statusLower === "completed") return "success";
  95. if (statusLower === "stocktaking" || statusLower === "processing") return "primary";
  96. if (statusLower === "approving") return "info";
  97. if (statusLower === "pending") return "warning";
  98. return "default";
  99. };
  100. const paged = useMemo(() => {
  101. const startIdx = page * PER_PAGE;
  102. return sessions.slice(startIdx, startIdx + PER_PAGE);
  103. }, [page, sessions]);
  104. if (loading) {
  105. return (
  106. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  107. <CircularProgress />
  108. </Box>
  109. );
  110. }
  111. return (
  112. <Box>
  113. <Grid container spacing={2}>
  114. {paged.map((session) => {
  115. const statusColor = getStatusColor(session.status);
  116. const planStart = session.planStartDate
  117. ? dayjs(session.planStartDate).format(OUTPUT_DATE_FORMAT)
  118. : "-";
  119. const roundLabel = session.stockTakeRoundId ?? session.stockTakeId;
  120. return (
  121. <Grid key={session.stockTakeId} item xs={12} sm={6} md={4}>
  122. <Card
  123. sx={{
  124. minHeight: 180,
  125. display: "flex",
  126. flexDirection: "column",
  127. border: "1px solid",
  128. borderColor:
  129. statusColor === "success" ? "success.main" : "primary.main",
  130. cursor: "pointer",
  131. "&:hover": {
  132. boxShadow: 4,
  133. },
  134. }}
  135. onClick={() => onCardClick(session)}
  136. >
  137. <CardContent sx={{ pb: 1, flexGrow: 1 }}>
  138. <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 0.5 }}>
  139. {t("Stock Take Round")}: #{roundLabel}
  140. </Typography>
  141. <Typography variant="body2" color="text.secondary">
  142. {t("Plan Start Date")}: {planStart}
  143. </Typography>
  144. <Typography variant="body2" color="text.secondary">
  145. {t("Total Items")}: {session.totalItemNumber}
  146. </Typography>
  147. <Typography variant="body2" color="text.secondary">
  148. {t("Total Lots")}: {session.totalInventoryLotNumber}
  149. </Typography>
  150. </CardContent>
  151. <CardActions sx={{ pt: 0.5, justifyContent: "space-between" }}>
  152. <Button
  153. size="small"
  154. variant="contained"
  155. onClick={(e) => {
  156. e.stopPropagation();
  157. onCardClick(session);
  158. }}
  159. >
  160. {t("View Details")}
  161. </Button>
  162. {session.status ? (
  163. <Chip
  164. size="small"
  165. label={t(session.status)}
  166. color={statusColor as any}
  167. />
  168. ) : (
  169. <Chip size="small" label={t(" ")} color="default" />
  170. )}
  171. </CardActions>
  172. </Card>
  173. </Grid>
  174. );
  175. })}
  176. </Grid>
  177. {sessions.length > 0 && (
  178. <TablePagination
  179. component="div"
  180. count={sessions.length}
  181. page={page}
  182. rowsPerPage={PER_PAGE}
  183. onPageChange={(_, p) => setPage(p)}
  184. rowsPerPageOptions={[PER_PAGE]}
  185. />
  186. )}
  187. </Box>
  188. );
  189. };
  190. export default ApproverAllCardList;