"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 = ({ 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([]); 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("All"); const [filterStockTakeSession, setFilterStockTakeSession] = useState(""); 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[] = [ { 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) => { 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(); 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 ( ); } return ( criteria={criteria} onSearch={handleSearch} onReset={handleResetSearch} /> {t("Total Sections")}: {total} {t("Start Stock Take Date")}: {planStartDate || "-"} {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 ( {t("Section")}: {session.stockTakeSession} {session.stockTakeSectionDescription ? ` (${session.stockTakeSectionDescription})` : null} {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} {t("Stock Taker")}: {session.stockTakerName} {t("start time")}: {startTimeDisplay(session.startTime) || "-"} {t("end time")}: {endTimeDisplay(session.endTime) || "-"} {t("Control Time")}: {t("Total Item Number")}: {session.totalItemNumber} ); })} {total > 0 && ( { onListPageChange(newPage); }} rowsPerPageOptions={[pageSize]} // 如果暂时不让用户改 pageSize,就写死 /> )} {/* Create Stock Take 確認 Dialog */} setOpenConfirmDialog(false)} maxWidth="xs" fullWidth > {t("Create Stock Take for All Sections")} {t("Confirm create stock take for all sections?")} ); }; export default PickerCardList;