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.

198 lines
5.8 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(["inventory", "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. // 找出最新一轮的 planStartDate
  39. const withPlanStart = list.filter((s) => s.planStartDate);
  40. if (withPlanStart.length === 0) {
  41. setSessions([]);
  42. setPage(0);
  43. return;
  44. }
  45. const latestPlanStart = withPlanStart
  46. .map((s) => s.planStartDate as string)
  47. .sort((a, b) => dayjs(b).valueOf() - dayjs(a).valueOf())[0];
  48. // 这一轮下所有 section 的卡片
  49. const roundSessions = list.filter((s) => s.planStartDate === latestPlanStart);
  50. // 汇总这一轮的总 item / lot 数
  51. const totalItems = roundSessions.reduce(
  52. (sum, s) => sum + (s.totalItemNumber || 0),
  53. 0
  54. );
  55. const totalLots = roundSessions.reduce(
  56. (sum, s) => sum + (s.totalInventoryLotNumber || 0),
  57. 0
  58. );
  59. // 用这一轮里的第一条作为代表,覆盖汇总数字
  60. const representative = roundSessions[0];
  61. const mergedRound: AllPickedStockTakeListReponse = {
  62. ...representative,
  63. totalItemNumber: totalItems,
  64. totalInventoryLotNumber: totalLots,
  65. };
  66. // UI 上只展示这一轮一张卡
  67. setSessions([mergedRound]);
  68. setPage(0);
  69. } catch (e) {
  70. console.error(e);
  71. setSessions([]);
  72. } finally {
  73. setLoading(false);
  74. }
  75. }, []);
  76. useEffect(() => {
  77. fetchSessions();
  78. }, [fetchSessions]);
  79. const getStatusColor = (status: string | null) => {
  80. if (!status) return "default";
  81. const statusLower = status.toLowerCase();
  82. if (statusLower === "completed") return "success";
  83. if (statusLower === "approving") return "info";
  84. return "warning";
  85. };
  86. const paged = useMemo(() => {
  87. const startIdx = page * PER_PAGE;
  88. return sessions.slice(startIdx, startIdx + PER_PAGE);
  89. }, [page, sessions]);
  90. if (loading) {
  91. return (
  92. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  93. <CircularProgress />
  94. </Box>
  95. );
  96. }
  97. return (
  98. <Box>
  99. <Grid container spacing={2}>
  100. {paged.map((session) => {
  101. const statusColor = getStatusColor(session.status);
  102. const planStart = session.planStartDate
  103. ? dayjs(session.planStartDate).format(OUTPUT_DATE_FORMAT)
  104. : "-";
  105. return (
  106. <Grid key={session.stockTakeId} item xs={12} sm={6} md={4}>
  107. <Card
  108. sx={{
  109. minHeight: 180,
  110. display: "flex",
  111. flexDirection: "column",
  112. border: "1px solid",
  113. borderColor:
  114. statusColor === "success" ? "success.main" : "primary.main",
  115. cursor: "pointer",
  116. "&:hover": {
  117. boxShadow: 4,
  118. },
  119. }}
  120. onClick={() => onCardClick(session)}
  121. >
  122. <CardContent sx={{ pb: 1, flexGrow: 1 }}>
  123. <Typography variant="subtitle1" fontWeight={600} sx={{ mb: 0.5 }}>
  124. {t("Stock Take Round")}: {planStart}
  125. </Typography>
  126. <Typography variant="body2" color="text.secondary">
  127. {t("Plan Start Date")}: {planStart}
  128. </Typography>
  129. <Typography variant="body2" color="text.secondary">
  130. {t("Total Items")}: {session.totalItemNumber}
  131. </Typography>
  132. <Typography variant="body2" color="text.secondary">
  133. {t("Total Lots")}: {session.totalInventoryLotNumber}
  134. </Typography>
  135. </CardContent>
  136. <CardActions sx={{ pt: 0.5, justifyContent: "space-between" }}>
  137. <Button
  138. size="small"
  139. variant="contained"
  140. onClick={(e) => {
  141. e.stopPropagation();
  142. onCardClick(session);
  143. }}
  144. >
  145. {t("View Details")}
  146. </Button>
  147. {session.status ? (
  148. <Chip
  149. size="small"
  150. label={t(session.status)}
  151. color={statusColor as any}
  152. />
  153. ) : (
  154. <Chip size="small" label={t(" ")} color="default" />
  155. )}
  156. </CardActions>
  157. </Card>
  158. </Grid>
  159. );
  160. })}
  161. </Grid>
  162. {sessions.length > 0 && (
  163. <TablePagination
  164. component="div"
  165. count={sessions.length}
  166. page={page}
  167. rowsPerPage={PER_PAGE}
  168. onPageChange={(_, p) => setPage(p)}
  169. rowsPerPageOptions={[PER_PAGE]}
  170. />
  171. )}
  172. </Box>
  173. );
  174. };
  175. export default ApproverAllCardList;