| @@ -71,6 +71,7 @@ import { | |||
| dayjsToDateTimeString, | |||
| } from "@/app/utils/formatUtil"; | |||
| import { AUTH } from "@/authorities"; | |||
| import { statusChipColor } from "./StockTakeRoundCardList"; | |||
| const FLOOR_GROUP_UNSET = "__UNSET__"; | |||
| /** 與 controllQRcodescanWarehouse 選倉相同:樓層 → 倉庫 → 倉位 */ | |||
| @@ -1264,21 +1265,6 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| return Array.from(sectionSet).sort((a, b) => a.localeCompare(b)); | |||
| }); | |||
| }, [roundRecords]); | |||
| const getStatusColor = (status: string) => { | |||
| const statusLower = status.toLowerCase().replace(/_/g, ""); | |||
| if (statusLower === "completed") return "success"; | |||
| if ( | |||
| statusLower === "stocktaking" || | |||
| statusLower === "inprogress" || | |||
| statusLower === "processing" | |||
| ) { | |||
| return "primary"; // 盤點中 — 藍 | |||
| } | |||
| if (statusLower === "approving") return "info"; | |||
| if (statusLower === "pending") return "warning"; // 待處理 — 橘 | |||
| if (statusLower === "nocycle") return "default"; | |||
| return "default"; | |||
| }; | |||
| const TimeDisplay: React.FC<{ startTime: string | null; endTime: string | null }> = ({ startTime, endTime }) => { | |||
| const [currentTime, setCurrentTime] = useState(dayjs()); | |||
| @@ -1339,10 +1325,6 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| return "-"; | |||
| } | |||
| }; | |||
| const getCompletionRate = (session: AllPickedStockTakeListReponse): number => { | |||
| if (session.totalInventoryLotNumber === 0) return 0; | |||
| return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100); | |||
| }; | |||
| const planStartDate = globalRoundPlanStartDate; | |||
| const closeCreateDialog = () => { | |||
| setOpenConfirmDialog(false); | |||
| @@ -1732,16 +1714,14 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| ) : ( | |||
| <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); | |||
| const sectionMeta = [ | |||
| session.stockTakeSectionDescription, | |||
| session.warehouseArea, | |||
| session.storeId, | |||
| ].filter((v): v is string => Boolean(v && v.trim())).join(" / "); | |||
| ].filter((v): v is string => Boolean(v && v.trim())).join(" · "); | |||
| return ( | |||
| <Grid key={session.id} item xs={12} md={6}> | |||
| @@ -1751,7 +1731,7 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| border: "1px solid", | |||
| borderColor: statusColor === "success" ? "success.main" : "primary.main", | |||
| borderColor: "divider", | |||
| }} | |||
| > | |||
| <CardContent sx={{ pb: 1, flexGrow: 1 }}> | |||
| @@ -1792,7 +1772,8 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| {t("View Details")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| variant="outlined" | |||
| color="inherit" | |||
| sx={{ minHeight: 48, px: 2 }} | |||
| onClick={() => onReStockTakeClick(session)} | |||
| disabled={!session.reStockTakeTrueFalse} | |||
| @@ -1802,9 +1783,14 @@ const PickerCardList: React.FC<PickerCardListProps> = ({ | |||
| </Stack> | |||
| <Stack direction="row" spacing={0.5} flexWrap="wrap" justifyContent="flex-end"> | |||
| {session.reStockTakeTrueFalse ? ( | |||
| <Chip size="small" label={t("notMatch")} color="warning" /> | |||
| <Chip size="small" variant="outlined" color="warning" label={t("notMatch")} /> | |||
| ) : null} | |||
| <Chip size="small" label={t(session.status || "")} color={statusColor as any} /> | |||
| <Chip | |||
| size="small" | |||
| variant="outlined" | |||
| color={statusChipColor(session.status || "")} | |||
| label={t(session.status || "")} | |||
| /> | |||
| </Stack> | |||
| </CardActions> | |||
| </Card> | |||
| @@ -11,6 +11,7 @@ import { | |||
| CardContent, | |||
| Chip, | |||
| CircularProgress, | |||
| LinearProgress, | |||
| Dialog, | |||
| DialogActions, | |||
| DialogContent, | |||
| @@ -205,13 +206,32 @@ const StockTakeRoundCardList: React.FC<StockTakeRoundCardListProps> = ({ | |||
| return `${t("Stock Take Round")} #${card.stockTakeRoundId}`; | |||
| }; | |||
| const statusColor = (status: string) => { | |||
| const s = (status || "").toLowerCase().replace(/_/g, ""); | |||
| if (s === "completed") return "success"; | |||
| if (s === "approving") return "info"; | |||
| if (s === "stocktaking" || s === "processing") return "primary"; | |||
| if (s === "pending") return "warning"; | |||
| return "default"; | |||
| const progressRow = (label: string, done: number, total: number, barColor: "primary.main" | "success.main") => { | |||
| const pct = total > 0 ? Math.min(100, (done / total) * 100) : 0; | |||
| return ( | |||
| <Box> | |||
| <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 1 }}> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {label} | |||
| </Typography> | |||
| <Typography variant="body2" fontWeight={600} sx={{ fontVariantNumeric: "tabular-nums" }}> | |||
| {t("Count of total", { done, total })} | |||
| </Typography> | |||
| </Box> | |||
| <LinearProgress | |||
| variant="determinate" | |||
| value={pct} | |||
| aria-label={`${label} ${t("Count of total", { done, total })}`} | |||
| sx={{ | |||
| mt: 0.4, | |||
| height: 4, | |||
| borderRadius: 99, | |||
| bgcolor: "action.hover", | |||
| "& .MuiLinearProgress-bar": { borderRadius: 99, bgcolor: barColor }, | |||
| }} | |||
| /> | |||
| </Box> | |||
| ); | |||
| }; | |||
| return ( | |||
| @@ -309,9 +329,8 @@ const StockTakeRoundCardList: React.FC<StockTakeRoundCardListProps> = ({ | |||
| const total = isItemRound ? (card.totalInventoryLotNumber || 0) : (card.sectionCount || 0); | |||
| const taken = card.stockTakenCount || 0; | |||
| const approved = card.approvedCount || 0; | |||
| const progressLegend = isItemRound | |||
| ? t("Round progress legend lots") | |||
| : t("Round progress legend"); | |||
| const floor = card.mainFloor?.trim(); | |||
| const modeLabel = isItemRound ? t("Create by item") : t("Create by section"); | |||
| return ( | |||
| <Grid key={card.stockTakeRoundId} item xs={12} sm={6} md={4}> | |||
| <Card | |||
| @@ -320,64 +339,54 @@ const StockTakeRoundCardList: React.FC<StockTakeRoundCardListProps> = ({ | |||
| display: "flex", | |||
| flexDirection: "column", | |||
| border: "1px solid", | |||
| borderColor: card.open ? "primary.main" : "success.main", | |||
| borderColor: "divider", | |||
| }} | |||
| > | |||
| <CardContent sx={{ pb: 1, flexGrow: 1 }}> | |||
| <Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 0.5 }}> | |||
| <Typography variant="subtitle1" fontWeight={600}> | |||
| {roundLabel(card)} | |||
| </Typography> | |||
| {card.stockTakeRoundMode === "item" ? ( | |||
| <Chip size="small" label={t("Create by item")} color="info" /> | |||
| ) : null} | |||
| <Stack direction="row" spacing={1} alignItems="flex-start" justifyContent="space-between"> | |||
| <Box sx={{ minWidth: 0 }}> | |||
| <Typography variant="subtitle1" fontWeight={600} noWrap> | |||
| {roundLabel(card)} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {floor ? `${modeLabel} · ${floor}` : modeLabel} | |||
| </Typography> | |||
| </Box> | |||
| <Chip | |||
| size="small" | |||
| variant="outlined" | |||
| color={statusChipColor(card.status)} | |||
| label={t(card.status || " ")} | |||
| sx={{ flexShrink: 0 }} | |||
| /> | |||
| </Stack> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||
| {t("Plan Start Date")}:{" "} | |||
| {card.planStartDate ? dayjs(card.planStartDate).format(OUTPUT_DATE_FORMAT) : "-"} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Main floor")}: {card.mainFloor?.trim() || "-"} | |||
| <Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 1.5 }}> | |||
| {isItemRound ? t("Round unit lots") : t("Round unit sections")} | |||
| </Typography> | |||
| <Stack spacing={0.25}> | |||
| <Typography variant="body2" color="text.secondary" noWrap> | |||
| {progressLegend} | |||
| </Typography> | |||
| <Typography | |||
| fontWeight={700} | |||
| sx={{ | |||
| fontSize: "1.5rem", | |||
| lineHeight: 1.2, | |||
| fontVariantNumeric: "tabular-nums", | |||
| letterSpacing: 0.5, | |||
| }} | |||
| > | |||
| <Box component="span" sx={{ color: "primary.main" }}>{taken}</Box> | |||
| <Box component="span" sx={{ color: "text.disabled", mx: 0.75 }}>/</Box> | |||
| <Box component="span" sx={{ color: "success.main" }}>{approved}</Box> | |||
| <Box component="span" sx={{ color: "text.disabled", mx: 0.75 }}>/</Box> | |||
| <Box component="span" sx={{ color: "text.primary" }}>{total}</Box> | |||
| </Typography> | |||
| <Stack spacing={1} sx={{ mt: 0.5 }}> | |||
| {progressRow(t("progressCounted"), taken, total, "primary.main")} | |||
| {progressRow(t("progressApproved"), approved, total, "success.main")} | |||
| </Stack> | |||
| </CardContent> | |||
| <CardActions sx={{ pt: 0.5, justifyContent: "space-between", flexWrap: "wrap", gap: 1 }}> | |||
| <Stack direction="row" spacing={1}> | |||
| <Button size="small" variant="contained" onClick={() => onEnterRound(card, rounds)}> | |||
| {t("View Details")} | |||
| <CardActions sx={{ pt: 0.5, gap: 1 }}> | |||
| <Button size="small" variant="contained" onClick={() => onEnterRound(card, rounds)}> | |||
| {t("View Details")} | |||
| </Button> | |||
| {isAdmin && card.open ? ( | |||
| <Button | |||
| size="small" | |||
| variant="outlined" | |||
| color="warning" | |||
| disabled={closingId === card.stockTakeRoundId} | |||
| onClick={() => setPendingCloseCard(card)} | |||
| > | |||
| {closingId === card.stockTakeRoundId ? <CircularProgress size={16} /> : t("Close round")} | |||
| </Button> | |||
| {isAdmin && card.open ? ( | |||
| <Button | |||
| size="small" | |||
| variant="outlined" | |||
| color="warning" | |||
| disabled={closingId === card.stockTakeRoundId} | |||
| onClick={() => setPendingCloseCard(card)} | |||
| > | |||
| {closingId === card.stockTakeRoundId ? <CircularProgress size={16} /> : t("Close round")} | |||
| </Button> | |||
| ) : null} | |||
| </Stack> | |||
| <Chip size="small" label={t(card.status || " ")} color={statusColor(card.status) as "success" | "info" | "primary" | "warning"} /> | |||
| ) : null} | |||
| </CardActions> | |||
| </Card> | |||
| </Grid> | |||
| @@ -420,4 +429,16 @@ const StockTakeRoundCardList: React.FC<StockTakeRoundCardListProps> = ({ | |||
| ); | |||
| }; | |||
| type StatusChipColor = "default" | "primary" | "secondary" | "error" | "info" | "success" | "warning"; | |||
| export function statusChipColor(status: string): StatusChipColor { | |||
| const s = (status || "").toLowerCase().replace(/_/g, ""); | |||
| if (s === "completed") return "success"; | |||
| if (s === "pending") return "warning"; | |||
| if (s === "approving") return "info"; | |||
| if (s === "stocktaking" || s === "processing" || s === "inprogress") return "primary"; | |||
| if (s === "notmatch") return "warning"; | |||
| return "default"; | |||
| } | |||
| export default StockTakeRoundCardList; | |||
| @@ -32,6 +32,11 @@ | |||
| "Month suffix": "", | |||
| "Next month": "Next month", | |||
| "Open rounds this month": "Open rounds", | |||
| "progressCounted": "Counted", | |||
| "progressApproved": "Approved", | |||
| "Count of total": "{{done}} of {{total}}", | |||
| "Round unit sections": "Sections", | |||
| "Round unit lots": "Lots", | |||
| "Previous month": "Previous month", | |||
| "Round closed": "Round closed", | |||
| "Main floor": "Main floor", | |||
| @@ -59,7 +64,7 @@ | |||
| "Confirm create stock take": "Confirm create stock take", | |||
| "Section create item overlap title": "These items are already in an open item stock-take round; this round will skip them", | |||
| "Section create item overlap line": "Item {{item}} is in round #{{round}}{{roundName}}; this round will not include it (section {{section}})", | |||
| "Control Time": "Control Time", | |||
| "Control Time": "Time spent", | |||
| "Create by item": "By item", | |||
| "Create by section": "By section", | |||
| "Cannot create while both section and item rounds are open": "Cannot create a new round while both section and item rounds are open", | |||
| @@ -77,7 +82,7 @@ | |||
| "Total selected lots label": "Selected lots", | |||
| "items unit": "item(s)", | |||
| "lots unit": "lot(s)", | |||
| "Create Stock Take (Select Sections)": "Create stock take (select sections)", | |||
| "Create Stock Take (Select Sections)": "Create stock take", | |||
| "Created": "Created", | |||
| "Creation date": "Creation date", | |||
| "Damage": "Damage", | |||
| @@ -179,11 +184,11 @@ | |||
| "Stock take round name": "Stock take round name", | |||
| "Stock take round name placeholder": "Optional, e.g. May full-site stock take", | |||
| "Stock take sections in current list": "{{count}} stock take section(s) in current list", | |||
| "Store ID": "Store ID", | |||
| "Store ID": "Floor", | |||
| "Submit All Inputted": "Submit All Inputted", | |||
| "Total": "Total", | |||
| "Total Adjustments": "Total Adjustments", | |||
| "Total Item Kind Number": "Total Item Kind Number", | |||
| "Total Item Kind Number": "Items", | |||
| "Total Items": "Total Items", | |||
| "Total Lots": "Total Lots", | |||
| "Total Sections": "Total Sections", | |||
| @@ -196,7 +201,7 @@ | |||
| "Variance filter inclusive only": "Within range", | |||
| "Variance filter inclusive range hint": "Show rows with |variance %| ≤ {{value}}%", | |||
| "View Details": "View Details", | |||
| "View ReStockTake": "View ReStockTake", | |||
| "View ReStockTake": "Re-count", | |||
| "Warehouse": "Warehouse", | |||
| "Warehouse code": "Warehouse code", | |||
| "Warehouse Location": "Warehouse Location", | |||
| @@ -211,7 +216,7 @@ | |||
| "book qty": "book qty", | |||
| "completed": "Completed", | |||
| "do": "Delivery order", | |||
| "end time": "end time", | |||
| "end time": "Finished", | |||
| "jo": "Job order", | |||
| "lot_issue": "Lot issue", | |||
| "material": "Material", | |||
| @@ -224,7 +229,7 @@ | |||
| "resolved": "Resolved", | |||
| "sections unit": "area(s)", | |||
| "selected stock take qty": "selected stock take qty", | |||
| "start time": "start time", | |||
| "start time": "Started", | |||
| "Select floor first": "Select floor first", | |||
| "Select warehouse first": "Select warehouse first", | |||
| "Select area first": "Select area first", | |||
| @@ -15,6 +15,11 @@ | |||
| "Month suffix": "月", | |||
| "Next month": "下一個月", | |||
| "Open rounds this month": "進行中的輪次", | |||
| "progressCounted": "已點算", | |||
| "progressApproved": "已審核", | |||
| "Count of total": "{{done}}/{{total}}", | |||
| "Round unit sections": "區域", | |||
| "Round unit lots": "批號", | |||
| "Previous month": "上一個月", | |||
| "Round closed": "已關閉此輪", | |||
| "Main floor": "主要樓層", | |||
| @@ -74,9 +79,9 @@ | |||
| "Last Stock Take Date": "上次盤點日期", | |||
| "Stock Taker": "盤點員", | |||
| "Approver": "審核員", | |||
| "start time": "開始時間", | |||
| "end time": "結束時間", | |||
| "Control Time": "操作時間", | |||
| "start time": "開始", | |||
| "end time": "結束", | |||
| "Control Time": "耗時", | |||
| "Approver stock take record saved successfully": "審核員盤點記錄保存成功", | |||
| "Stock take record status updated to not match": "盤點狀態更新為要求重點", | |||
| "Batch approver save completed: {{success}} success, {{errors}} errors": "批次審核儲存完成:成功 {{success}} 筆,錯誤 {{errors}} 筆", | |||
| @@ -158,9 +163,9 @@ | |||
| "Total selected lots label": "已選批號", | |||
| "items unit": "項", | |||
| "lots unit": "筆", | |||
| "Create Stock Take (Select Sections)": "建立盤點(選擇區域)", | |||
| "Total Item Kind Number": "貨品種類數量", | |||
| "View ReStockTake": "查看重新盤點", | |||
| "Create Stock Take (Select Sections)": "建立盤點", | |||
| "Total Item Kind Number": "貨品種類", | |||
| "View ReStockTake": "重新盤點", | |||
| "Warehouse missing stock take section tooltip has": "有 {{count}} 個倉庫未設定盤點區域,點擊查看", | |||
| "Warehouse missing stock take section tooltip none": "所有倉庫均已設定盤點區域", | |||
| "Warehouse missing stock take section warn title": "未設定盤點區域的倉庫", | |||