|
- "use client";
-
- import {
- Box,
- Button,
- Card,
- CardContent,
- CardActions,
- Stack,
- Typography,
- Chip,
- CircularProgress,
- TablePagination,
- Grid,
- LinearProgress,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogContentText,
- DialogActions,
- } from "@mui/material";
- import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox";
- import { useState, useCallback, useEffect } from "react";
- import { useTranslation } from "react-i18next";
- import duration from "dayjs/plugin/duration";
- import {
- getStockTakeRecords,
- AllPickedStockTakeListReponse,
- createStockTakeForSections,
- getStockTakeRecordsPaged,
-
- } from "@/app/api/stockTake/actions";
- import { fetchStockTakeSections } from "@/app/api/warehouse/actions";
- import dayjs from "dayjs";
- import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
-
- const PER_PAGE = 6;
-
- interface PickerCardListProps {
- /** 由父層保存,從明細返回時仍回到同一頁 */
- page: number;
- pageSize: number;
- onListPageChange: (page: number) => void;
- onCardClick: (session: AllPickedStockTakeListReponse) => void;
- onReStockTakeClick: (session: AllPickedStockTakeListReponse) => void;
- }
-
- const PickerCardList: React.FC<PickerCardListProps> = ({
- page,
- pageSize,
- onListPageChange,
- onCardClick,
- onReStockTakeClick,
- }) => {
- const { t } = useTranslation(["inventory", "common"]);
- dayjs.extend(duration);
-
- const PER_PAGE = 6;
- const [loading, setLoading] = useState(false);
- const [stockTakeSessions, setStockTakeSessions] = useState<AllPickedStockTakeListReponse[]>([]);
- const [total, setTotal] = useState(0);
- /** 建立盤點後若仍在 page 0,仍強制重新載入 */
- const [listRefreshNonce, setListRefreshNonce] = useState(0);
- const [creating, setCreating] = useState(false);
- const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
- const [filterSectionDescription, setFilterSectionDescription] = useState<string>("All");
- const [filterStockTakeSession, setFilterStockTakeSession] = useState<string>("");
- const [sectionDescriptionAutocompleteOptions, setSectionDescriptionAutocompleteOptions] = useState<{ value: string; label: string }[]>([]);
- type PickerSearchKey = "sectionDescription" | "stockTakeSession";
- const sectionDescriptionOptions = Array.from(
- new Set(
- stockTakeSessions
- .map((s) => s.stockTakeSectionDescription)
- .filter((v): v is string => !!v)
- )
- );
- /*
- // 按 description + section 双条件过滤
- const filteredSessions = stockTakeSessions.filter((s) => {
- const matchDesc =
- filterSectionDescription === "All" ||
- s.stockTakeSectionDescription === filterSectionDescription;
-
- const sessionParts = (filterStockTakeSession ?? "")
- .split(",")
- .map((p) => p.trim().toLowerCase())
- .filter(Boolean);
-
- const matchSession =
- sessionParts.length === 0 ||
- sessionParts.some((part) =>
- (s.stockTakeSession ?? "").toString().toLowerCase().includes(part)
- );
-
- return matchDesc && matchSession;
- });
- */
-
- // SearchBox 的条件配置
- const criteria: Criterion<PickerSearchKey>[] = [
- {
- type: "autocomplete",
- label: t("Stock Take Section Description"),
- paramName: "sectionDescription",
- options: sectionDescriptionAutocompleteOptions,
- needAll: true,
- },
- {
- type: "text",
- label: t("Stock Take Section (can use , to search multiple sections)"),
- paramName: "stockTakeSession",
- placeholder: "",
- },
- ];
-
- const handleSearch = (inputs: Record<PickerSearchKey | `${PickerSearchKey}To`, string>) => {
- setFilterSectionDescription(inputs.sectionDescription || "All");
- setFilterStockTakeSession(inputs.stockTakeSession || "");
- onListPageChange(0);
- };
- const handleResetSearch = () => {
- setFilterSectionDescription("All");
- setFilterStockTakeSession("");
- onListPageChange(0);
- };
-
- useEffect(() => {
- let cancelled = false;
- setLoading(true);
- getStockTakeRecordsPaged(page, pageSize, {
- sectionDescription: filterSectionDescription,
- stockTakeSections: filterStockTakeSession,
- })
- .then((res) => {
- if (cancelled) return;
- setStockTakeSessions(Array.isArray(res.records) ? res.records : []);
- setTotal(res.total || 0);
- })
- .catch((e) => {
- console.error(e);
- if (!cancelled) {
- setStockTakeSessions([]);
- setTotal(0);
- }
- })
- .finally(() => {
- if (!cancelled) setLoading(false);
- });
- return () => {
- cancelled = true;
- };
- }, [page, pageSize, filterSectionDescription, filterStockTakeSession, listRefreshNonce]);
-
- //const startIdx = page * PER_PAGE;
- //const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE);
-
- const handleCreateStockTake = useCallback(async () => {
- setOpenConfirmDialog(false);
- setCreating(true);
- try {
- const result = await createStockTakeForSections();
- const createdCount = Object.values(result).filter(msg => msg.startsWith("Created:")).length;
- const skippedCount = Object.values(result).filter(msg => msg.startsWith("Skipped:")).length;
- const errorCount = Object.values(result).filter(msg => msg.startsWith("Error:")).length;
-
- let message = `${t("Created")}: ${createdCount}, ${t("Skipped")}: ${skippedCount}`;
- if (errorCount > 0) {
- message += `, ${t("Errors")}: ${errorCount}`;
- }
-
- console.log(message);
-
- onListPageChange(0);
- setListRefreshNonce((n) => n + 1);
- } catch (e) {
- console.error(e);
- } finally {
- setCreating(false);
- }
- }, [onListPageChange, t]);
- useEffect(() => {
- fetchStockTakeSections()
- .then((sections) => {
- const descSet = new Set<string>();
- sections.forEach((s) => {
- const desc = s.stockTakeSectionDescription?.trim();
- if (desc) descSet.add(desc);
- });
- setSectionDescriptionAutocompleteOptions(
- Array.from(descSet).map((desc) => ({ value: desc, label: desc }))
- );
- })
- .catch((e) => {
- console.error("Failed to load section descriptions for filter:", e);
- });
- }, []);
- const getStatusColor = (status: string) => {
- const statusLower = status.toLowerCase();
- if (statusLower === "completed") return "success";
- if (statusLower === "in_progress" || statusLower === "processing") return "primary";
- if (statusLower === "approving") return "info";
- if (statusLower === "stockTaking") return "primary";
- if (statusLower === "no_cycle") return "default";
- return "warning";
- };
- const TimeDisplay: React.FC<{ startTime: string | null; endTime: string | null }> = ({ startTime, endTime }) => {
- const [currentTime, setCurrentTime] = useState(dayjs());
-
- useEffect(() => {
- if (!endTime && startTime) {
- const interval = setInterval(() => {
- setCurrentTime(dayjs());
- }, 1000); // 每秒更新一次
-
- return () => clearInterval(interval);
- }
- }, [startTime, endTime]);
-
- if (endTime && startTime) {
- // 当有结束时间时,计算从开始到结束的持续时间
- const start = dayjs(startTime);
- const end = dayjs(endTime);
- const duration = dayjs.duration(end.diff(start));
- const hours = Math.floor(duration.asHours());
- const minutes = duration.minutes();
- const seconds = duration.seconds();
-
- return (
- <>
- {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
- </>
- );
- } else if (startTime) {
- // 当没有结束时间时,显示实时计时器
- const start = dayjs(startTime);
- const duration = dayjs.duration(currentTime.diff(start));
- const hours = Math.floor(duration.asHours());
- const minutes = duration.minutes();
- const seconds = duration.seconds();
-
- return (
- <>
- {hours.toString().padStart(2, '0')}:{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
- </>
- );
- } else {
- return <>-</>;
- }
- };
- const startTimeDisplay = (startTime: string | null) => {
- if (startTime) {
- const start = dayjs(startTime);
- return start.format("HH:mm");
- } else {
- return "-";
- }
- };
- const endTimeDisplay = (endTime: string | null) => {
- if (endTime) {
- const end = dayjs(endTime);
- return end.format("HH:mm");
- } else {
- return "-";
- }
- };
- const getCompletionRate = (session: AllPickedStockTakeListReponse): number => {
- if (session.totalInventoryLotNumber === 0) return 0;
- return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100);
- };
- const planStartDate = (() => {
- const first = stockTakeSessions.find(s => s.planStartDate);
- if (!first?.planStartDate) return null;
- return dayjs(first.planStartDate).format(OUTPUT_DATE_FORMAT);
- })();
- if (loading) {
- return (
- <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
- <CircularProgress />
- </Box>
- );
- }
-
- return (
- <Box>
- <Box sx={{ width: "100%", mb: 2 }}>
- <SearchBox<PickerSearchKey>
- criteria={criteria}
- onSearch={handleSearch}
- onReset={handleResetSearch}
- />
- </Box>
- <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
-
-
- <Typography variant="body2" color="text.secondary">
- {t("Total Sections")}: {total}
- </Typography>
- <Typography variant="body2" color="text.secondary">
- {t("Start Stock Take Date")}: {planStartDate || "-"}
- </Typography>
-
- <Button
- variant="contained"
- color="primary"
- onClick={() => setOpenConfirmDialog(true)}
- disabled={creating}
- >
- {creating ? <CircularProgress size={20} /> : t("Create Stock Take for All Sections")}
- </Button>
- </Box>
-
- <Grid container spacing={2}>
- {stockTakeSessions.map((session: AllPickedStockTakeListReponse) => {
- const statusColor = getStatusColor(session.status || "");
- const lastStockTakeDate = session.lastStockTakeDate
- ? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT)
- : "-";
- const completionRate = getCompletionRate(session);
-
- return (
- <Grid key={session.id} item xs={12} sm={6} md={4}>
- <Card
- sx={{
- minHeight: 200,
- display: "flex",
- flexDirection: "column",
- border: "1px solid",
- borderColor: statusColor === "success" ? "success.main" : "primary.main",
- }}
- >
- <CardContent sx={{ pb: 1, flexGrow: 1 }}>
- <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
- <Typography variant="subtitle1" fontWeight={600}>
- {t("Section")}: {session.stockTakeSession}
- {session.stockTakeSectionDescription ? ` (${session.stockTakeSectionDescription})` : null}
- </Typography>
-
- </Stack>
-
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
- {t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
- </Typography>
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName}</Typography>
- <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
-
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("start time")}: {startTimeDisplay(session.startTime) || "-"}</Typography>
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("end time")}: {endTimeDisplay(session.endTime) || "-"}</Typography>
- </Stack>
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
- {t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} />
- </Typography>
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Total Item Number")}: {session.totalItemNumber}</Typography>
-
- </CardContent>
-
- <CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}>
- <Stack direction="row" spacing={1}>
- <Button
- variant="contained"
- size="small"
- onClick={() => onCardClick(session)}
- >
- {t("View Details")}
- </Button>
- <Button
- variant="contained"
- size="small"
- onClick={() => onReStockTakeClick(session)}
- disabled={!session.reStockTakeTrueFalse}
- >
- {t("View ReStockTake")}
- </Button>
- </Stack>
- <Chip size="small" label={t(session.status || "")} color={statusColor as any} />
- </CardActions>
- </Card>
- </Grid>
- );
- })}
- </Grid>
-
- {total > 0 && (
- <TablePagination
- component="div"
- count={total}
- page={page}
- rowsPerPage={pageSize}
- onPageChange={(e, newPage) => {
- onListPageChange(newPage);
- }}
- rowsPerPageOptions={[pageSize]} // 如果暂时不让用户改 pageSize,就写死
- />
- )}
- {/* Create Stock Take 確認 Dialog */}
- <Dialog
- open={openConfirmDialog}
- onClose={() => setOpenConfirmDialog(false)}
- maxWidth="xs"
- fullWidth
- >
- <DialogTitle>{t("Create Stock Take for All Sections")}</DialogTitle>
- <DialogContent>
- <DialogContentText>
- {t("Confirm create stock take for all sections?")}
- </DialogContentText>
- </DialogContent>
- <DialogActions>
- <Button onClick={() => setOpenConfirmDialog(false)}>
- {t("Cancel")}
- </Button>
- <Button
- variant="contained"
- color="primary"
- onClick={handleCreateStockTake}
- disabled={creating}
- >
- {creating ? <CircularProgress size={20} /> : t("Confirm")}
- </Button>
- </DialogActions>
- </Dialog>
- </Box>
- );
- };
-
- export default PickerCardList;
|