Files
star-erp/resources/js/Pages/Warehouse/Inventory.tsx

238 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useMemo } from "react";
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) {
const [searchTerm, setSearchTerm] = useState("");
const [typeFilter, setTypeFilter] = useState<string>("all");
const [deleteId, setDeleteId] = useState<string | null>(null);
const [importDialogOpen, setImportDialogOpen] = useState(false);
// 篩選庫存列表
const filteredInventories = useMemo(() => {
return inventories.filter((group) => {
// 搜尋條件:匹配商品名稱、編號 或 該商品下任一批號
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]);
// 計算統計資訊
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 (
<AuthenticatedLayout breadcrumbs={getInventoryBreadcrumbs(warehouse.id, warehouse.name)}>
<Head title={`庫存管理 - ${warehouse.name}`} />
<div className="container mx-auto p-6 max-w-7xl">
{/* 頁面標題與導航 */}
<div className="mb-6">
<Link href="/warehouses">
<Button
variant="outline"
className="gap-2 button-outlined-primary mb-6"
>
<ArrowLeft className="h-4 w-4" />
</Button>
</Link>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
<Boxes className="h-6 w-6 text-primary-main" />
- {warehouse.name}
</h1>
<p className="text-gray-500 mt-1"></p>
</div>
</div>
</div>
{/* 操作按鈕 (位於標題下方) */}
<div className="flex items-center gap-3 mb-6">
{/* 安全庫存設定按鈕 */}
<Can permission="inventory.safety_stock">
<Link href={route('warehouses.safety-stock.index', warehouse.id)}>
<Button
variant="outline"
className="button-outlined-primary"
>
<Shield className="mr-2 h-4 w-4" />
</Button>
</Link>
</Can>
{/* 庫存警告顯示 */}
<Button
variant="outline"
className={`button-outlined-primary cursor-default hover:bg-transparent ${lowStockItems > 0
? "border-orange-500 text-orange-600"
: "border-green-500 text-green-600"
}`}
>
<AlertTriangle className="mr-2 h-4 w-4" />
{lowStockItems}
</Button>
{/* 匯入入庫按鈕 */}
<Can permission="inventory.adjust">
<Button
variant="outline"
className="button-outlined-primary"
onClick={() => setImportDialogOpen(true)}
>
<FileUp className="mr-2 h-4 w-4" />
</Button>
</Can>
{/* 新增庫存按鈕 */}
<Can permission="inventory.adjust">
<Link href={route('warehouses.inventory.create', warehouse.id)}>
<Button
className="button-filled-primary"
>
<PackagePlus className="mr-2 h-4 w-4" />
</Button>
</Link>
</Can>
</div>
{/* 篩選工具列 */}
<div className="mb-6">
<InventoryToolbar
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
typeFilter={typeFilter}
onTypeFilterChange={setTypeFilter}
/>
</div>
{/* 庫存表格 */}
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
<InventoryTable
inventories={filteredInventories}
onView={handleView}
onDelete={confirmDelete}
onViewProduct={handleViewProduct}
/>
</div>
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="button-outlined-primary"></AlertDialogCancel>
<AlertDialogAction
onClick={() => {
handleDelete();
}}
className="button-filled-error"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 匯入對話框 */}
<InventoryImportDialog
open={importDialogOpen}
onOpenChange={setImportDialogOpen}
warehouseId={warehouse.id}
/>
</div>
</AuthenticatedLayout>
);
}