|
- "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<PickerCardListProps> = ({
- 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<AllPickedStockTakeListReponse[]>([]);
- 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<string | null>(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<MissingStockTakeSectionIssueItem[]>([]);
- const [missingSectionIssuesLimit, setMissingSectionIssuesLimit] = useState(50);
- const [missingSectionIssuesLoading, setMissingSectionIssuesLoading] = useState(false);
- const [createDialogSelectedSections, setCreateDialogSelectedSections] = useState<string[]>([]);
- const [createDialogRoundName, setCreateDialogRoundName] = useState("");
- const [createDialogPlanStart, setCreateDialogPlanStart] = useState<Dayjs | null>(() => dayjs());
- /** 建立盤點對話框:目前選中的樓層分頁(對應 sortedFloorKeys 索引) */
- const [createFloorTabIndex, setCreateFloorTabIndex] = useState(0);
- const [createDialogSearchQuery, setCreateDialogSearchQuery] = useState("");
- const [sectionsLoading, setSectionsLoading] = useState(false);
- const [stockTakeSectionRows, setStockTakeSectionRows] = useState<StockTakeSectionInfo[]>([]);
- const createStockTakeInFlightRef = useRef(false);
- const createSectionGridScrollRef = useRef<HTMLDivElement>(null);
- const createSectionCardRefs = useRef<Map<string, HTMLElement>>(new Map());
- const marqueeDragRef = useRef<{
- pointerId: number;
- startClientX: number;
- startClientY: number;
- currentClientX: number;
- currentClientY: number;
- baseSelection: Set<string>;
- isMarquee: boolean;
- pointerDownSection: string | null;
- sectionIds: string[];
- } | null>(null);
- const [marqueeRect, setMarqueeRect] = useState<MarqueeRect | null>(null);
- const [marqueePreviewIds, setMarqueePreviewIds] = useState<Set<string>>(() => new Set());
- const [sectionDescriptionOptions, setSectionDescriptionOptions] = useState<string[]>([]);
- const [stockTakeSectionOptions, setStockTakeSectionOptions] = useState<string[]>([]);
- const [storeIdOptions, setStoreIdOptions] = useState<string[]>(["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<string, string>();
- 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<string>();
- 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<string, Set<string>>();
- 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<string, string[]>();
- 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<string>();
- 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<string>();
- 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<HTMLDivElement>) => {
- 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<string>();
- const sectionSet = new Set<string>();
- const storeIdSet = new Set<string>(["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<string>(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<string>([...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 (
- <Box>
- <Card elevation={0} sx={{ mb: 2 }}>
- <CardContent>
- <Typography variant="overline" sx={{ display: "block", mb: 1 }}>
- {t("Search Criteria")}
- </Typography>
- <Grid container spacing={2}>
- <Grid item xs={12} md={4}>
- <FormControl fullWidth>
- <InputLabel>{t("Stock Take Section")}</InputLabel>
- <Select
- size="small"
- value={searchFilters.sectionDescription}
- label={t("Stock Take Section")}
- onChange={(e) =>
- onSearchFiltersChange({
- ...searchFilters,
- sectionDescription: e.target.value,
- })
- }
- >
- <MenuItem value="All">{t("All")}</MenuItem>
- {sectionDescriptionOptions.map((desc) => (
- <MenuItem key={desc} value={desc}>
- {desc}
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- </Grid>
- <Grid item xs={12} md={4}>
- <Autocomplete
- freeSolo
- options={stockTakeSectionOptions}
- value={searchFilters.stockTakeSession}
- onChange={(_, newValue) =>
- onSearchFiltersChange({
- ...searchFilters,
- stockTakeSession: newValue || "",
- })
- }
- onInputChange={(_, newValue) =>
- onSearchFiltersChange({
- ...searchFilters,
- stockTakeSession: newValue,
- })
- }
- renderInput={(params) => (
- <TextField
- {...params}
- fullWidth
- label={t("Stock Take Section (can use , to search multiple sections)")}
- />
- )}
- />
- </Grid>
- <Grid item xs={12} md={4}>
- <FormControl fullWidth>
- <InputLabel>{t("Status")}</InputLabel>
- <Select
- value={searchFilters.status}
- label={t("Status")}
- onChange={(e) =>
- onSearchFiltersChange({
- ...searchFilters,
- status: e.target.value,
- })
- }
- >
- <MenuItem value="All">{t("All")}</MenuItem>
- {statusOptions.map((status) => (
- <MenuItem key={status} value={status}>
- {t(status)}
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- </Grid>
- <Grid item xs={12} md={6}>
- <TextField
- fullWidth
- label={t("Area")}
- value={searchFilters.area}
- onChange={(e) =>
- onSearchFiltersChange({
- ...searchFilters,
- area: e.target.value,
- })
- }
- />
- </Grid>
- <Grid item xs={12} md={6}>
- <FormControl fullWidth>
- <InputLabel>{t("Store ID")}</InputLabel>
- <Select
- value={searchFilters.storeId}
- label={t("Store ID")}
- onChange={(e) =>
- onSearchFiltersChange({
- ...searchFilters,
- storeId: e.target.value,
- })
- }
- >
- <MenuItem value="All">{t("All")}</MenuItem>
- {storeIdOptions.map((storeId) => (
- <MenuItem key={storeId} value={storeId}>
- {storeId}
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- </Grid>
- </Grid>
- <CardActions sx={{ px: 0, pt: 2, gap: 1 }}>
- <Button variant="outlined" onClick={handleResetSearch}>
- {t("Reset")}
- </Button>
- <Button variant="contained" onClick={handleSearch}>
- {t("Search")}
- </Button>
- </CardActions>
- </CardContent>
- </Card>
- <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
-
-
- <Typography variant="body2" color="text.secondary">
- {t("Total Sections")}: {total}
- </Typography>
- <Typography variant="body2" color="text.secondary">
- {t("Start Stock Take Date")}: {planStartDate || "-"}
- </Typography>
-
- <Button
- variant="contained"
- color="primary"
- onClick={() => setOpenConfirmDialog(true)}
- disabled={creating || !canManageStockTake}
- >
- {creating ? <CircularProgress size={20} /> : t("Create Stock Take (Select Sections)")}
- </Button>
- </Box>
-
- {loading ? (
- <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
- <CircularProgress />
- </Box>
- ) : (
- <Grid container spacing={2}>
- {stockTakeSessions.map((session: AllPickedStockTakeListReponse) => {
- const statusColor = getStatusColor(session.status || "");
- const lastStockTakeDate = session.lastStockTakeDate
- ? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT)
- : "-";
- const completionRate = getCompletionRate(session);
- const sectionMeta = [
- session.stockTakeSectionDescription,
- session.warehouseArea,
- session.storeId,
- ].filter((v): v is string => Boolean(v && v.trim())).join(" / ");
-
- return (
- <Grid key={session.id} item xs={12} sm={6} md={4}>
- <Card
- sx={{
- minHeight: 200,
- display: "flex",
- flexDirection: "column",
- border: "1px solid",
- borderColor: statusColor === "success" ? "success.main" : "primary.main",
- }}
- >
- <CardContent sx={{ pb: 1, flexGrow: 1 }}>
- <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
- <Typography variant="subtitle1" fontWeight={600}>
- {t("Section")}: {session.stockTakeSession}
- {sectionMeta ? ` (${sectionMeta})` : null}
- </Typography>
-
- </Stack>
-
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
- {t("Last Stock Take Date")}: {lastStockTakeDate || "-"}
- </Typography>
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Stock Taker")}: {session.stockTakerName}</Typography>
- <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
-
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("start time")}: {startTimeDisplay(session.startTime) || "-"}</Typography>
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("end time")}: {endTimeDisplay(session.endTime) || "-"}</Typography>
- </Stack>
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
- {t("Control Time")}: <TimeDisplay startTime={session.startTime} endTime={session.endTime} />
- </Typography>
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>{t("Total Item Kind Number")}: {session.totalItemNumber}</Typography>
-
- </CardContent>
-
- <CardActions sx={{ pt: 0.5 ,justifyContent: "space-between"}}>
- <Stack direction="row" spacing={1}>
- <Button
- variant="contained"
- size="small"
- onClick={() => onCardClick(session)}
- >
- {t("View Details")}
- </Button>
- <Button
- variant="contained"
- size="small"
- onClick={() => onReStockTakeClick(session)}
- disabled={!session.reStockTakeTrueFalse}
- >
- {t("View ReStockTake")}
- </Button>
- </Stack>
- <Chip size="small" label={t(session.status || "")} color={statusColor as any} />
- </CardActions>
- </Card>
- </Grid>
- );
- })}
- </Grid>
- )}
-
- {total > 0 && (
- <TablePagination
- component="div"
- count={total}
- page={page}
- rowsPerPage={pageSize}
- onPageChange={(e, newPage) => {
- onListPageChange(newPage);
- }}
- rowsPerPageOptions={[pageSize]} // 如果暂时不让用户改 pageSize,就写死
- />
- )}
- {/* Create Stock Take 確認 Dialog */}
- <Dialog
- open={openConfirmDialog}
- onClose={() => {
- setOpenConfirmDialog(false);
- setCreateDialogRoundName("");
- setCreateDialogPlanStart(dayjs());
- }}
- maxWidth={false}
- fullWidth
- PaperProps={{
- sx: {
- width: "100%",
- maxWidth: { xs: "100%", sm: 960, md: 1120 },
- maxHeight: "90vh",
- display: "flex",
- flexDirection: "column",
- },
- }}
- >
- <DialogTitle
- sx={{
- pb: 1,
- flexShrink: 0,
- display: "flex",
- alignItems: "center",
- justifyContent: "space-between",
- gap: 1,
- }}
- >
- <Typography component="span" variant="h6" sx={{ fontSize: "1.25rem", fontWeight: 500 }}>
- {t("Create Stock Take (Select Sections)")}
- </Typography>
- <Tooltip
- title={
- missingSectionCount > 0
- ? t("Warehouse missing stock take section tooltip has", { count: missingSectionCount })
- : t("Warehouse missing stock take section tooltip none")
- }
- >
- <Box component="span" sx={{ flexShrink: 0, display: "inline-flex" }}>
- <IconButton
- size="small"
- aria-label={t("Warehouse missing stock take section warn title")}
- onClick={() => setMissingSectionWarnDrawerOpen(true)}
- sx={{
- color: missingSectionCount > 0 ? "warning.main" : "text.secondary",
- }}
- >
- <Badge
- color="error"
- badgeContent={missingSectionCount > 99 ? "99+" : missingSectionCount}
- invisible={missingSectionCount === 0}
- >
- <NotificationsNoneOutlinedIcon />
- </Badge>
- </IconButton>
- </Box>
- </Tooltip>
- </DialogTitle>
- <DialogContent
- dividers
- sx={{
- p: 0,
- flex: 1,
- minHeight: 0,
- overflow: "hidden",
- display: "flex",
- flexDirection: "column",
- }}
- >
- <Box
- sx={{
- display: "flex",
- flex: 1,
- minHeight: 0,
- flexDirection: { xs: "column", md: "row" },
- height: "100%",
- }}
- >
- {/* 左側:盤點設定與樓層 */}
- <Box
- sx={{
- width: { xs: "100%", md: 280 },
- flexShrink: 0,
- minHeight: 0,
- maxHeight: { xs: "38vh", md: "100%" },
- overflowY: "auto",
- borderRight: { md: 1 },
- borderBottom: { xs: 1, md: 0 },
- borderColor: "divider",
- p: 2.5,
- display: "flex",
- flexDirection: "column",
- gap: 2,
- bgcolor: "grey.50",
- }}
- >
- <Box>
- <Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 0.5 }}>
- {t("Stock take round name")}
-
- </Typography>
- <TextField
- fullWidth
- size="small"
- hiddenLabel
- // placeholder={t("Stock take round name placeholder")}
- value={createDialogRoundName}
- onChange={(e) => setCreateDialogRoundName(e.target.value)}
- inputProps={{ maxLength: 255, "aria-label": t("Stock take round name") }}
- sx={createDialogCompactInputSx}
- />
- </Box>
- <Box>
- <Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 0.5 }}>
- {t("Creation date")}
- </Typography>
- <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk">
- <DatePicker
- value={createDialogPlanStart}
- onChange={(newValue) => setCreateDialogPlanStart(newValue)}
- format={OUTPUT_DATE_FORMAT}
- slotProps={{
- textField: {
- fullWidth: true,
- size: "small",
- hiddenLabel: true,
- inputProps: { "aria-label": t("Creation date") },
- sx: createDialogCompactInputSx,
- },
- }}
- />
- </LocalizationProvider>
- </Box>
- {sortedFloorKeys.length === 0 && !sectionsLoading ? (
- <Typography variant="body2" color="text.secondary">
- {t("No stock take sections from warehouse")}
- </Typography>
- ) : (
- <Stack
- spacing={1}
- sx={{
- flexShrink: 0,
- ...(sortedFloorKeys.length > 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 (
- <Button
- key={floorKey}
- fullWidth
- variant="outlined"
- onClick={() => {
- setCreateFloorTabIndex(idx);
- setCreateDialogSearchQuery("");
- }}
- sx={{
- justifyContent: "space-between",
- textTransform: "none",
- py: 1.25,
- px: 1.5,
- borderRadius: 2,
- borderWidth: selected ? 2 : 1,
- borderColor: selected ? "primary.main" : "divider",
- color: selected ? "primary.main" : "text.primary",
- bgcolor: "background.paper",
- "&:hover": {
- borderColor: selected ? "primary.main" : "divider",
- bgcolor: "background.paper",
- },
- }}
- >
- <Typography variant="body2" fontWeight={selected ? 700 : 500}>
- {floorGroupLabel(floorKey)}
- </Typography>
- <Chip
- size="small"
- label={floorSections.length}
- sx={{
- height: 22,
- minWidth: 28,
- bgcolor: selected ? "primary.main" : "grey.200",
- color: selected ? "primary.contrastText" : "text.secondary",
- }}
- />
- </Button>
- );
- })}
- </Stack>
- )}
- <Stack spacing={1}>
- <Button
- fullWidth
- variant="outlined"
- onClick={() => setCreateDialogSelectedSections([...allSectionsForCreateDialog])}
- disabled={allSectionsForCreateDialog.length === 0}
- sx={{ bgcolor: "background.paper" }}
- >
- {t("Select all sections all floors")}
- </Button>
- <Button
- fullWidth
- variant="outlined"
- onClick={() => setCreateDialogSelectedSections([])}
- sx={{ bgcolor: "background.paper" }}
- >
- {t("Clear selection all floors")}
- </Button>
- </Stack>
- <Typography variant="body2" color="text.secondary">
- {t("Total selected sections label")}{" "}
- <Typography component="span" variant="body2" color="primary.main" fontWeight={700}>
- {createDialogSelectedSections.length}
- </Typography>{" "}
- {t("sections unit")}
- </Typography>
- </Box>
-
- {/* 右側:區域卡片網格 */}
- <Box
- sx={{
- flex: 1,
- minHeight: 0,
- display: "flex",
- flexDirection: "column",
- minWidth: 0,
- p: 2.5,
- bgcolor: "background.paper",
- overflow: "hidden",
- }}
- >
- {activeCreateFloorKey != null ? (
- <>
- <Stack
- direction="row"
- alignItems="center"
- justifyContent="space-between"
- flexWrap="wrap"
- gap={1}
- sx={{ mb: 1 }}
- >
- <Typography variant="subtitle1" fontWeight={700}>
- {t("Floor area selection header", {
- floor: floorGroupLabel(activeCreateFloorKey),
- count: activeCreateFloorSections.length,
- })}
- </Typography>
- <Button
- size="small"
- variant="outlined"
- onClick={() => toggleFloorSelectAll(activeCreateFloorKey)}
- disabled={activeCreateFloorSections.length === 0}
- >
- {activeCreateFloorSections.length > 0 &&
- activeCreateFloorSections.every((s) => createDialogSelectedSections.includes(s))
- ? t("Deselect all on this floor", {
- floor: floorGroupLabel(activeCreateFloorKey),
- })
- : t("Select all on this floor", {
- floor: floorGroupLabel(activeCreateFloorKey),
- })}
- </Button>
- </Stack>
- <TextField
- fullWidth
- size="small"
- hiddenLabel
- placeholder={t("Search section code or name")}
- value={createDialogSearchQuery}
- onChange={(e) => setCreateDialogSearchQuery(e.target.value)}
- inputProps={{ "aria-label": t("Search section code or name") }}
- InputProps={{
- startAdornment: (
- <InputAdornment position="start">
- <SearchIcon fontSize="small" color="action" />
- </InputAdornment>
- ),
- }}
- sx={createDialogCompactInputSx}
- />
- <Box
- ref={createSectionGridScrollRef}
- onPointerDown={handleCreateSectionGridPointerDown}
- sx={{
- position: "relative",
- flex: 1,
- minHeight: 0,
- mt: 2,
- overflowY: "auto",
- pr: 0.5,
- userSelect: marqueeRect ? "none" : "auto",
- touchAction: "pan-y",
- cursor: "default",
- }}
- >
- {sectionsLoading ? (
- <Box
- sx={{
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- minHeight: 240,
- }}
- >
- <CircularProgress />
- </Box>
- ) : createDialogFilteredSections.length === 0 ? (
- <Typography variant="body2" color="text.secondary" sx={{ py: 4, textAlign: "center" }}>
- {createDialogSearchQuery.trim()
- ? t("No sections match search")
- : t("No stock take sections from warehouse")}
- </Typography>
- ) : (
- <>
- {marqueeRect ? (
- <Box
- aria-hidden
- sx={{
- position: "absolute",
- left: marqueeRect.left,
- top: marqueeRect.top,
- width: marqueeRect.width,
- height: marqueeRect.height,
- border: "1px solid",
- borderColor: "primary.main",
- bgcolor: "primary.main",
- opacity: 0.28,
- pointerEvents: "none",
- zIndex: 3,
- boxSizing: "border-box",
- }}
- />
- ) : null}
- <Box
- sx={{
- display: "grid",
- gridTemplateColumns: {
- xs: "repeat(2, 1fr)",
- sm: "repeat(3, 1fr)",
- md: "repeat(4, 1fr)",
- },
- gap: 1.5,
- alignItems: "start",
- //minHeight: "100%",
- }}
- >
- {createDialogFilteredSections.map((section) => {
- const meta = getCreateDialogSectionMeta(section);
- const checked = createDialogSelectedSections.includes(section);
- const previewSelected = marqueePreviewIds.has(section);
- const visuallySelected = checked || previewSelected;
- return (
- <Card
- key={section}
- data-section-card
- data-section={section}
- variant="outlined"
- ref={(el) => {
- 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",
- },
- }}
- >
- <CardContent sx={{ p: 1.5, "&:last-child": { pb: 1.5 } }}>
- <Typography variant="body2" fontWeight={700} noWrap title={section}>
- {section}
- </Typography>
- {meta ? (
- <Typography
- variant="caption"
- color="text.secondary"
- sx={{
- display: "-webkit-box",
- WebkitLineClamp: 2,
- WebkitBoxOrient: "vertical",
- overflow: "hidden",
- lineHeight: 1.35,
- mt: 0.25,
- }}
- >
- {meta}
- </Typography>
- ) : null}
- </CardContent>
- </Card>
- );
- })}
- </Box>
- </>
- )}
- </Box>
- </>
- ) : (
- <Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", flex: 1 }}>
- {sectionsLoading ? (
- <CircularProgress />
- ) : (
- <Typography variant="body2" color="text.secondary">
- {t("No stock take sections from warehouse")}
- </Typography>
- )}
- </Box>
- )}
- </Box>
- </Box>
- </DialogContent>
- <DialogActions sx={{ px: 3, py: 2, flexShrink: 0 }}>
- <Button
- onClick={() => {
- setOpenConfirmDialog(false);
- setCreateDialogRoundName("");
- setCreateDialogPlanStart(dayjs());
- }}
- >
- {t("Cancel")}
- </Button>
- <Button
- variant="contained"
- color="primary"
- onClick={handleOpenCreateStockTakeSummaryConfirm}
- disabled={
- creating ||
- createDialogSelectedSections.length === 0 ||
- createDialogPlanStart == null ||
- !createDialogPlanStart.isValid()
- }
- >
- {t("Confirm")}
- </Button>
- </DialogActions>
- </Dialog>
-
- <Dialog
- open={openCreateStockTakeSummaryConfirm}
- onClose={() => {
- if (!creating) setOpenCreateStockTakeSummaryConfirm(false);
- }}
- maxWidth="sm"
- fullWidth
- >
- <DialogTitle>{t("Confirm create stock take")}</DialogTitle>
- <DialogContent dividers>
- <Stack spacing={1.5} sx={{ mb: 2 }}>
- <Typography variant="body2">
- <Typography component="span" color="text.secondary">
- {t("Stock take round name")}:{" "}
- </Typography>
- {createDialogRoundName.trim() || t("Not filled")}
- </Typography>
- <Typography variant="body2">
- <Typography component="span" color="text.secondary">
- {t("Creation date")}:{" "}
- </Typography>
- {createDialogPlanStart?.isValid()
- ? createDialogPlanStart.format(OUTPUT_DATE_FORMAT)
- : "-"}
- </Typography>
- <Typography variant="body2" fontWeight={600}>
- {t("Total selected sections label")}{" "}
- <Typography component="span"fontWeight={600} >
- {createStockTakeSummarySections.length}
- </Typography>{" "}
- {t("sections unit")}
- </Typography>
- </Stack>
- <Box
- sx={{
- maxHeight: 280,
- overflowY: "auto",
- border: 1,
- borderColor: "divider",
- borderRadius: 1,
- p: 1.5,
- }}
- >
- <Stack spacing={1}>
- {createStockTakeSummarySections.map((section) => {
- const meta = getCreateDialogSectionMeta(section);
- return (
- <Box key={section}>
- <Typography variant="body2" fontWeight={600}>
- {section}
- </Typography>
- {meta ? (
- <Typography variant="caption" color="text.secondary">
- {meta}
- </Typography>
- ) : null}
- </Box>
- );
- })}
- </Stack>
- </Box>
- </DialogContent>
- <DialogActions sx={{ px: 3, py: 2 }}>
- <Button
- onClick={() => setOpenCreateStockTakeSummaryConfirm(false)}
- disabled={creating}
- >
- {t("Cancel")}
- </Button>
- <Button
- variant="contained"
- color="primary"
- onClick={handleCreateStockTake}
- disabled={creating || createDialogSelectedSections.length === 0}
- >
- {creating ? <CircularProgress size={20} /> : t("Confirm")}
- </Button>
- </DialogActions>
- </Dialog>
-
- <Drawer
- anchor="right"
- open={missingSectionWarnDrawerOpen}
- onClose={() => 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",
- },
- }}
- >
- <Box sx={{ p: 2, borderBottom: 1, borderColor: "divider" }}>
- <Typography variant="h6" fontWeight={700}>
- {t("Warehouse missing stock take section warn title")}
- </Typography>
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
- {t("Warehouse missing stock take section drawer hint")}
- </Typography>
- {missingSectionCount > 0 ? (
- <Typography variant="caption" color="text.secondary" display="block" sx={{ mt: 1 }}>
- {t("Warehouse missing stock take section showing", {
- shown: Math.min(missingSectionItems.length, missingSectionIssuesLimit),
- count: missingSectionCount,
- })}
- </Typography>
- ) : null}
- </Box>
- <Box sx={{ flex: 1, overflowY: "auto", px: 2, py: 1 }}>
- {missingSectionIssuesLoading ? (
- <Box sx={{ display: "flex", justifyContent: "center", py: 4 }}>
- <CircularProgress size={28} />
- </Box>
- ) : missingSectionItems.length === 0 ? (
- <Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
- {t("Warehouse missing stock take section empty")}
- </Typography>
- ) : (
- <Stack spacing={1.5} divider={<Divider flexItem />}>
- {missingSectionItems.map((row) => (
- <Box key={row.id}>
- <Typography variant="body2" fontWeight={600}>
- {row.code || `#${row.id}`}
- </Typography>
- <Typography variant="caption" color="text.secondary" display="block">
- {[row.storeId, row.warehouse, row.area, row.slot].filter(Boolean).join(" / ") ||
- "—"}
- </Typography>
- {row.order ? (
- <Typography variant="caption" color="text.secondary" display="block">
- {row.order}
- </Typography>
- ) : null}
- </Box>
- ))}
- </Stack>
- )}
- </Box>
- <Box
- sx={{
- p: 2,
- borderTop: 1,
- borderColor: "divider",
- display: "flex",
- gap: 1,
- flexWrap: "wrap",
- }}
- >
- <Button variant="outlined" onClick={() => setMissingSectionWarnDrawerOpen(false)}>
- {t("Cancel")}
- </Button>
- <Button
- variant="contained"
- color="primary"
- onClick={handleGoWarehouseSettings}
- disabled={missingSectionCount === 0}
- >
- {t("Warehouse missing stock take section go settings")}
- </Button>
- </Box>
- </Drawer>
- </Box>
- );
- };
-
- export default PickerCardList;
|