"use client"; import { Box, Button, Card, CardContent, CardActions, Stack, Typography, Chip, CircularProgress, TablePagination, Grid, Dialog, DialogTitle, DialogContent, DialogActions, TextField, InputAdornment, FormControl, InputLabel, Select, MenuItem, Badge, IconButton, Tooltip, Drawer, Divider, Tab, Tabs, ToggleButton, ToggleButtonGroup, Checkbox, } 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, createStockTakeForItems, createStockTakeForSections, getStockTakeRecordsPaged, getLatestStockTakeRoundMeta, getBusyStockTakeSections, getStockTakeRoundCards, } from "@/app/api/stockTake/actions"; import { fetchItemsByPage } from "@/app/api/settings/item/actions"; import { fetchStockTakeSections } from "@/app/api/warehouse/actions"; import { fetchMissingStockTakeSectionIssues, fetchWarehouseListClient } from "@/app/api/warehouse/client"; import type { MissingStockTakeSectionIssueItem, StockTakeSectionInfo, WarehouseResult } 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__"; /** 與 controllQRcodescanWarehouse 選倉相同:樓層 → 倉庫 → 倉位 */ const LOCATION_ALL = "ALL"; const LOCATION_BUTTONS_PER_ROW = 15; const LOCATION_BUTTON_WIDTH = 80; const LOCATION_NONE_SECTION = "__NONE__"; function chunkList(items: T[], size: number): T[][] { const rows: T[][] = []; for (let i = 0; i < items.length; i += size) { rows.push(items.slice(i, i + size)); } return rows; } type CreateDialogItemHit = { id: number; code: string; name: string }; function itemRecordsFromPage(res: unknown): { id: string | number; code?: string; name?: string }[] { const records = (res as { records?: unknown[] })?.records; return Array.isArray(records) ? (records as { id: string | number; code?: string; name?: string }[]) : []; } function withAllOption(options: string[]): string[] { return options.length > 1 ? [LOCATION_ALL, ...options] : options; } function compareLocationLabel(a: string, b: string): number { return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }); } function matchesPickerCardFilters( session: AllPickedStockTakeListReponse, filters: PickerCardListFilters, ): boolean { if (filters.sectionDescription && filters.sectionDescription !== "All") { if (session.stockTakeSectionDescription !== filters.sectionDescription) return false; } const sectionQuery = filters.stockTakeSession?.trim() ?? ""; if (sectionQuery) { const parts = sectionQuery.split(",").map((part) => part.trim()).filter(Boolean); const name = (session.stockTakeSession || "").toLowerCase(); const hit = parts.some((part) => { const key = part.toLowerCase(); return name === key || name.includes(key); }); if (!hit) return false; } if (filters.status && filters.status !== "All") { const normalized = filters.status.trim().toLowerCase(); if (normalized === "notmatch") { if (!session.reStockTakeTrueFalse) return false; } else { const accepted = normalized === "stocktaking" ? ["stocktaking", "processing", "in_progress"] : [normalized]; if (!accepted.includes((session.status || "").trim().toLowerCase())) return false; } } const area = filters.area?.trim() ?? ""; if (area && !(session.warehouseArea || "").toLowerCase().includes(area.toLowerCase())) return false; if (filters.storeId && filters.storeId !== "All") { const floor = filters.storeId.trim().toLowerCase(); const itemFloor = (session.storeId || "").toLowerCase(); if (itemFloor !== floor && !itemFloor.includes(floor)) return false; } return true; } /** 移動超過此距離才視為框選(Windows 橡皮筋),否則視為單擊 */ const MARQUEE_DRAG_THRESHOLD_PX = 4; /** 左側樓層列表超過此數量才啟用捲動 */ const FLOOR_LIST_SCROLL_THRESHOLD = 8; const SEARCH_FIELD_HEIGHT = 56; /** 下拉最多先顯示約 8 列,其餘在選單內捲動 */ const SEARCH_MENU_MAX_HEIGHT = 320; const searchMenuProps = { PaperProps: { sx: { maxHeight: SEARCH_MENU_MAX_HEIGHT }, }, } as const; const searchFieldSx = { "& .MuiOutlinedInput-root": { height: SEARCH_FIELD_HEIGHT, minHeight: SEARCH_FIELD_HEIGHT, boxSizing: "border-box", alignItems: "center", }, "& .MuiSelect-select": { height: `${SEARCH_FIELD_HEIGHT}px !important`, minHeight: "0 !important", display: "flex", alignItems: "center", boxSizing: "border-box", py: "0 !important", }, } as const; function selectedSectionCodes(value: string): string[] { if (!value.trim() || value.trim() === LOCATION_NONE_SECTION) return []; return value .split(",") .map((part) => part.trim()) .filter((part) => part && part !== LOCATION_NONE_SECTION); } 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; /** 樓層/倉庫/倉位,由父層保存,從明細返回時仍顯示 */ locationPick: PickerLocationPick; onLocationPickChange: (next: PickerLocationPick) => void; stockTakeRoundId?: number | null; hideSectionList?: boolean; autoOpenCreate?: boolean; onCreateDialogClose?: () => void; onRoundsChanged?: () => void; } export type PickerSearchKind = "location" | "section"; export type PickerCardListFilters = { sectionDescription: string; stockTakeSession: string; status: string; area: string; storeId: string; searchKind: PickerSearchKind; }; function locationQueryFromPick( pick: PickerLocationPick, warehouses: WarehouseResult[], ): { stockTakeSession: string; storeId: string } { const useSections = (pick.warehouse !== "" && pick.warehouse !== LOCATION_ALL) || (pick.area !== "" && pick.area !== LOCATION_ALL) || (pick.slot !== "" && pick.slot !== LOCATION_ALL); const sections = new Set(); if (useSections) { warehouses.forEach((w) => { const section = w.stockTakeSection?.trim(); if (!section) return; if (pick.storeId && pick.storeId !== LOCATION_ALL && (w.store_id ?? "").trim() !== pick.storeId) return; if (pick.warehouse && pick.warehouse !== LOCATION_ALL && (w.warehouse ?? "").trim() !== pick.warehouse) return; if (pick.area && pick.area !== LOCATION_ALL && (w.area ?? "").trim() !== pick.area) return; if (pick.slot && pick.slot !== LOCATION_ALL && (w.slot ?? "").trim() !== pick.slot) return; sections.add(section); }); } return { stockTakeSession: useSections ? Array.from(sections).sort(compareLocationLabel).join(",") || LOCATION_NONE_SECTION : "", storeId: !pick.storeId || pick.storeId === LOCATION_ALL || useSections ? "All" : pick.storeId, }; } function appliedPickerFilters( filters: PickerCardListFilters, pick: PickerLocationPick, warehouses: WarehouseResult[], ): PickerCardListFilters { const kind = filters.searchKind ?? "location"; if (kind === "section") { return { sectionDescription: filters.sectionDescription || "All", stockTakeSession: filters.stockTakeSession || "", status: filters.status || "All", area: "", storeId: "All", searchKind: "section", }; } const location = locationQueryFromPick(pick, warehouses); return { sectionDescription: "All", stockTakeSession: location.stockTakeSession, status: filters.status || "All", area: "", storeId: location.storeId, searchKind: "location", }; } export type PickerLocationPick = { storeId: string; warehouse: string; area: string; slot: string }; export const EMPTY_PICKER_LOCATION: PickerLocationPick = { storeId: "", warehouse: "", area: "", slot: "" }; const PickerCardList: React.FC = ({ page, pageSize, onListPageChange, onCardClick, onReStockTakeClick, searchFilters, appliedFilters, onSearchFiltersChange, onAppliedFiltersChange, locationPick, onLocationPickChange, stockTakeRoundId = null, hideSectionList = false, autoOpenCreate = false, onCreateDialogClose, onRoundsChanged, }) => { const { t } = useTranslation(["stockTake", "common"]); const router = useRouter(); dayjs.extend(duration); const [loading, setLoading] = useState(true); /** 這一輪全部卡片,篩選在前端做,點選不再重打 API */ const [roundRecords, setRoundRecords] = useState([]); 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 [busySections, setBusySections] = useState>(() => new Set()); 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 [createMode, setCreateMode] = useState<"section" | "item">("section"); const [blockSectionCreate, setBlockSectionCreate] = useState(false); const [createDialogItemCode, setCreateDialogItemCode] = useState(""); const [createDialogItemName, setCreateDialogItemName] = useState(""); const [createDialogItemHits, setCreateDialogItemHits] = useState([]); const [createDialogSelectedItems, setCreateDialogSelectedItems] = useState([]); const [createDialogItemSearching, setCreateDialogItemSearching] = useState(false); 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 [stockTakeSectionOptions, setStockTakeSectionOptions] = useState([]); const [locationWarehouses, setLocationWarehouses] = useState([]); /** 由選倉寫入的盤點區域字串;使用者改過區域欄後就不再覆寫 */ const locationOwnedSectionRef = useRef( (locationPick.warehouse && locationPick.warehouse !== LOCATION_ALL) || (locationPick.area && locationPick.area !== LOCATION_ALL) || (locationPick.slot && locationPick.slot !== LOCATION_ALL) ? searchFilters.stockTakeSession : null, ); const statusOptions = ["All", "pending", "stockTaking", "approving", "completed", "notMatch"]; const searchKind = searchFilters.searchKind ?? "location"; const handleResetSearch = () => { const resetFilters: PickerCardListFilters = { sectionDescription: "All", stockTakeSession: "", status: "All", area: "", storeId: "All", searchKind, }; onSearchFiltersChange(resetFilters); onAppliedFiltersChange(resetFilters); onLocationPickChange(EMPTY_PICKER_LOCATION); locationOwnedSectionRef.current = null; onListPageChange(0); }; useEffect(() => { let cancelled = false; fetchWarehouseListClient() .then((rows) => { if (!cancelled) setLocationWarehouses(Array.isArray(rows) ? rows : []); }) .catch((e) => { console.error("Failed to load warehouses for stock take location filter:", e); if (!cancelled) setLocationWarehouses([]); }); return () => { cancelled = true; }; }, []); useEffect(() => { if (autoOpenCreate) setOpenConfirmDialog(true); }, [autoOpenCreate]); useEffect(() => { if (hideSectionList) { setLoading(false); return; } let cancelled = false; setLoading(true); getStockTakeRecordsPaged(0, 10000, { sectionDescription: "All", stockTakeSections: "", status: "All", area: "", storeId: "All", onlyLatestRound: !(stockTakeRoundId != null && stockTakeRoundId > 0), stockTakeRoundId: stockTakeRoundId != null && stockTakeRoundId > 0 ? stockTakeRoundId : undefined, }) .then((res) => { if (cancelled) return; setRoundRecords(Array.isArray(res.records) ? res.records : []); }) .catch((e) => { console.error("Failed to load this round's stock take sections:", e); if (!cancelled) setRoundRecords([]); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [hideSectionList, listRefreshNonce, stockTakeRoundId]); const roundSectionCodes = useMemo( () => Array.from( new Set( roundRecords .map((row) => row.stockTakeSession?.trim()) .filter((code): code is string => Boolean(code)), ), ), [roundRecords], ); const filteredSessions = useMemo( () => roundRecords.filter((session) => matchesPickerCardFilters(session, appliedFilters)), [appliedFilters, roundRecords], ); const total = filteredSessions.length; const stockTakeSessions = useMemo( () => filteredSessions.slice(page * pageSize, page * pageSize + pageSize), [filteredSessions, page, pageSize], ); const stockTakeLocationWarehouses = useMemo(() => { if (roundSectionCodes.length === 0) return []; const inRound = new Set(roundSectionCodes); return locationWarehouses.filter((w) => { const section = w.stockTakeSection?.trim(); return Boolean(section && inRound.has(section)); }); }, [locationWarehouses, roundSectionCodes]); const locationFloorOptions = useMemo(() => { const set = new Set(); stockTakeLocationWarehouses.forEach((w) => { const floor = w.store_id?.trim(); if (floor) set.add(floor); }); return withAllOption(Array.from(set).sort(compareLocationLabel)); }, [stockTakeLocationWarehouses]); const locationWarehouseOptions = useMemo(() => { if (!locationPick.storeId) return []; const set = new Set(); stockTakeLocationWarehouses.forEach((w) => { if (locationPick.storeId !== LOCATION_ALL && (w.store_id ?? "").trim() !== locationPick.storeId) return; const zone = w.warehouse?.trim(); if (zone) set.add(zone); }); return withAllOption(Array.from(set).sort(compareLocationLabel)); }, [stockTakeLocationWarehouses, locationPick.storeId]); const locationAreaOptions = useMemo(() => { if (!locationPick.storeId || !locationPick.warehouse) return []; const set = new Set(); stockTakeLocationWarehouses.forEach((w) => { if (locationPick.storeId !== LOCATION_ALL && (w.store_id ?? "").trim() !== locationPick.storeId) return; if (locationPick.warehouse !== LOCATION_ALL && (w.warehouse ?? "").trim() !== locationPick.warehouse) return; const area = w.area?.trim(); if (area) set.add(area); }); return withAllOption(Array.from(set).sort(compareLocationLabel)); }, [stockTakeLocationWarehouses, locationPick.storeId, locationPick.warehouse]); const locationSlotOptions = useMemo(() => { if (!locationPick.storeId || !locationPick.warehouse || !locationPick.area) return []; const set = new Set(); stockTakeLocationWarehouses.forEach((w) => { if (locationPick.storeId !== LOCATION_ALL && (w.store_id ?? "").trim() !== locationPick.storeId) return; if (locationPick.warehouse !== LOCATION_ALL && (w.warehouse ?? "").trim() !== locationPick.warehouse) return; if (locationPick.area !== LOCATION_ALL && (w.area ?? "").trim() !== locationPick.area) return; const slot = w.slot?.trim(); if (slot) set.add(slot); }); return withAllOption(Array.from(set).sort(compareLocationLabel)); }, [stockTakeLocationWarehouses, locationPick.storeId, locationPick.warehouse, locationPick.area]); const locationWarehouseRows = useMemo( () => chunkList(locationWarehouseOptions, LOCATION_BUTTONS_PER_ROW), [locationWarehouseOptions], ); const locationAreaRows = useMemo( () => chunkList(locationAreaOptions, LOCATION_BUTTONS_PER_ROW), [locationAreaOptions], ); const locationSlotRows = useMemo( () => chunkList(locationSlotOptions, LOCATION_BUTTONS_PER_ROW), [locationSlotOptions], ); const applyFilters = useCallback((next: PickerCardListFilters) => { const withKind = { ...next, searchKind: next.searchKind ?? "location" }; onSearchFiltersChange(withKind); onAppliedFiltersChange(appliedPickerFilters(withKind, locationPick, stockTakeLocationWarehouses)); onListPageChange(0); }, [locationPick, onAppliedFiltersChange, onListPageChange, onSearchFiltersChange, stockTakeLocationWarehouses]); /** 依盤點區域搜索時用這一輪全部區域;依位置搜索時下拉不顯示 */ const searchSectionCodes = useMemo(() => { if (searchKind === "section" || !locationPick.storeId) return [...roundSectionCodes].sort(compareLocationLabel); const codes = new Set(); stockTakeLocationWarehouses.forEach((w) => { const section = w.stockTakeSection?.trim(); if (!section) return; if (locationPick.storeId !== LOCATION_ALL && (w.store_id ?? "").trim() !== locationPick.storeId) return; if (locationPick.warehouse && locationPick.warehouse !== LOCATION_ALL && (w.warehouse ?? "").trim() !== locationPick.warehouse) return; if (locationPick.area && locationPick.area !== LOCATION_ALL && (w.area ?? "").trim() !== locationPick.area) return; if (locationPick.slot && locationPick.slot !== LOCATION_ALL && (w.slot ?? "").trim() !== locationPick.slot) return; codes.add(section); }); return Array.from(codes).sort(compareLocationLabel); }, [locationPick, roundSectionCodes, searchKind, stockTakeLocationWarehouses]); const sectionDescriptionByCode = useMemo(() => { const map = new Map(); stockTakeSectionRows.forEach((row) => { const section = row.stockTakeSection?.trim(); const text = row.stockTakeSectionDescription?.trim(); if (section && text) map.set(section, text); }); roundRecords.forEach((session) => { const section = session.stockTakeSession?.trim(); const text = session.stockTakeSectionDescription?.trim(); if (section && text && !map.has(section)) map.set(section, text); }); return map; }, [stockTakeSectionRows, roundRecords]); const searchSectionDescriptionOptions = useMemo(() => { const picked = selectedSectionCodes(searchFilters.stockTakeSession).filter((code) => searchSectionCodes.includes(code) ); const source = picked.length > 0 ? picked : searchSectionCodes; const desc = new Set(); source.forEach((code) => { const text = sectionDescriptionByCode.get(code); if (text) desc.add(text); }); return Array.from(desc).sort((a, b) => a.localeCompare(b)); }, [searchFilters.stockTakeSession, searchSectionCodes, sectionDescriptionByCode]); const sectionCodeMenuOptions = useMemo(() => { const descFilter = searchFilters.sectionDescription !== "All" ? searchFilters.sectionDescription : ""; if (!descFilter) return searchSectionCodes; return searchSectionCodes.filter((code) => sectionDescriptionByCode.get(code) === descFilter); }, [searchFilters.sectionDescription, searchSectionCodes, sectionDescriptionByCode]); useEffect(() => { if (searchKind !== "section") return; const current = searchFilters.sectionDescription; if (!current || current === "All") return; if (roundSectionCodes.length === 0) return; if (searchSectionDescriptionOptions.includes(current)) return; applyFilters({ ...searchFilters, sectionDescription: "All", searchKind: "section" }); }, [ applyFilters, roundSectionCodes.length, searchFilters, searchKind, searchSectionDescriptionOptions, ]); const applyLocationPick = useCallback( (pick: PickerLocationPick) => { const location = locationQueryFromPick(pick, stockTakeLocationWarehouses); locationOwnedSectionRef.current = location.stockTakeSession || null; const next: PickerCardListFilters = { ...searchFilters, searchKind: "location", }; onSearchFiltersChange(next); onAppliedFiltersChange(appliedPickerFilters(next, pick, stockTakeLocationWarehouses)); onListPageChange(0); }, [onAppliedFiltersChange, onListPageChange, onSearchFiltersChange, searchFilters, stockTakeLocationWarehouses], ); useEffect(() => { if (searchKind !== "location") return; if (!locationPick.storeId && !locationPick.warehouse && !locationPick.area && !locationPick.slot) return; if (roundSectionCodes.length === 0 || locationWarehouses.length === 0) return; const floorOk = !locationPick.storeId || locationFloorOptions.includes(locationPick.storeId); const warehouseOk = !locationPick.warehouse || locationWarehouseOptions.includes(locationPick.warehouse); const areaOk = !locationPick.area || locationAreaOptions.includes(locationPick.area); const slotOk = !locationPick.slot || locationSlotOptions.includes(locationPick.slot); if (floorOk && warehouseOk && areaOk && slotOk) return; onLocationPickChange(EMPTY_PICKER_LOCATION); applyLocationPick(EMPTY_PICKER_LOCATION); }, [applyLocationPick, locationAreaOptions, locationFloorOptions, locationPick, locationSlotOptions, locationWarehouseOptions, locationWarehouses.length, onLocationPickChange, roundSectionCodes.length]); 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]); useEffect(() => { if (!openConfirmDialog) return; let cancelled = false; getBusyStockTakeSections() .then((list) => { if (cancelled) return; setBusySections(new Set((Array.isArray(list) ? list : []).map((s) => s.trim()).filter(Boolean))); }) .catch((e) => { console.error("Failed to load busy stock take sections:", e); if (!cancelled) setBusySections(new Set()); }); return () => { cancelled = true; }; }, [openConfirmDialog]); useEffect(() => { if (!openConfirmDialog) return; let cancelled = false; getStockTakeRoundCards() .then((res) => { if (cancelled) return; const blocked = (res.rounds || []).some( (round) => round.open && round.stockTakeRoundMode === "item", ); setBlockSectionCreate(blocked); if (blocked) setCreateMode("item"); }) .catch((e) => { console.error("Failed to check open item stock take rounds:", e); if (!cancelled) setBlockSectionCreate(false); }); return () => { cancelled = true; }; }, [openConfirmDialog]); 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 (createDialogPlanStart == null || !createDialogPlanStart.isValid()) return; if (createDialogPlanStart.startOf("day").isBefore(dayjs().startOf("day"))) return; if (createMode === "item") { if (createDialogSelectedItems.length === 0) return; } else if (createDialogSelectedSections.length === 0) { return; } setOpenCreateStockTakeSummaryConfirm(true); }, [ createDialogPlanStart, createDialogSelectedItems.length, createDialogSelectedSections.length, createMode, ]); /** 盤點區域 → 樓層:優先 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); }); roundRecords.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, roundRecords, 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 isSectionBusy = useCallback( (section: string) => busySections.has(section.trim()), [busySections], ); const allSectionsForCreateDialog = useMemo(() => { const s = new Set(); sectionsByStore.forEach((arr) => arr.forEach((x) => s.add(x))); return Array.from(s) .filter((sec) => !busySections.has(sec.trim())) .sort((a, b) => a.localeCompare(b)); }, [busySections, 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) ?? []).filter((s) => !busySections.has(s.trim())); 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)); }); }, [busySections, sectionsByStore], ); /** 與卡片標題括號內相同:描述 / 區域 / 樓層 */ const getCreateDialogSectionMeta = useCallback( (section: string) => { const row = stockTakeSectionRows.find((r) => r.stockTakeSection?.trim() === section); const fromList = roundRecords.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, roundRecords], ); 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) => { if (!busySections.has(id.trim())) merged.add(id); }); setCreateDialogSelectedSections( Array.from(merged).sort((a, b) => a.localeCompare(b)), ); } else if (drag?.pointerDownSection) { const sec = drag.pointerDownSection; if (busySections.has(sec.trim())) { /* in progress */ } else { 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()); }, [busySections, 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 (createMode === "item") { if (createDialogSelectedItems.length === 0) return; } else 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 = createMode === "item" ? await createStockTakeForItems( createDialogSelectedItems.map((it) => it.id), createDialogRoundName, planStart, ) : 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); onRoundsChanged?.(); setCreateDialogRoundName(""); setCreateDialogPlanStart(dayjs()); setCreateDialogSelectedItems([]); setCreateMode("section"); } catch (e) { console.error(e); } finally { setCreating(false); createStockTakeInFlightRef.current = false; } }, [ createDialogPlanStart, createDialogRoundName, createDialogSelectedItems, createDialogSelectedSections, createMode, onListPageChange, onRoundsChanged, t, ]); useEffect(() => { if (!openConfirmDialog || createMode !== "item") return; const code = createDialogItemCode.trim(); const name = createDialogItemName.trim(); if (!code && !name) { setCreateDialogItemHits([]); return; } let cancelled = false; const timer = window.setTimeout(() => { setCreateDialogItemSearching(true); const params: Record = { pageSize: 300, pageNum: 1 }; if (code) params.code = code; if (name) params.name = name; fetchItemsByPage(params) .then((page) => { if (cancelled) return; const uniq: CreateDialogItemHit[] = []; itemRecordsFromPage(page).forEach((row) => { const id = Number(row.id); if (!id) return; uniq.push({ id, code: row.code || "", name: row.name || "" }); }); uniq.sort((a, b) => a.code.localeCompare(b.code, undefined, { numeric: true, sensitivity: "base" }), ); setCreateDialogItemHits(uniq); }) .catch((e) => { console.error("Failed to search items for stock take create:", e); if (!cancelled) setCreateDialogItemHits([]); }) .finally(() => { if (!cancelled) setCreateDialogItemSearching(false); }); }, 300); return () => { cancelled = true; window.clearTimeout(timer); }; }, [openConfirmDialog, createMode, createDialogItemCode, createDialogItemName]); useEffect(() => { setSectionsLoading(true); fetchStockTakeSections() .then((sections) => { setStockTakeSectionRows(Array.isArray(sections) ? sections : []); const sectionSet = new Set(); sections.forEach((s) => { const section = s.stockTakeSection?.trim(); if (section) sectionSet.add(section); }); setStockTakeSectionOptions(Array.from(sectionSet).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); roundRecords.forEach((item) => { const section = item.stockTakeSession?.trim(); if (section) sectionSet.add(section); }); return Array.from(sectionSet).sort((a, b) => a.localeCompare(b)); }); }, [roundRecords]); 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; const closeCreateDialog = () => { setOpenConfirmDialog(false); setCreateDialogRoundName(""); setCreateDialogPlanStart(dayjs()); setCreateMode(blockSectionCreate ? "item" : "section"); setCreateDialogItemCode(""); setCreateDialogItemName(""); setCreateDialogItemHits([]); setCreateDialogSelectedItems([]); onCreateDialogClose?.(); }; return ( {!hideSectionList && ( <> {t("Search Criteria")} applyFilters({ ...searchFilters, searchKind: value })} sx={{ minHeight: 48 }} > {searchKind === "section" && ( {t("Stock Take Section")} )} {searchKind === "section" && ( {t("Stock Take Section code")} )} {t("Status")} { if (next == null) return; applyFilters({ ...searchFilters, status: next }); }} > {statusOptions.map((status) => ( {status === "All" ? t("All") : t(status)} ))} {searchKind === "location" && ( {t("Store ID")} { const pick = { storeId: next ?? "", warehouse: "", area: "", slot: "" }; onLocationPickChange(pick); applyLocationPick(pick); }} > {locationFloorOptions.map((floor) => ( {floor === LOCATION_ALL ? t("All") : floor} ))} {t("Warehouse")} {locationWarehouseRows.map((row, rowIndex) => ( { if (next == null) return; const pick = { ...locationPick, warehouse: next, area: "", slot: "" }; onLocationPickChange(pick); applyLocationPick(pick); }} sx={{ "& .MuiToggleButtonGroup-grouped": { width: LOCATION_BUTTON_WIDTH, minWidth: LOCATION_BUTTON_WIDTH, maxWidth: LOCATION_BUTTON_WIDTH, px: 0, }, }} > {row.map((zone) => ( {zone === LOCATION_ALL ? t("All") : zone} ))} ))} {!locationPick.storeId ? ( {t("Select floor first")} ) : null} {t("Area")} {locationAreaRows.map((row, rowIndex) => ( { if (next == null) return; const pick = { ...locationPick, area: next, slot: "" }; onLocationPickChange(pick); applyLocationPick(pick); }} sx={{ "& .MuiToggleButtonGroup-grouped": { width: LOCATION_BUTTON_WIDTH, minWidth: LOCATION_BUTTON_WIDTH, maxWidth: LOCATION_BUTTON_WIDTH, px: 0, }, }} > {row.map((area) => ( {area === LOCATION_ALL ? t("All") : area} ))} ))} {!locationPick.warehouse ? ( {locationPick.storeId ? t("Select warehouse first") : t("Select floor first")} ) : null} {t("Slot")} {locationSlotRows.map((row, rowIndex) => ( { if (next == null) return; const pick = { ...locationPick, slot: next }; onLocationPickChange(pick); applyLocationPick(pick); }} sx={{ "& .MuiToggleButtonGroup-grouped": { width: LOCATION_BUTTON_WIDTH, minWidth: LOCATION_BUTTON_WIDTH, maxWidth: LOCATION_BUTTON_WIDTH, px: 0, }, }} > {row.map((slot) => ( {slot === LOCATION_ALL ? t("All") : slot} ))} ))} {!locationPick.area ? ( {locationPick.warehouse ? t("Select area first") : locationPick.storeId ? t("Select warehouse first") : t("Select floor first")} ) : null} )} {roundRecords.some((s) => s.listMode === "item") ? t("Total Items") : t("Total Sections")}: {total} {t("Start Stock Take Date")}: {planStartDate || "-"} {stockTakeRoundId != null && stockTakeRoundId > 0 ? null : ( )} {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 ( {session.listMode === "item" ? `${t("Item")}: ${session.stockTakeSession}${ session.stockTakeSectionDescription ? ` (${session.stockTakeSectionDescription})` : "" }` : `${t("Section")}: ${session.stockTakeSession}${sectionMeta ? ` (${sectionMeta})` : ""}`} {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} {session.reStockTakeTrueFalse ? ( ) : null} ); })} )} {total > 0 && ( { onListPageChange(newPage); }} rowsPerPageOptions={[pageSize]} // 如果暂时不让用户改 pageSize,就写死 /> )} )} {/* Create Stock Take 確認 Dialog */} {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} > { if (!v) return; setCreateMode(v); }} > {blockSectionCreate ? null : ( )} {/* 左側:盤點設定與樓層 */} {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} minDate={dayjs().startOf("day")} slotProps={{ textField: { fullWidth: true, size: "small", hiddenLabel: true, inputProps: { "aria-label": t("Creation date") }, sx: createDialogCompactInputSx, }, }} /> {createMode === "item" ? ( {t("Create by item hint")} ) : 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 ( ); })} )} {createMode === "item" ? null : ( )} {createMode === "item" ? t("Total selected items label") : t("Total selected sections label")}{" "} {createMode === "item" ? createDialogSelectedItems.length : createDialogSelectedSections.length} {" "} {createMode === "item" ? t("items unit") : t("sections unit")} {/* 右側:區域卡片網格 */} {createMode === "item" ? ( setCreateDialogItemCode(e.target.value)} InputProps={{ startAdornment: ( ), }} sx={createDialogCompactInputSx} /> setCreateDialogItemName(e.target.value)} InputProps={{ startAdornment: ( ), }} sx={createDialogCompactInputSx} /> {t("Create by item selected count", { count: createDialogSelectedItems.length })} {createDialogSelectedItems.map((it) => ( setCreateDialogSelectedItems((prev) => prev.filter((x) => x.id !== it.id)) } /> ))} {createDialogItemSearching ? ( ) : createDialogItemHits.length === 0 ? ( {createDialogItemCode.trim() || createDialogItemName.trim() ? t("No data") : t("Create by item search hint")} ) : ( {createDialogItemHits.map((it) => { const checked = createDialogSelectedItems.some((x) => x.id === it.id); return ( { setCreateDialogSelectedItems((prev) => checked ? prev.filter((x) => x.id !== it.id) : [...prev, it], ); }} /> {it.code} {it.name} ); })} )} ) : 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 busy = isSectionBusy(section); const checked = createDialogSelectedSections.includes(section); const previewSelected = !busy && marqueePreviewIds.has(section); const visuallySelected = !busy && (checked || previewSelected); return ( { if (el) { createSectionCardRefs.current.set(section, el); } else { createSectionCardRefs.current.delete(section); } }} sx={{ cursor: busy ? "not-allowed" : "default", opacity: busy ? 0.55 : 1, 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} {busy ? ( {t("In progress")} ) : null} {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) : "-"} {createMode === "item" ? t("Total selected items label") : t("Total selected sections label")}{" "} {createMode === "item" ? createDialogSelectedItems.length : createStockTakeSummarySections.length} {" "} {createMode === "item" ? t("items unit") : t("sections unit")} {createMode === "item" ? createDialogSelectedItems.map((it) => ( {it.code} {it.name} )) : 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;