Files
star-erp/source-code/ERP(A-b)-倉庫管理/src/App.tsx
2025-12-30 15:03:19 +08:00

723 lines
22 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 } from "react";
import NavigationSidebar from "./components/NavigationSidebar";
import WarehouseManagement from "./components/WarehouseManagement";
import WarehouseInventoryPage from "./components/WarehouseInventoryPage";
import AddInventoryPage from "./components/AddInventoryPage";
import EditInventoryPage from "./components/EditInventoryPage";
import SafetyStockPage from "./components/SafetyStockPage";
import { Toaster } from "./components/ui/sonner";
import { WarehouseInventory, Warehouse, InventoryTransaction, InboundItem, InboundReason, SafetyStockSetting } from "./types/warehouse";
import { toast } from "sonner@2.0.3";
import { generateId } from "./utils/format";
interface PageState {
page: string;
params?: {
warehouseId?: string;
inventoryId?: string;
};
}
export default function App() {
const [currentPage, setCurrentPage] = useState<PageState>({
page: "warehouse-management",
});
// Shared inventory state
const [inventories, setInventories] = useState<WarehouseInventory[]>([
// ===== 中央倉id=1- 原物料庫存 =====
{
warehouseId: "1",
productId: "101",
productName: "二砂糖",
quantity: 50,
batchNumber: "SG20251201",
expiryDate: "2026-12-01",
lastInboundDate: "2025-12-01",
lastOutboundDate: "2025-12-10",
},
{
warehouseId: "1",
productId: "101",
productName: "二砂糖",
quantity: 45,
batchNumber: "SG20251115",
expiryDate: "2026-11-15",
lastInboundDate: "2025-11-15",
lastOutboundDate: "2025-12-08",
},
{
warehouseId: "1",
productId: "102",
productName: "黑糖",
quantity: 30,
batchNumber: "BS20251205",
expiryDate: "2026-06-05",
lastInboundDate: "2025-12-05",
lastOutboundDate: "2025-12-09",
},
{
warehouseId: "1",
productId: "103",
productName: "冰糖",
quantity: 25,
batchNumber: "RS20251203",
expiryDate: "2026-12-03",
lastInboundDate: "2025-12-03",
lastOutboundDate: "2025-12-07",
},
{
warehouseId: "1",
productId: "104",
productName: "奶精粉",
quantity: 18,
batchNumber: "CR20251208",
expiryDate: "2026-03-08",
lastInboundDate: "2025-12-08",
lastOutboundDate: "2025-12-10",
},
{
warehouseId: "1",
productId: "105",
productName: "鮮奶油",
quantity: 12,
batchNumber: "FC20251210",
expiryDate: "2025-12-25",
lastInboundDate: "2025-12-10",
lastOutboundDate: "2025-12-11",
},
{
warehouseId: "1",
productId: "106",
productName: "紅豆(生)",
quantity: 40,
batchNumber: "RBR20251202",
expiryDate: "2026-06-02",
lastInboundDate: "2025-12-02",
lastOutboundDate: "2025-12-09",
},
{
warehouseId: "1",
productId: "107",
productName: "綠豆(生)",
quantity: 35,
batchNumber: "GBR20251204",
expiryDate: "2026-06-04",
lastInboundDate: "2025-12-04",
lastOutboundDate: "2025-12-10",
},
{
warehouseId: "1",
productId: "108",
productName: "芋頭(生)",
quantity: 28,
batchNumber: "TR20251206",
expiryDate: "2026-01-06",
lastInboundDate: "2025-12-06",
lastOutboundDate: "2025-12-09",
},
{
warehouseId: "1",
productId: "109",
productName: "地瓜(生)",
quantity: 32,
batchNumber: "SPR20251207",
expiryDate: "2026-01-07",
lastInboundDate: "2025-12-07",
lastOutboundDate: "2025-12-10",
},
{
warehouseId: "1",
productId: "110",
productName: "仙草乾",
quantity: 20,
batchNumber: "GD20251205",
expiryDate: "2026-12-05",
lastInboundDate: "2025-12-05",
lastOutboundDate: "2025-12-08",
},
// ===== 中央倉id=1- 半成品庫存 =====
{
warehouseId: "1",
productId: "1",
productName: "粉粿原漿",
quantity: 25,
batchNumber: "PG20251201",
expiryDate: "2025-12-15",
lastInboundDate: "2025-12-01",
lastOutboundDate: "2025-12-08",
},
{
warehouseId: "1",
productId: "1",
productName: "粉粿原漿",
quantity: 30,
batchNumber: "PG20251128",
expiryDate: "2025-12-12",
lastInboundDate: "2025-11-28",
lastOutboundDate: "2025-12-06",
},
{
warehouseId: "1",
productId: "2",
productName: "黑糖粉圓(未加糖)",
quantity: 8,
batchNumber: "BT20251203",
expiryDate: "2025-12-10",
lastInboundDate: "2025-12-03",
lastOutboundDate: "2025-12-09",
},
{
warehouseId: "1",
productId: "2",
productName: "黑糖粉圓(未加糖)",
quantity: 15,
batchNumber: "BT20251130",
expiryDate: "2025-12-07",
lastInboundDate: "2025-11-30",
lastOutboundDate: "2025-12-05",
},
{
warehouseId: "1",
productId: "3",
productName: "熟紅豆(未加糖)",
quantity: 12,
batchNumber: "RB20251205",
expiryDate: "2025-12-12",
lastInboundDate: "2025-12-05",
lastOutboundDate: "2025-12-10",
},
{
warehouseId: "1",
productId: "3",
productName: "熟紅豆(未加糖)",
quantity: 18,
batchNumber: "RB20251202",
expiryDate: "2025-12-09",
lastInboundDate: "2025-12-02",
lastOutboundDate: "2025-12-07",
},
{
warehouseId: "1",
productId: "4",
productName: "熟綠豆(未加糖)",
quantity: 10,
batchNumber: "GB20251204",
expiryDate: "2025-12-11",
lastInboundDate: "2025-12-04",
lastOutboundDate: "2025-12-09",
},
{
warehouseId: "1",
productId: "4",
productName: "熟綠豆(未加糖)",
quantity: 14,
batchNumber: "GB20251201",
expiryDate: "2025-12-08",
lastInboundDate: "2025-12-01",
lastOutboundDate: "2025-12-06",
},
{
warehouseId: "1",
productId: "5",
productName: "芋頭泥(無調味)",
quantity: 20,
batchNumber: "TM20251206",
expiryDate: "2025-12-20",
lastInboundDate: "2025-12-06",
lastOutboundDate: "2025-12-10",
},
{
warehouseId: "1",
productId: "5",
productName: "芋頭泥(無調味)",
quantity: 25,
batchNumber: "TM20251203",
expiryDate: "2025-12-17",
lastInboundDate: "2025-12-03",
lastOutboundDate: "2025-12-08",
},
{
warehouseId: "1",
productId: "6",
productName: "地瓜泥(無調味)",
quantity: 18,
batchNumber: "SM20251207",
expiryDate: "2025-12-21",
lastInboundDate: "2025-12-07",
lastOutboundDate: "2025-12-10",
},
{
warehouseId: "1",
productId: "6",
productName: "地瓜泥(無調味)",
quantity: 22,
batchNumber: "SM20251204",
expiryDate: "2025-12-18",
lastInboundDate: "2025-12-04",
lastOutboundDate: "2025-12-09",
},
{
warehouseId: "1",
productId: "7",
productName: "仙草原凍(整塊)",
quantity: 16,
batchNumber: "GJ20251208",
expiryDate: "2025-12-22",
lastInboundDate: "2025-12-08",
lastOutboundDate: "2025-12-10",
},
{
warehouseId: "1",
productId: "7",
productName: "仙草原凍(整塊)",
quantity: 20,
batchNumber: "GJ20251205",
expiryDate: "2025-12-19",
lastInboundDate: "2025-12-05",
lastOutboundDate: "2025-12-09",
},
// ===== 門市冷藏庫id=2- 半成品庫存 =====
{
warehouseId: "2",
productId: "1",
productName: "粉粿原漿",
quantity: 8,
batchNumber: "PG20251209",
expiryDate: "2025-12-16",
lastInboundDate: "2025-12-09",
lastOutboundDate: "2025-12-11",
},
{
warehouseId: "2",
productId: "2",
productName: "黑糖粉圓(未加糖)",
quantity: 6,
batchNumber: "BT20251208",
expiryDate: "2025-12-13",
lastInboundDate: "2025-12-08",
lastOutboundDate: "2025-12-11",
},
{
warehouseId: "2",
productId: "3",
productName: "熟紅豆(未加糖)",
quantity: 5,
batchNumber: "RB20251209",
expiryDate: "2025-12-14",
lastInboundDate: "2025-12-09",
lastOutboundDate: "2025-12-11",
},
{
warehouseId: "2",
productId: "4",
productName: "熟綠豆(未加糖)",
quantity: 4,
batchNumber: "GB20251210",
expiryDate: "2025-12-15",
lastInboundDate: "2025-12-10",
lastOutboundDate: "2025-12-11",
},
{
warehouseId: "2",
productId: "5",
productName: "芋頭泥(無調味)",
quantity: 10,
batchNumber: "TM20251210",
expiryDate: "2025-12-24",
lastInboundDate: "2025-12-10",
lastOutboundDate: "2025-12-11",
},
{
warehouseId: "2",
productId: "6",
productName: "地瓜泥(無調味)",
quantity: 9,
batchNumber: "SM20251209",
expiryDate: "2025-12-23",
lastInboundDate: "2025-12-09",
lastOutboundDate: "2025-12-11",
},
{
warehouseId: "2",
productId: "7",
productName: "仙草原凍(整塊)",
quantity: 7,
batchNumber: "GJ20251210",
expiryDate: "2025-12-24",
lastInboundDate: "2025-12-10",
lastOutboundDate: "2025-12-11",
},
]);
const [warehouses] = useState([
{
id: "1",
name: "中央倉",
address: "台北市信義區信義路五段7號",
manager: "張經理",
phone: "02-1234-5678",
description: "主要原物料儲存倉庫",
createdAt: "2025-11-01",
type: "中央倉庫" as const,
},
{
id: "2",
name: "門市冷藏庫",
address: "台北市大安區敦化南路一段100號",
manager: "李主管",
phone: "02-8765-4321",
description: "門市專用冷藏倉庫",
createdAt: "2025-11-10",
type: "門市" as const,
},
]);
// Shared inventory transactions state
const [transactions, setTransactions] = useState<InventoryTransaction[]>([]);
// Safety stock settings state
const [safetyStockSettings, setSafetyStockSettings] = useState<SafetyStockSetting[]>([]);
const handleNavigate = (path: string, params?: { warehouseId?: string }) => {
setCurrentPage({ page: path, params });
};
const handleNavigateToInventory = (warehouseId: string) => {
setCurrentPage({ page: "warehouse-inventory", params: { warehouseId } });
};
const handleBackFromInventory = () => {
setCurrentPage({ page: "warehouse-management" });
};
const handleSaveInventory = (
warehouseId: string,
updatedInventories: WarehouseInventory[]
) => {
// Remove old inventories for this warehouse
const otherInventories = inventories.filter(
(inv) => inv.warehouseId !== warehouseId
);
// Add updated inventories
setInventories([...otherInventories, ...updatedInventories]);
};
const handleNavigateToEditInventory = (warehouseId: string, inventoryId: string) => {
setCurrentPage({ page: "edit-inventory", params: { warehouseId, inventoryId } });
};
const handleBackFromEditInventory = (warehouseId: string) => {
setCurrentPage({ page: "warehouse-inventory", params: { warehouseId } });
};
const handleUpdateInventory = (
warehouseId: string,
updatedInventory: WarehouseInventory & { inventoryId: string }
) => {
// 從 inventoryId 解析出索引
const inventoryIdParts = updatedInventory.inventoryId.split('-');
const index = parseInt(inventoryIdParts[inventoryIdParts.length - 1], 10);
// 找出該倉庫的所有庫存及其索引
const warehouseInventories = inventories.filter((inv) => inv.warehouseId === warehouseId);
const otherInventories = inventories.filter((inv) => inv.warehouseId !== warehouseId);
// 更新特定索引的庫存項目
const updatedWarehouseInventories = warehouseInventories.map((inv, idx) => {
return idx === index ? updatedInventory : inv;
});
setInventories([...otherInventories, ...updatedWarehouseInventories]);
handleBackFromEditInventory(warehouseId);
};
const handleDeleteInventory = (warehouseId: string, inventoryId: string) => {
// 從 inventoryId 解析出索引
const inventoryIdParts = inventoryId.split('-');
const index = parseInt(inventoryIdParts[inventoryIdParts.length - 1], 10);
// 找出該倉庫的所有庫存
const warehouseInventories = inventories.filter((inv) => inv.warehouseId === warehouseId);
const otherInventories = inventories.filter((inv) => inv.warehouseId !== warehouseId);
// 刪除特定索引的庫存項目
const updatedWarehouseInventories = warehouseInventories.filter((_, idx) => idx !== index);
setInventories([...otherInventories, ...updatedWarehouseInventories]);
handleBackFromEditInventory(warehouseId);
};
const handleNavigateToAddInventory = (warehouseId: string) => {
setCurrentPage({ page: "add-inventory", params: { warehouseId } });
};
const handleBackFromAddInventory = (warehouseId: string) => {
setCurrentPage({ page: "warehouse-inventory", params: { warehouseId } });
};
const handleNavigateToSafetyStock = (warehouseId: string) => {
setCurrentPage({ page: "safety-stock", params: { warehouseId } });
};
const handleBackFromSafetyStock = (warehouseId: string) => {
setCurrentPage({ page: "warehouse-inventory", params: { warehouseId } });
};
const handleSaveSafetyStockSettings = (
warehouseId: string,
settings: SafetyStockSetting[]
) => {
// 移除該倉庫舊的設定
const otherSettings = safetyStockSettings.filter(
(s) => s.warehouseId !== warehouseId
);
// 添加新的設定
setSafetyStockSettings([...otherSettings, ...settings]);
};
const handleSaveInbound = (
warehouseId: string,
data: {
inboundDate: string;
reason: InboundReason;
notes: string;
items: InboundItem[];
}
) => {
const now = new Date().toISOString();
const warehouse = warehouses.find((w) => w.id === warehouseId);
const warehouseName = warehouse?.name || "";
const currentInventories = inventories.filter((inv) => inv.warehouseId === warehouseId);
const updatedInventories = [...currentInventories];
// 處理每一筆入庫明細
data.items.forEach((inboundItem) => {
// 檢查是否已存在相同商品和批號的庫存
const existingIndex = updatedInventories.findIndex(
(item) =>
item.productId === inboundItem.productId &&
item.batchNumber === inboundItem.batchNumber
);
if (existingIndex !== -1) {
// 更新既有庫存數量
updatedInventories[existingIndex] = {
...updatedInventories[existingIndex],
quantity: updatedInventories[existingIndex].quantity + inboundItem.quantity,
lastInboundDate: data.inboundDate,
};
} else {
// 新增庫存項目
const newItem: WarehouseInventory = {
warehouseId,
productId: inboundItem.productId,
productName: inboundItem.productName,
quantity: inboundItem.quantity,
batchNumber: inboundItem.batchNumber,
expiryDate: inboundItem.expiryDate,
lastInboundDate: data.inboundDate,
};
updatedInventories.push(newItem);
}
// 建立庫存異動記錄
const transaction: InventoryTransaction = {
id: generateId(),
warehouseId,
warehouseName,
productId: inboundItem.productId,
productName: inboundItem.productName,
batchNumber: inboundItem.batchNumber,
quantity: inboundItem.quantity,
transactionType: "手動入庫",
reason: data.reason,
notes: data.notes,
expiryDate: inboundItem.expiryDate,
operatorName: "系統使用者",
createdAt: now,
};
setTransactions([...transactions, transaction]);
});
// 更新庫存
handleSaveInventory(warehouseId, updatedInventories);
const totalInboundQty = data.items.reduce((sum, item) => sum + item.quantity, 0);
toast.success(`入庫成功!共 ${data.items.length} 項商品,總數量 ${totalInboundQty}`);
// 返回庫存管理頁面
handleBackFromAddInventory(warehouseId);
};
const renderPage = () => {
switch (currentPage.page) {
case "warehouse-management":
return (
<WarehouseManagement
onNavigateToInventory={handleNavigateToInventory}
inventories={inventories}
onUpdateInventories={setInventories}
safetyStockSettings={safetyStockSettings}
/>
);
case "warehouse-inventory": {
const warehouseId = currentPage.params?.warehouseId;
const warehouse = warehouses.find((w) => w.id === warehouseId);
if (!warehouse) {
// Fallback to warehouse management if warehouse not found
return (
<WarehouseManagement
onNavigateToInventory={handleNavigateToInventory}
inventories={inventories}
onUpdateInventories={setInventories}
safetyStockSettings={safetyStockSettings}
/>
);
}
return (
<WarehouseInventoryPage
warehouseId={warehouse.id}
warehouseName={warehouse.name}
inventories={inventories.filter(
(inv) => inv.warehouseId === warehouse.id
)}
safetyStockSettings={safetyStockSettings.filter(
(s) => s.warehouseId === warehouse.id
)}
onBack={handleBackFromInventory}
onNavigateToAddInventory={() => handleNavigateToAddInventory(warehouse.id)}
onNavigateToEditInventory={(inventoryId) =>
handleNavigateToEditInventory(warehouse.id, inventoryId)
}
onNavigateToSafetyStock={() => handleNavigateToSafetyStock(warehouse.id)}
/>
);
}
case "add-inventory": {
const warehouseId = currentPage.params?.warehouseId;
const warehouse = warehouses.find((w) => w.id === warehouseId);
if (!warehouse) {
return (
<WarehouseManagement
onNavigateToInventory={handleNavigateToInventory}
inventories={inventories}
onUpdateInventories={setInventories}
safetyStockSettings={safetyStockSettings}
/>
);
}
return (
<AddInventoryPage
warehouseId={warehouse.id}
warehouseName={warehouse.name}
onBack={() => handleBackFromAddInventory(warehouse.id)}
onSave={(data) => handleSaveInbound(warehouse.id, data)}
/>
);
}
case "edit-inventory": {
const warehouseId = currentPage.params?.warehouseId;
const inventoryId = currentPage.params?.inventoryId;
const warehouse = warehouses.find((w) => w.id === warehouseId);
if (!warehouse || !inventoryId) {
return (
<WarehouseManagement
onNavigateToInventory={handleNavigateToInventory}
inventories={inventories}
onUpdateInventories={setInventories}
safetyStockSettings={safetyStockSettings}
/>
);
}
// 找出對應的庫存項目
const warehouseInventories = inventories.filter((inv) => inv.warehouseId === warehouseId);
const inventoryIdParts = inventoryId.split('-');
const index = parseInt(inventoryIdParts[inventoryIdParts.length - 1], 10);
const inventory = warehouseInventories[index];
if (!inventory) {
return (
<WarehouseManagement
onNavigateToInventory={handleNavigateToInventory}
inventories={inventories}
onUpdateInventories={setInventories}
safetyStockSettings={safetyStockSettings}
/>
);
}
return (
<EditInventoryPage
inventory={{ ...inventory, inventoryId }}
warehouseName={warehouse.name}
onBack={() => handleBackFromEditInventory(warehouse.id)}
onSave={(updatedInventory) => handleUpdateInventory(warehouse.id, updatedInventory)}
onDelete={(invId) => handleDeleteInventory(warehouse.id, invId)}
/>
);
}
case "safety-stock": {
const warehouseId = currentPage.params?.warehouseId;
const warehouse = warehouses.find((w) => w.id === warehouseId);
if (!warehouse) {
return (
<WarehouseManagement
onNavigateToInventory={handleNavigateToInventory}
inventories={inventories}
onUpdateInventories={setInventories}
safetyStockSettings={safetyStockSettings}
/>
);
}
// 找出該倉庫的所有庫存
const warehouseInventories = inventories.filter((inv) => inv.warehouseId === warehouseId);
return (
<SafetyStockPage
warehouseId={warehouse.id}
warehouseName={warehouse.name}
safetyStockSettings={safetyStockSettings.filter((s) => s.warehouseId === warehouseId)}
inventories={warehouseInventories}
onBack={() => handleBackFromSafetyStock(warehouse.id)}
onSave={(settings) => handleSaveSafetyStockSettings(warehouse.id, settings)}
/>
);
}
default:
// Default to warehouse management
return (
<WarehouseManagement
onNavigateToInventory={handleNavigateToInventory}
inventories={inventories}
onUpdateInventories={setInventories}
safetyStockSettings={safetyStockSettings}
/>
);
}
};
return (
<div className="flex min-h-screen bg-background">
{/* Sidebar Navigation */}
<NavigationSidebar
currentPath={currentPage.page}
onNavigate={handleNavigate}
/>
{/* Main Content */}
<main className="flex-1 overflow-auto">
{renderPage()}
</main>
<Toaster />
</div>
);
}