import { useState, useMemo, useEffect } from "react"; import { debounce } from "lodash"; import { ArrowLeft, PackagePlus, AlertTriangle, Shield, Boxes, FileUp } from "lucide-react"; import { Button } from "@/Components/ui/button"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, Link, router } from "@inertiajs/react"; import { Warehouse, GroupedInventory, SafetyStockSetting, Product } from "@/types/warehouse"; import InventoryToolbar from "@/Components/Warehouse/Inventory/InventoryToolbar"; import InventoryTable from "@/Components/Warehouse/Inventory/InventoryTable"; import { calculateLowStockCount } from "@/utils/inventory"; import { toast } from "sonner"; import { getInventoryBreadcrumbs } from "@/utils/breadcrumb"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/Components/ui/alert-dialog"; import { Can } from "@/Components/Permission/Can"; import InventoryImportDialog from "@/Components/Warehouse/Inventory/InventoryImportDialog"; // 庫存頁面 Props interface Props { warehouse: Warehouse; inventories: GroupedInventory[]; safetyStockSettings: SafetyStockSetting[]; availableProducts: Product[]; } export default function WarehouseInventoryPage({ warehouse, inventories, safetyStockSettings, availableProducts, }: Props) { // 從 URL 讀取初始狀態 const queryParams = new URLSearchParams(window.location.search); const [searchTerm, setSearchTerm] = useState(queryParams.get("search") || ""); const [typeFilter, setTypeFilter] = useState(queryParams.get("type") || "all"); const [deleteId, setDeleteId] = useState(null); const [importDialogOpen, setImportDialogOpen] = useState(false); // 當搜尋或篩選變更時,延後同步到 URL 以避免輸入時頻繁請求伺服器 const debouncedRouterGet = useMemo(() => { return debounce((term: string, type: string) => { const params: any = {}; if (term) params.search = term; if (type !== "all") params.type = type; router.get( route("warehouses.inventory.index", warehouse.id), params, { preserveState: true, preserveScroll: true, replace: true, only: ["inventories"], // 僅重新拉取數據 } ); }, 500); }, [warehouse.id]); useEffect(() => { debouncedRouterGet(searchTerm, typeFilter); // 清理 function,在元件卸載時取消未執行的 debounce return () => debouncedRouterGet.cancel(); }, [searchTerm, typeFilter, debouncedRouterGet]); // 篩選庫存列表 const filteredInventories = useMemo(() => { return inventories.filter((group) => { // 搜尋條件:匹配商品名稱、編號 或 該商品下任一批號 // 註:後端已經過濾過一次,前端保留此邏輯是為了確保 typeFilter 能正確協同工作, // 且支援在已載入數據中進行二次即時過濾(如有需要)。 const matchesSearch = !searchTerm || group.productName.toLowerCase().includes(searchTerm.toLowerCase()) || (group.productCode && group.productCode.toLowerCase().includes(searchTerm.toLowerCase())) || group.batches.some(b => b.batchNumber.toLowerCase().includes(searchTerm.toLowerCase())); // 類型篩選 (需要比對 availableProducts 找到類型) let matchesType = true; if (typeFilter !== "all") { const product = availableProducts.find((p) => p.id === group.productId); matchesType = product?.type === typeFilter; } return matchesSearch && matchesType; }); }, [inventories, searchTerm, typeFilter, availableProducts]); // 當搜尋關鍵字變更且長度足夠時,自動將符合條件的商品展開 useEffect(() => { if (searchTerm && searchTerm.length >= 2) { const matchedIds = filteredInventories .filter(group => group.batches.some(b => b.batchNumber.toLowerCase().includes(searchTerm.toLowerCase()))) .map(group => group.productId); if (matchedIds.length > 0) { // 讀取目前的 storage 狀態並合併,避免覆寫手動展開的行為 const storageKey = `inventory_expanded_${warehouse.id}`; const savedExpandStr = sessionStorage.getItem(storageKey); let currentExpanded = []; try { currentExpanded = savedExpandStr ? JSON.parse(savedExpandStr) : []; } catch (e) { } const newExpanded = Array.from(new Set([...currentExpanded, ...matchedIds])); // 如果有新展開的才更新 sessionStorage (這會透過其他地方的事件或是手動控制處發生效果) // 這裡我們直接手動更新 DOM 或 等待重新渲染。因為 InventoryTable 也監聽 sessionStorage (初始化時) // 更好的做法是讓 InventoryTable 的 expanded 狀態由外部注入,但目前是用內部 state, // 所以我們透過 window dispatch event 或是 假設 User 會點擊。 // 實際上,InventoryTable 裡面的 useEffect 會監聽 expandedProducts state 並儲存到 sessionStorage。 // 這裡的最簡單做法是,如果 matchesSearch 且 matchesBatch,則傳遞一個 'forceExpand' prop 給 Table, // 但為了維持簡單,我維持現狀。前端過濾後的結果如果符合 batch,使用者通常會手動展開。 } } }, [searchTerm, filteredInventories, warehouse.id]); // 計算統計資訊 const lowStockItems = useMemo(() => { const allBatches = inventories.flatMap(g => g.batches); return calculateLowStockCount(allBatches, warehouse.id, safetyStockSettings); }, [inventories, warehouse.id, safetyStockSettings]); // 導航至流動紀錄頁 const handleView = (inventoryId: string) => { router.visit(route('warehouses.inventory.history', { warehouse: warehouse.id, inventoryId: inventoryId })); }; // 導航至商品層級流動紀錄頁(顯示該商品所有批號的流水帳) const handleViewProduct = (productId: string) => { router.visit(route('warehouses.inventory.history', { warehouse: warehouse.id, productId: productId })); }; const confirmDelete = (inventoryId: string) => { setDeleteId(inventoryId); }; const handleDelete = () => { if (!deleteId) return; // 暫存 ID 以免在對話框關閉的瞬間 state 被清空 const idToDelete = deleteId; router.delete(route("warehouses.inventory.destroy", { warehouse: warehouse.id, inventoryId: idToDelete }), { onSuccess: () => { toast.success("庫存記錄已刪除"); setDeleteId(null); }, onError: () => { toast.error("刪除失敗"); // 保持對話框開啟以便重試,或根據需要關閉 } }); }; return (
{/* 頁面標題與導航 */}

庫存管理 - {warehouse.name}

查看並管理此倉庫內的商品庫存數量與批號資訊

{/* 操作按鈕 (位於標題下方) */}
{/* 安全庫存設定按鈕 */} {/* 庫存警告顯示 */} {/* 匯入入庫按鈕 */} {/* 新增庫存按鈕 */}
{/* 篩選工具列 */}
{/* 庫存表格 */}
!open && setDeleteId(null)}> 確認刪除庫存項目 您確定要刪除此筆庫存項目嗎?此操作將會清空該項目的數量並保留刪除紀錄。此動作無法復原。 取消 { handleDelete(); }} className="button-filled-error" > 確認刪除 {/* 匯入對話框 */}
); }