"use client"; import { Box, Button, Card, CardContent, CardActions, Stack, Typography, Chip, CircularProgress, TablePagination, Grid, Dialog, DialogTitle, DialogContent, DialogActions, TextField, InputAdornment, FormControl, InputLabel, Select, MenuItem, Autocomplete, Badge, IconButton, Tooltip, Drawer, Divider, } from "@mui/material"; import SearchIcon from "@mui/icons-material/Search"; import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined"; import { useRouter } from "next/navigation"; import { SessionWithTokens } from "@/config/authConfig"; import { useSession } from "next-auth/react"; import { useState, useCallback, useEffect, useRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import duration from "dayjs/plugin/duration"; import { AllPickedStockTakeListReponse, createStockTakeForSections, getStockTakeRecordsPaged, getLatestStockTakeRoundMeta, } from "@/app/api/stockTake/actions"; import { fetchStockTakeSections } from "@/app/api/warehouse/actions"; import { fetchMissingStockTakeSectionIssues } from "@/app/api/warehouse/client"; import type { MissingStockTakeSectionIssueItem, StockTakeSectionInfo } from "@/app/api/warehouse"; import dayjs, { type Dayjs } from "dayjs"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { OUTPUT_DATE_FORMAT, OUTPUT_DATETIME_FORMAT, dayjsToDateTimeString, } from "@/app/utils/formatUtil"; import { AUTH } from "@/authorities"; const FLOOR_GROUP_UNSET = "__UNSET__"; /** 移動超過此距離才視為框選(Windows 橡皮筋),否則視為單擊 */ const MARQUEE_DRAG_THRESHOLD_PX = 4; /** 左側樓層列表超過此數量才啟用捲動 */ const FLOOR_LIST_SCROLL_THRESHOLD = 8; const createDialogCompactInputSx = { mt: 0, mb: 0, "& .MuiInputBase-root": { bgcolor: "background.paper", }, "& .MuiOutlinedInput-input": { py: "8.5px", }, "& .MuiInputAdornment-positionStart": { marginRight: 0.5, }, "& .MuiInputBase-input::placeholder": { opacity: 1, // MUI 預設 opacity 較低,設 1 顏色才準 color: "text.secondary", // 中等灰;要更淡用 "text.disabled" }, } as const; type MarqueeRect = { left: number; top: number; width: number; height: number }; function rectsIntersect(a: DOMRect, b: DOMRect): boolean { return !(a.right < b.left || a.left > b.right || a.bottom < b.top || a.top > b.bottom); } function clientMarqueeRect(startX: number, startY: number, endX: number, endY: number): DOMRect { const left = Math.min(startX, endX); const top = Math.min(startY, endY); const right = Math.max(startX, endX); const bottom = Math.max(startY, endY); return { left, top, right, bottom, width: right - left, height: bottom - top } as DOMRect; } interface PickerCardListProps { /** 由父層保存,從明細返回時仍回到同一頁 */ page: number; pageSize: number; onListPageChange: (page: number) => void; onCardClick: (session: AllPickedStockTakeListReponse) => void; onReStockTakeClick: (session: AllPickedStockTakeListReponse) => void; searchFilters: PickerCardListFilters; appliedFilters: PickerCardListFilters; onSearchFiltersChange: (next: PickerCardListFilters) => void; onAppliedFiltersChange: (next: PickerCardListFilters) => void; } export type PickerCardListFilters = { sectionDescription: string; stockTakeSession: string; status: string; area: string; storeId: string; }; const PickerCardList: React.FC = ({ page, pageSize, onListPageChange, onCardClick, onReStockTakeClick, searchFilters, appliedFilters, onSearchFiltersChange, onAppliedFiltersChange, }) => { const { t } = useTranslation(["inventory", "common"]); const router = useRouter(); dayjs.extend(duration); const [loading, setLoading] = useState(false); const [stockTakeSessions, setStockTakeSessions] = useState([]); const [total, setTotal] = useState(0); const { data: session } = useSession() as { data: SessionWithTokens | null }; const abilities = session?.abilities ?? session?.user?.abilities ?? []; const canManageStockTake = abilities.some((a) => a.trim() === AUTH.ADMIN); /** 建立盤點後若仍在 page 0,仍強制重新載入 */ const [listRefreshNonce, setListRefreshNonce] = useState(0); const [globalRoundPlanStartDate, setGlobalRoundPlanStartDate] = useState(null); const [creating, setCreating] = useState(false); const [openConfirmDialog, setOpenConfirmDialog] = useState(false); const [openCreateStockTakeSummaryConfirm, setOpenCreateStockTakeSummaryConfirm] = useState(false); const [missingSectionWarnDrawerOpen, setMissingSectionWarnDrawerOpen] = useState(false); const [missingSectionCount, setMissingSectionCount] = useState(0); const [missingSectionItems, setMissingSectionItems] = useState([]); const [missingSectionIssuesLimit, setMissingSectionIssuesLimit] = useState(50); const [missingSectionIssuesLoading, setMissingSectionIssuesLoading] = useState(false); const [createDialogSelectedSections, setCreateDialogSelectedSections] = useState([]); const [createDialogRoundName, setCreateDialogRoundName] = useState(""); const [createDialogPlanStart, setCreateDialogPlanStart] = useState(() => dayjs()); /** 建立盤點對話框:目前選中的樓層分頁(對應 sortedFloorKeys 索引) */ const [createFloorTabIndex, setCreateFloorTabIndex] = useState(0); const [createDialogSearchQuery, setCreateDialogSearchQuery] = useState(""); const [sectionsLoading, setSectionsLoading] = useState(false); const [stockTakeSectionRows, setStockTakeSectionRows] = useState([]); const createStockTakeInFlightRef = useRef(false); const createSectionGridScrollRef = useRef(null); const createSectionCardRefs = useRef>(new Map()); const marqueeDragRef = useRef<{ pointerId: number; startClientX: number; startClientY: number; currentClientX: number; currentClientY: number; baseSelection: Set; isMarquee: boolean; pointerDownSection: string | null; sectionIds: string[]; } | null>(null); const [marqueeRect, setMarqueeRect] = useState(null); const [marqueePreviewIds, setMarqueePreviewIds] = useState>(() => new Set()); const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState([]); const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState([]); const [storeIdOptions, setStoreIdOptions] = useState(["2F", "4F"]); const statusOptions = ["pending", "stockTaking", "approving", "completed"]; const handleSearch = () => { onAppliedFiltersChange({ sectionDescription: searchFilters.sectionDescription || "All", stockTakeSession: searchFilters.stockTakeSession || "", status: searchFilters.status || "All", area: searchFilters.area || "", storeId: searchFilters.storeId || "All", }); onListPageChange(0); }; const handleResetSearch = () => { const resetFilters: PickerCardListFilters = { sectionDescription: "All", stockTakeSession: "", status: "All", area: "", storeId: "All", }; onSearchFiltersChange(resetFilters); onAppliedFiltersChange(resetFilters); onListPageChange(0); }; useEffect(() => { let cancelled = false; setLoading(true); getStockTakeRecordsPaged(page, pageSize, { sectionDescription: appliedFilters.sectionDescription, stockTakeSections: appliedFilters.stockTakeSession, status: appliedFilters.status, area: appliedFilters.area, storeId: appliedFilters.storeId, onlyLatestRound: true, }) .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, appliedFilters, listRefreshNonce]); useEffect(() => { let cancelled = false; getLatestStockTakeRoundMeta() .then((meta) => { if (cancelled) return; if (meta?.planStartDate) { setGlobalRoundPlanStartDate(dayjs(meta.planStartDate).format(OUTPUT_DATE_FORMAT)); } else { setGlobalRoundPlanStartDate(null); } }) .catch((e) => { console.error("Failed to load latest stock take round meta:", e); if (!cancelled) setGlobalRoundPlanStartDate(null); }); return () => { cancelled = true; }; }, [listRefreshNonce]); //const startIdx = page * PER_PAGE; //const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); useEffect(() => { if (openConfirmDialog) { setCreateDialogSelectedSections([]); setCreateDialogRoundName(""); setCreateDialogPlanStart(dayjs()); setCreateFloorTabIndex(0); setCreateDialogSearchQuery(""); marqueeDragRef.current = null; setMarqueeRect(null); setMarqueePreviewIds(new Set()); setOpenCreateStockTakeSummaryConfirm(false); } }, [openConfirmDialog]); const loadMissingStockTakeSectionIssues = useCallback(async () => { setMissingSectionIssuesLoading(true); try { const res = await fetchMissingStockTakeSectionIssues(50); setMissingSectionCount(res.count); setMissingSectionItems(Array.isArray(res.items) ? res.items : []); setMissingSectionIssuesLimit(res.limit ?? 50); } catch (e) { console.error("Failed to load missing stock take section issues:", e); setMissingSectionCount(0); setMissingSectionItems([]); } finally { setMissingSectionIssuesLoading(false); } }, []); useEffect(() => { if (!openConfirmDialog) return; void loadMissingStockTakeSectionIssues(); }, [openConfirmDialog, loadMissingStockTakeSectionIssues]); const handleGoWarehouseSettings = useCallback(() => { setMissingSectionWarnDrawerOpen(false); setOpenConfirmDialog(false); router.push("/settings/warehouse"); }, [router]); const createStockTakeSummarySections = useMemo(() => { return [...createDialogSelectedSections].sort((a, b) => a.localeCompare(b)); }, [createDialogSelectedSections]); const handleOpenCreateStockTakeSummaryConfirm = useCallback(() => { if (createDialogSelectedSections.length === 0) return; if (createDialogPlanStart == null || !createDialogPlanStart.isValid()) return; setOpenCreateStockTakeSummaryConfirm(true); }, [createDialogSelectedSections.length, createDialogPlanStart]); /** 盤點區域 → 樓層:優先 API(warehouse 帶 storeId),否則用列表卡片上的 storeId */ const sectionsByStore = useMemo(() => { const sectionToFloor = new Map(); stockTakeSectionRows.forEach((row) => { const sec = row.stockTakeSection?.trim(); if (!sec) return; const sid = row.storeId?.trim(); if (sid) sectionToFloor.set(sec, sid); }); stockTakeSessions.forEach((session) => { const sec = session.stockTakeSession?.trim(); if (!sec) return; const sid = session.storeId?.trim(); if (sid && !sectionToFloor.has(sec)) sectionToFloor.set(sec, sid); }); const allSecs = new Set(); stockTakeSectionRows.forEach((r) => { const s = r.stockTakeSection?.trim(); if (s) allSecs.add(s); }); stockTakeSectionOptions.forEach((s) => { const x = s.trim(); if (x) allSecs.add(x); }); const byFloor = new Map>(); allSecs.forEach((sec) => { const floorKey = sectionToFloor.get(sec)?.trim() || FLOOR_GROUP_UNSET; if (!byFloor.has(floorKey)) byFloor.set(floorKey, new Set()); byFloor.get(floorKey)!.add(sec); }); const out = new Map(); byFloor.forEach((set, key) => { out.set(key, Array.from(set).sort((a, b) => a.localeCompare(b))); }); return out; }, [stockTakeSectionRows, stockTakeSessions, stockTakeSectionOptions]); const sortedFloorKeys = useMemo(() => { const keys = Array.from(sectionsByStore.keys()); keys.sort((a, b) => { if (a === FLOOR_GROUP_UNSET) return 1; if (b === FLOOR_GROUP_UNSET) return -1; return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }); }); return keys; }, [sectionsByStore]); const allSectionsForCreateDialog = useMemo(() => { const s = new Set(); sectionsByStore.forEach((arr) => arr.forEach((x) => s.add(x))); return Array.from(s).sort((a, b) => a.localeCompare(b)); }, [sectionsByStore]); useEffect(() => { if (createFloorTabIndex >= sortedFloorKeys.length && sortedFloorKeys.length > 0) { setCreateFloorTabIndex(0); } }, [sortedFloorKeys.length, createFloorTabIndex]); const floorGroupLabel = useCallback( (storeKey: string) => (storeKey === FLOOR_GROUP_UNSET ? t("Floor unassigned") : storeKey), [t], ); const toggleFloorSelectAll = useCallback( (storeKey: string) => { const list = sectionsByStore.get(storeKey) ?? []; setCreateDialogSelectedSections((prev) => { const allIn = list.length > 0 && list.every((s) => prev.includes(s)); if (allIn) { return prev.filter((s) => !list.includes(s)); } const next = new Set(prev); list.forEach((s) => next.add(s)); return Array.from(next).sort((a, b) => a.localeCompare(b)); }); }, [sectionsByStore], ); /** 與卡片標題括號內相同:描述 / 區域 / 樓層 */ const getCreateDialogSectionMeta = useCallback( (section: string) => { const row = stockTakeSectionRows.find((r) => r.stockTakeSection?.trim() === section); const fromList = stockTakeSessions.find((s) => s.stockTakeSession?.trim() === section); const desc = row?.stockTakeSectionDescription?.trim() || fromList?.stockTakeSectionDescription?.trim() || ""; const area = row?.warehouseArea?.trim() || fromList?.warehouseArea?.trim() || ""; // const store = row?.storeId?.trim() || fromList?.storeId?.trim() || ""; const parts = [desc, area].filter((v) => Boolean(v && v.trim())); return parts.length ? parts.join(" / ") : ""; }, [stockTakeSectionRows, stockTakeSessions], ); const activeCreateFloorKey = useMemo(() => { if (sortedFloorKeys.length === 0) return null; return sortedFloorKeys[Math.min(createFloorTabIndex, sortedFloorKeys.length - 1)]; }, [sortedFloorKeys, createFloorTabIndex]); const activeCreateFloorSections = useMemo(() => { if (activeCreateFloorKey == null) return []; return sectionsByStore.get(activeCreateFloorKey) ?? []; }, [activeCreateFloorKey, sectionsByStore]); const createDialogFilteredSections = useMemo(() => { const q = createDialogSearchQuery.trim().toLowerCase(); if (!q) return activeCreateFloorSections; return activeCreateFloorSections.filter((section) => { const meta = getCreateDialogSectionMeta(section); return section.toLowerCase().includes(q) || meta.toLowerCase().includes(q); }); }, [activeCreateFloorSections, createDialogSearchQuery, getCreateDialogSectionMeta]); const getSectionIdsInMarquee = useCallback( (startX: number, startY: number, endX: number, endY: number, sectionIds: string[]) => { const marquee = clientMarqueeRect(startX, startY, endX, endY); const hit = new Set(); sectionIds.forEach((section) => { const el = createSectionCardRefs.current.get(section); if (!el) return; if (rectsIntersect(el.getBoundingClientRect(), marquee)) { hit.add(section); } }); return hit; }, [], ); const syncMarqueeDragUi = useCallback(() => { const drag = marqueeDragRef.current; const scrollEl = createSectionGridScrollRef.current; if (!drag || !scrollEl) return; const dist = Math.hypot( drag.currentClientX - drag.startClientX, drag.currentClientY - drag.startClientY, ); if (!drag.isMarquee && dist < MARQUEE_DRAG_THRESHOLD_PX) { setMarqueeRect(null); setMarqueePreviewIds(new Set()); return; } drag.isMarquee = true; const scrollRect = scrollEl.getBoundingClientRect(); setMarqueeRect({ left: Math.min(drag.startClientX, drag.currentClientX) - scrollRect.left + scrollEl.scrollLeft, top: Math.min(drag.startClientY, drag.currentClientY) - scrollRect.top + scrollEl.scrollTop, width: Math.abs(drag.currentClientX - drag.startClientX), height: Math.abs(drag.currentClientY - drag.startClientY), }); setMarqueePreviewIds( getSectionIdsInMarquee( drag.startClientX, drag.startClientY, drag.currentClientX, drag.currentClientY, drag.sectionIds, ), ); }, [getSectionIdsInMarquee]); const finishMarqueeDrag = useCallback(() => { const drag = marqueeDragRef.current; if (drag?.isMarquee) { const hit = getSectionIdsInMarquee( drag.startClientX, drag.startClientY, drag.currentClientX, drag.currentClientY, drag.sectionIds, ); const merged = new Set(drag.baseSelection); hit.forEach((id) => merged.add(id)); setCreateDialogSelectedSections( Array.from(merged).sort((a, b) => a.localeCompare(b)), ); } else if (drag?.pointerDownSection) { const sec = drag.pointerDownSection; setCreateDialogSelectedSections((prev) => { if (prev.includes(sec)) return prev.filter((s) => s !== sec); return [...prev, sec].sort((a, b) => a.localeCompare(b)); }); } marqueeDragRef.current = null; setMarqueeRect(null); setMarqueePreviewIds(new Set()); }, [getSectionIdsInMarquee]); const handleCreateSectionGridPointerDown = useCallback( (e: React.PointerEvent) => { if (e.button !== 0) return; const scrollEl = createSectionGridScrollRef.current; if (!scrollEl || createDialogFilteredSections.length === 0) return; const cardEl = (e.target as HTMLElement).closest("[data-section-card]"); const pointerDownSection = cardEl?.getAttribute("data-section") ?? null; e.preventDefault(); marqueeDragRef.current = { pointerId: e.pointerId, startClientX: e.clientX, startClientY: e.clientY, currentClientX: e.clientX, currentClientY: e.clientY, baseSelection: new Set(createDialogSelectedSections), isMarquee: false, pointerDownSection, sectionIds: createDialogFilteredSections, }; const onPointerMove = (ev: PointerEvent) => { const drag = marqueeDragRef.current; if (!drag || ev.pointerId !== drag.pointerId) return; drag.currentClientX = ev.clientX; drag.currentClientY = ev.clientY; syncMarqueeDragUi(); }; const onPointerEnd = (ev: PointerEvent) => { const drag = marqueeDragRef.current; if (!drag || ev.pointerId !== drag.pointerId) return; drag.currentClientX = ev.clientX; drag.currentClientY = ev.clientY; syncMarqueeDragUi(); finishMarqueeDrag(); window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointerup", onPointerEnd); window.removeEventListener("pointercancel", onPointerEnd); try { scrollEl.releasePointerCapture(ev.pointerId); } catch { /* ignore */ } }; try { scrollEl.setPointerCapture(e.pointerId); } catch { /* ignore */ } window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointerup", onPointerEnd); window.addEventListener("pointercancel", onPointerEnd); }, [createDialogFilteredSections, createDialogSelectedSections, finishMarqueeDrag, syncMarqueeDragUi], ); const handleCreateStockTake = useCallback(async () => { if (createStockTakeInFlightRef.current) return; if (createDialogSelectedSections.length === 0) return; createStockTakeInFlightRef.current = true; setOpenCreateStockTakeSummaryConfirm(false); setOpenConfirmDialog(false); setCreating(true); try { const planStart = createDialogPlanStart != null ? dayjsToDateTimeString(createDialogPlanStart.startOf("day")) : dayjsToDateTimeString(dayjs().startOf("day")); const result = await createStockTakeForSections( createDialogSelectedSections, createDialogRoundName, planStart, ); 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); setCreateDialogRoundName(""); setCreateDialogPlanStart(dayjs()); } catch (e) { console.error(e); } finally { setCreating(false); createStockTakeInFlightRef.current = false; } }, [createDialogPlanStart, createDialogRoundName, createDialogSelectedSections, onListPageChange, t]); useEffect(() => { setSectionsLoading(true); fetchStockTakeSections() .then((sections) => { setStockTakeSectionRows(Array.isArray(sections) ? sections : []); const descSet = new Set(); const sectionSet = new Set(); const storeIdSet = new Set(["2F", "4F"]); sections.forEach((s) => { const section = s.stockTakeSection?.trim(); if (section) sectionSet.add(section); const desc = s.stockTakeSectionDescription?.trim(); if (desc) descSet.add(desc); const storeId = s.storeId?.trim(); if (storeId) storeIdSet.add(storeId); }); setStockTakeSectionOptions(Array.from(sectionSet).sort((a, b) => a.localeCompare(b))); setSectionDescriptionOptions(Array.from(descSet).sort((a, b) => a.localeCompare(b))); setStoreIdOptions(Array.from(storeIdSet).sort((a, b) => a.localeCompare(b))); }) .catch((e) => { console.error("Failed to load section descriptions for filter:", e); }) .finally(() => { setSectionsLoading(false); }); }, []); useEffect(() => { setStockTakeSectionOptions((prev) => { const sectionSet = new Set(prev); stockTakeSessions.forEach((item) => { const section = item.stockTakeSession?.trim(); if (section) sectionSet.add(section); }); return Array.from(sectionSet).sort((a, b) => a.localeCompare(b)); }); }, [stockTakeSessions]); useEffect(() => { setStoreIdOptions((prev) => { const storeIdSet = new Set([...prev, "2F", "4F"]); stockTakeSessions.forEach((item) => { const storeId = item.storeId?.trim(); if (storeId) storeIdSet.add(storeId); }); return Array.from(storeIdSet).sort((a, b) => a.localeCompare(b)); }); }, [stockTakeSessions]); 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 = globalRoundPlanStartDate; return ( {t("Search Criteria")} {t("Stock Take Section")} onSearchFiltersChange({ ...searchFilters, stockTakeSession: newValue || "", }) } onInputChange={(_, newValue) => onSearchFiltersChange({ ...searchFilters, stockTakeSession: newValue, }) } renderInput={(params) => ( )} /> {t("Status")} onSearchFiltersChange({ ...searchFilters, area: e.target.value, }) } /> {t("Store ID")} {t("Total Sections")}: {total} {t("Start Stock Take Date")}: {planStartDate || "-"} {loading ? ( ) : ( {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(" / "); return ( {t("Section")}: {session.stockTakeSession} {sectionMeta ? ` (${sectionMeta})` : 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 Kind Number")}: {session.totalItemNumber} ); })} )} {total > 0 && ( { onListPageChange(newPage); }} rowsPerPageOptions={[pageSize]} // 如果暂时不让用户改 pageSize,就写死 /> )} {/* Create Stock Take 確認 Dialog */} { setOpenConfirmDialog(false); setCreateDialogRoundName(""); setCreateDialogPlanStart(dayjs()); }} maxWidth={false} fullWidth PaperProps={{ sx: { width: "100%", maxWidth: { xs: "100%", sm: 960, md: 1120 }, maxHeight: "90vh", display: "flex", flexDirection: "column", }, }} > {t("Create Stock Take (Select Sections)")} 0 ? t("Warehouse missing stock take section tooltip has", { count: missingSectionCount }) : t("Warehouse missing stock take section tooltip none") } > setMissingSectionWarnDrawerOpen(true)} sx={{ color: missingSectionCount > 0 ? "warning.main" : "text.secondary", }} > 99 ? "99+" : missingSectionCount} invisible={missingSectionCount === 0} > {/* 左側:盤點設定與樓層 */} {t("Stock take round name")} setCreateDialogRoundName(e.target.value)} inputProps={{ maxLength: 255, "aria-label": t("Stock take round name") }} sx={createDialogCompactInputSx} /> {t("Creation date")} setCreateDialogPlanStart(newValue)} format={OUTPUT_DATE_FORMAT} slotProps={{ textField: { fullWidth: true, size: "small", hiddenLabel: true, inputProps: { "aria-label": t("Creation date") }, sx: createDialogCompactInputSx, }, }} /> {sortedFloorKeys.length === 0 && !sectionsLoading ? ( {t("No stock take sections from warehouse")} ) : ( FLOOR_LIST_SCROLL_THRESHOLD ? { maxHeight: 300, overflowY: "auto", pr: 0.5 } : {}), }} > {sortedFloorKeys.map((floorKey, idx) => { const floorSections = sectionsByStore.get(floorKey) ?? []; const selected = idx === createFloorTabIndex; return ( ); })} )} {t("Total selected sections label")}{" "} {createDialogSelectedSections.length} {" "} {t("sections unit")} {/* 右側:區域卡片網格 */} {activeCreateFloorKey != null ? ( <> {t("Floor area selection header", { floor: floorGroupLabel(activeCreateFloorKey), count: activeCreateFloorSections.length, })} setCreateDialogSearchQuery(e.target.value)} inputProps={{ "aria-label": t("Search section code or name") }} InputProps={{ startAdornment: ( ), }} sx={createDialogCompactInputSx} /> {sectionsLoading ? ( ) : createDialogFilteredSections.length === 0 ? ( {createDialogSearchQuery.trim() ? t("No sections match search") : t("No stock take sections from warehouse")} ) : ( <> {marqueeRect ? ( ) : null} {createDialogFilteredSections.map((section) => { const meta = getCreateDialogSectionMeta(section); const checked = createDialogSelectedSections.includes(section); const previewSelected = marqueePreviewIds.has(section); const visuallySelected = checked || previewSelected; return ( { if (el) { createSectionCardRefs.current.set(section, el); } else { createSectionCardRefs.current.delete(section); } }} sx={{ cursor: "default", borderWidth: visuallySelected ? 2 : 1, borderColor: visuallySelected ? "primary.main" : "divider", bgcolor: visuallySelected ? previewSelected && !checked ? "action.hover" : "action.selected" : "background.paper", transition: marqueeRect ? "none" : "border-color 0.15s, background-color 0.15s", "&:hover": { borderColor: visuallySelected ? "primary.main" : "grey.400", }, }} > {section} {meta ? ( {meta} ) : null} ); })} )} ) : ( {sectionsLoading ? ( ) : ( {t("No stock take sections from warehouse")} )} )} { if (!creating) setOpenCreateStockTakeSummaryConfirm(false); }} maxWidth="sm" fullWidth > {t("Confirm create stock take")} {t("Stock take round name")}:{" "} {createDialogRoundName.trim() || t("Not filled")} {t("Creation date")}:{" "} {createDialogPlanStart?.isValid() ? createDialogPlanStart.format(OUTPUT_DATE_FORMAT) : "-"} {t("Total selected sections label")}{" "} {createStockTakeSummarySections.length} {" "} {t("sections unit")} {createStockTakeSummarySections.map((section) => { const meta = getCreateDialogSectionMeta(section); return ( {section} {meta ? ( {meta} ) : null} ); })} setMissingSectionWarnDrawerOpen(false)} ModalProps={{ sx: (theme) => ({ // Drawer 預設 z-index 1200,低於 Dialog (1300),會被建立盤點對話框遮住 zIndex: theme.zIndex.modal + 2, }), }} PaperProps={{ sx: { width: { xs: "100%", sm: 400 }, p: 0, display: "flex", flexDirection: "column", maxHeight: "100vh", }, }} > {t("Warehouse missing stock take section warn title")} {t("Warehouse missing stock take section drawer hint")} {missingSectionCount > 0 ? ( {t("Warehouse missing stock take section showing", { shown: Math.min(missingSectionItems.length, missingSectionIssuesLimit), count: missingSectionCount, })} ) : null} {missingSectionIssuesLoading ? ( ) : missingSectionItems.length === 0 ? ( {t("Warehouse missing stock take section empty")} ) : ( }> {missingSectionItems.map((row) => ( {row.code || `#${row.id}`} {[row.storeId, row.warehouse, row.area, row.slot].filter(Boolean).join(" / ") || "—"} {row.order ? ( {row.order} ) : null} ))} )} ); }; export default PickerCardList;