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

217 lines
9.0 KiB
TypeScript
Raw Normal View History

2025-12-30 15:03:19 +08:00
import { useState, useMemo } from "react";
import { ArrowLeft, PackagePlus, AlertTriangle, Shield, Boxes } from "lucide-react";
2025-12-30 15:03:19 +08:00
import { Button } from "@/Components/ui/button";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, Link, router } from "@inertiajs/react";
2026-01-22 15:39:35 +08:00
import { Warehouse, GroupedInventory, SafetyStockSetting, Product } from "@/types/warehouse";
2025-12-30 15:03:19 +08:00
import InventoryToolbar from "@/Components/Warehouse/Inventory/InventoryToolbar";
import InventoryTable from "@/Components/Warehouse/Inventory/InventoryTable";
import { calculateLowStockCount } from "@/utils/inventory";
import { toast } from "sonner";
2026-01-07 13:06:49 +08:00
import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
2025-12-30 15:03:19 +08:00
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/Components/ui/alert-dialog";
import { Can } from "@/Components/Permission/Can";
2025-12-30 15:03:19 +08:00
// 庫存頁面 Props
interface Props {
warehouse: Warehouse;
2026-01-22 15:39:35 +08:00
inventories: GroupedInventory[];
2025-12-30 15:03:19 +08:00
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 filteredInventories = useMemo(() => {
2026-01-22 15:39:35 +08:00
return inventories.filter((group) => {
// 搜尋條件:匹配商品名稱、編號 或 該商品下任一批號
2025-12-30 15:03:19 +08:00
const matchesSearch = !searchTerm ||
2026-01-22 15:39:35 +08:00
group.productName.toLowerCase().includes(searchTerm.toLowerCase()) ||
(group.productCode && group.productCode.toLowerCase().includes(searchTerm.toLowerCase())) ||
group.batches.some(b => b.batchNumber.toLowerCase().includes(searchTerm.toLowerCase()));
2025-12-30 15:03:19 +08:00
// 類型篩選 (需要比對 availableProducts 找到類型)
let matchesType = true;
if (typeFilter !== "all") {
2026-01-22 15:39:35 +08:00
const product = availableProducts.find((p) => p.id === group.productId);
2025-12-30 15:03:19 +08:00
matchesType = product?.type === typeFilter;
}
return matchesSearch && matchesType;
});
}, [inventories, searchTerm, typeFilter, availableProducts]);
// 計算統計資訊
2026-01-22 15:39:35 +08:00
const lowStockItems = useMemo(() => {
const allBatches = inventories.flatMap(g => g.batches);
return calculateLowStockCount(allBatches, warehouse.id, safetyStockSettings);
}, [inventories, warehouse.id, safetyStockSettings]);
2025-12-30 15:03:19 +08:00
// 導航至流動紀錄頁
const handleView = (inventoryId: string) => {
2026-01-08 16:32:10 +08:00
router.visit(route('warehouses.inventory.history', { warehouse: warehouse.id, inventoryId: inventoryId }));
2025-12-30 15:03:19 +08:00
};
2026-01-22 15:39:35 +08:00
// 導航至商品層級流動紀錄頁(顯示該商品所有批號的流水帳)
const handleViewProduct = (productId: string) => {
router.visit(route('warehouses.inventory.history', {
warehouse: warehouse.id,
productId: productId
}));
};
2025-12-30 15:03:19 +08:00
const confirmDelete = (inventoryId: string) => {
setDeleteId(inventoryId);
};
const handleDelete = () => {
if (!deleteId) return;
2026-01-08 16:32:10 +08:00
// 暫存 ID 以免在對話框關閉的瞬間 state 被清空
const idToDelete = deleteId;
router.delete(route("warehouses.inventory.destroy", { warehouse: warehouse.id, inventoryId: idToDelete }), {
2025-12-30 15:03:19 +08:00
onSuccess: () => {
toast.success("庫存記錄已刪除");
setDeleteId(null);
},
onError: () => {
toast.error("刪除失敗");
2026-01-08 16:32:10 +08:00
// 保持對話框開啟以便重試,或根據需要關閉
2025-12-30 15:03:19 +08:00
}
});
};
2026-01-22 15:39:35 +08:00
2025-12-30 15:03:19 +08:00
return (
2026-01-07 13:06:49 +08:00
<AuthenticatedLayout breadcrumbs={getInventoryBreadcrumbs(warehouse.id, warehouse.name)}>
2025-12-30 15:03:19 +08:00
<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>
2025-12-30 15:03:19 +08:00
</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>
2025-12-30 15:03:19 +08:00
{/* 庫存警告顯示 */}
<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">
<Link href={route('warehouses.inventory.create', warehouse.id)}>
<Button
className="button-filled-primary"
>
<PackagePlus className="mr-2 h-4 w-4" />
</Button>
</Link>
</Can>
2025-12-30 15:03:19 +08:00
</div>
{/* 篩選工具列 */}
2026-01-22 15:39:35 +08:00
<div className="mb-6">
2025-12-30 15:03:19 +08:00
<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}
2026-01-22 15:39:35 +08:00
onViewProduct={handleViewProduct}
2025-12-30 15:03:19 +08:00
/>
</div>
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="button-outlined-primary"></AlertDialogCancel>
2026-01-08 16:32:10 +08:00
<AlertDialogAction
onClick={() => {
2026-01-08 16:32:10 +08:00
handleDelete();
}}
className="button-filled-error"
2026-01-08 16:32:10 +08:00
>
2025-12-30 15:03:19 +08:00
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</AuthenticatedLayout>
);
}