生產工單BOM以及批號完善
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 57s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

This commit is contained in:
2026-01-22 15:39:35 +08:00
parent 1ae21febb5
commit 1d134c9ad8
31 changed files with 2684 additions and 694 deletions

View File

@@ -3,7 +3,7 @@ import { ArrowLeft, PackagePlus, AlertTriangle, Shield, Boxes } from "lucide-rea
import { Button } from "@/Components/ui/button";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, Link, router } from "@inertiajs/react";
import { Warehouse, WarehouseInventory, SafetyStockSetting, Product } from "@/types/warehouse";
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";
@@ -24,7 +24,7 @@ import { Can } from "@/Components/Permission/Can";
// 庫存頁面 Props
interface Props {
warehouse: Warehouse;
inventories: WarehouseInventory[];
inventories: GroupedInventory[];
safetyStockSettings: SafetyStockSetting[];
availableProducts: Product[];
}
@@ -41,17 +41,17 @@ export default function WarehouseInventoryPage({
// 篩選庫存列表
const filteredInventories = useMemo(() => {
return inventories.filter((item) => {
// 搜尋條件:匹配商品名稱、編號批號
return inventories.filter((group) => {
// 搜尋條件:匹配商品名稱、編號 或 該商品下任一批號
const matchesSearch = !searchTerm ||
item.productName.toLowerCase().includes(searchTerm.toLowerCase()) ||
(item.productCode && item.productCode.toLowerCase().includes(searchTerm.toLowerCase())) ||
item.batchNumber.toLowerCase().includes(searchTerm.toLowerCase());
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 === item.productId);
const product = availableProducts.find((p) => p.id === group.productId);
matchesType = product?.type === typeFilter;
}
@@ -60,13 +60,24 @@ export default function WarehouseInventoryPage({
}, [inventories, searchTerm, typeFilter, availableProducts]);
// 計算統計資訊
const lowStockItems = calculateLowStockCount(inventories, warehouse.id, safetyStockSettings);
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);
@@ -90,6 +101,17 @@ export default function WarehouseInventoryPage({
});
};
const handleAdjust = (batchId: string, data: { operation: string; quantity: number; reason: string }) => {
router.put(route("warehouses.inventory.update", { warehouse: warehouse.id, inventoryId: batchId }), data, {
onSuccess: () => {
toast.success("庫存已更新");
},
onError: () => {
toast.error("庫存更新失敗");
}
});
};
return (
<AuthenticatedLayout breadcrumbs={getInventoryBreadcrumbs(warehouse.id, warehouse.name)}>
<Head title={`庫存管理 - ${warehouse.name}`} />
@@ -158,7 +180,7 @@ export default function WarehouseInventoryPage({
</div>
{/* 篩選工具列 */}
<div className="mb-6 bg-white rounded-lg shadow-sm border p-4">
<div className="mb-6">
<InventoryToolbar
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
@@ -173,6 +195,8 @@ export default function WarehouseInventoryPage({
inventories={filteredInventories}
onView={handleView}
onDelete={confirmDelete}
onAdjust={handleAdjust}
onViewProduct={handleViewProduct}
/>
</div>
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>