first commit

This commit is contained in:
2025-12-30 15:03:19 +08:00
commit c735c36009
902 changed files with 83591 additions and 0 deletions

View File

@@ -0,0 +1,301 @@
/**
* 倉庫管理主元件
* 重構後:職責更清晰,邏輯更模組化
*/
import { useState } from "react";
import { Plus, ArrowRightLeft } from "lucide-react";
import { Button } from "./ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
import { toast } from "sonner@2.0.3";
import WarehouseDialog from "./WarehouseDialog";
import TransferOrderDialog, { TransferOrder } from "./TransferOrderDialog";
import SearchToolbar from "./shared/SearchToolbar";
import WarehouseFilter from "./warehouse/WarehouseFilter";
import WarehouseCard from "./warehouse/WarehouseCard";
import WarehouseEmptyState from "./warehouse/WarehouseEmptyState";
import { Warehouse, WarehouseInventory, Store, SafetyStockSetting } from "../types/warehouse";
import { useWarehouseFilter } from "../hooks/useWarehouseFilter";
import { calculateWarehouseStats, hasWarehouseWarning } from "../utils/inventory";
import { generateId, getCurrentDate, generateOrderNumber } from "../utils/format";
export type { Warehouse, WarehouseInventory };
interface WarehouseManagementProps {
onNavigateToInventory: (warehouseId: string) => void;
inventories: WarehouseInventory[];
onUpdateInventories: (inventories: WarehouseInventory[]) => void;
safetyStockSettings: SafetyStockSetting[];
}
export default function WarehouseManagement({
onNavigateToInventory,
inventories,
onUpdateInventories,
safetyStockSettings,
}: WarehouseManagementProps) {
// 門市列表Mock 資料)
const [stores] = useState<Store[]>([
{
id: "store-1",
name: "信義門市",
address: "台北市信義區信義路五段10號",
},
{
id: "store-2",
name: "敦化門市",
address: "台北市大安區敦化南路一段100號",
},
{
id: "store-3",
name: "南京門市",
address: "台北市松山區南京東路三段256號",
},
]);
// 倉庫狀態
const [warehouses, setWarehouses] = useState<Warehouse[]>([
{
id: "1",
name: "中央倉庫",
address: "台北市信義區信義路五段7號",
manager: "張經理",
phone: "02-1234-5678",
description: "主要原物料儲存倉庫",
createdAt: "2025-11-01",
type: "中央倉庫",
},
{
id: "2",
name: "門市冷藏庫",
address: "台北市大安區敦化南路一段100號",
manager: "李主管",
phone: "02-8765-4321",
description: "門市專用冷藏倉庫",
createdAt: "2025-11-10",
type: "門市",
storeId: "store-2",
storeName: "敦化門市",
},
]);
// 篩選狀態
const [searchTerm, setSearchTerm] = useState("");
const [typeFilter, setTypeFilter] = useState<string>("all");
// 對話框狀態
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingWarehouse, setEditingWarehouse] = useState<Warehouse | null>(null);
const [transferOrderDialogOpen, setTransferOrderDialogOpen] = useState(false);
const [editingTransferOrder, setEditingTransferOrder] = useState<TransferOrder | null>(null);
const [transferOrders, setTransferOrders] = useState<TransferOrder[]>([]);
// 使用篩選 Hook
const filteredWarehouses = useWarehouseFilter(warehouses, searchTerm, typeFilter);
// 倉庫操作處理函式
const handleAddWarehouse = () => {
setEditingWarehouse(null);
setIsDialogOpen(true);
};
const handleEditWarehouse = (warehouse: Warehouse) => {
setEditingWarehouse(warehouse);
setIsDialogOpen(true);
};
const handleSaveWarehouse = (warehouse: Omit<Warehouse, "id" | "createdAt">) => {
if (editingWarehouse) {
setWarehouses(
warehouses.map((w) =>
w.id === editingWarehouse.id ? { ...w, ...warehouse } : w
)
);
toast.success("倉庫資料已更新");
} else {
const newWarehouse: Warehouse = {
id: generateId(),
...warehouse,
createdAt: getCurrentDate(),
};
setWarehouses([...warehouses, newWarehouse]);
toast.success("倉庫已新增");
}
setIsDialogOpen(false);
setEditingWarehouse(null);
};
const handleDeleteWarehouse = (id: string) => {
setWarehouses(warehouses.filter((w) => w.id !== id));
onUpdateInventories(
inventories.filter((inv) => inv.warehouseId !== id)
);
toast.success("倉庫已刪除");
};
// 撥補單操作處理函式
const handleAddTransferOrder = () => {
setEditingTransferOrder(null);
setTransferOrderDialogOpen(true);
};
const handleSaveTransferOrder = (
orderData: Omit<TransferOrder, "id" | "createdAt" | "orderNumber">
) => {
const newOrder: TransferOrder = {
id: generateId(),
orderNumber: generateOrderNumber(),
...orderData,
createdAt: getCurrentDate(),
};
setTransferOrders([...transferOrders, newOrder]);
// 更新庫存
const updatedInventories = [...inventories];
// 尋找來源倉庫庫存
const sourceIndex = updatedInventories.findIndex(
(inv) =>
inv.warehouseId === orderData.sourceWarehouseId &&
inv.productId === orderData.productId &&
inv.batchNumber === orderData.batchNumber
);
if (sourceIndex !== -1) {
// 減少來源倉庫數量
updatedInventories[sourceIndex] = {
...updatedInventories[sourceIndex],
quantity: updatedInventories[sourceIndex].quantity - orderData.quantity,
};
// 尋找或建立目標倉庫庫存
const targetIndex = updatedInventories.findIndex(
(inv) =>
inv.warehouseId === orderData.targetWarehouseId &&
inv.productId === orderData.productId &&
inv.batchNumber === orderData.batchNumber
);
if (targetIndex !== -1) {
// 更新既有目標倉庫數量
updatedInventories[targetIndex] = {
...updatedInventories[targetIndex],
quantity: updatedInventories[targetIndex].quantity + orderData.quantity,
};
} else {
// 建立新的目標倉庫庫存項目
const sourceItem = updatedInventories[sourceIndex];
updatedInventories.push({
warehouseId: orderData.targetWarehouseId,
productId: orderData.productId,
productName: orderData.productName,
quantity: orderData.quantity,
batchNumber: orderData.batchNumber,
expiryDate: sourceItem.expiryDate,
});
}
onUpdateInventories(updatedInventories);
}
toast.success(`撥補單 ${newOrder.orderNumber} 已新增並完成庫存調整`);
setTransferOrderDialogOpen(false);
};
return (
<div className="container mx-auto p-6 max-w-7xl">
{/* 頁面標題 */}
<div className="mb-6">
<h1 className="mb-2"></h1>
<p className="text-gray-600"></p>
</div>
{/* 工具列 */}
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between">
<div className="flex flex-col sm:flex-row gap-4 flex-1 w-full">
{/* 搜尋框 */}
<SearchToolbar
value={searchTerm}
onChange={setSearchTerm}
placeholder="搜尋倉庫名稱..."
className="flex-1 w-full md:max-w-md"
/>
{/* 類別篩選 */}
<WarehouseFilter
value={typeFilter}
onChange={setTypeFilter}
className="w-full sm:w-48"
/>
</div>
{/* 操作按鈕 */}
<div className="flex gap-2 w-full md:w-auto">
<Button
onClick={handleAddTransferOrder}
className="flex-1 md:flex-initial button-outlined-primary"
>
<Plus className="mr-2 h-4 w-4" />
</Button>
<Button
onClick={handleAddWarehouse}
className="flex-1 md:flex-initial button-filled-primary"
>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
</div>
{/* 倉庫卡片列表 */}
{filteredWarehouses.length === 0 ? (
<WarehouseEmptyState />
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredWarehouses.map((warehouse) => (
<WarehouseCard
key={warehouse.id}
warehouse={warehouse}
stats={calculateWarehouseStats(inventories, warehouse.id, safetyStockSettings)}
hasWarning={hasWarehouseWarning(inventories, warehouse.id, safetyStockSettings)}
onViewInventory={onNavigateToInventory}
onEdit={handleEditWarehouse}
/>
))}
</div>
)}
{/* 倉庫對話框 */}
<WarehouseDialog
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
warehouse={editingWarehouse}
onSave={handleSaveWarehouse}
onDelete={handleDeleteWarehouse}
stores={stores}
/>
{/* 撥補單對話框 */}
<TransferOrderDialog
open={transferOrderDialogOpen}
onOpenChange={setTransferOrderDialogOpen}
order={editingTransferOrder}
onSave={handleSaveTransferOrder}
warehouses={warehouses}
inventories={inventories}
/>
</div>
);
}