Files
star-erp/source-code/ERP(A-a)-商品建檔管理/src/components/ProductManagement.tsx

416 lines
11 KiB
TypeScript
Raw Normal View History

2025-12-30 15:03:19 +08:00
import { useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import { Plus, Search } from "lucide-react";
import ProductTable from "./ProductTable";
import ProductDialog from "./ProductDialog";
export type ProductType = "raw_material" | "finished_product";
export type ProductUnit = "kg" | "g" | "l" | "ml" | "piece" | "box" | "pack" | "bottle" | "can" | "jar" | "bag" | "basin" | "container";
export interface Product {
id: string;
product_code: string; // 商品編號(系統自動生成)
name: string;
type: ProductType;
unit: ProductUnit;
barcode_value: string; // 條碼內容(預設等於商品編號)
createdAt: string;
}
export default function ProductManagement() {
const [products, setProducts] = useState<Product[]>([
// 原物料
{
id: "1",
product_code: "RM0001",
name: "紅糖",
type: "raw_material",
unit: "pack",
barcode_value: "RM0001",
createdAt: "2025-11-01",
},
{
id: "2",
product_code: "RM0002",
name: "白糖",
type: "raw_material",
unit: "pack",
barcode_value: "RM0002",
createdAt: "2025-11-01",
},
{
id: "3",
product_code: "RM0003",
name: "砂糖",
type: "raw_material",
unit: "pack",
barcode_value: "RM0003",
createdAt: "2025-11-02",
},
{
id: "4",
product_code: "RM0004",
name: "全脂牛奶-小",
type: "raw_material",
unit: "bottle",
barcode_value: "RM0004",
createdAt: "2025-11-02",
},
{
id: "5",
product_code: "RM0005",
name: "煉乳",
type: "raw_material",
unit: "can",
barcode_value: "RM0005",
createdAt: "2025-11-03",
},
{
id: "6",
product_code: "RM0006",
name: "椰奶",
type: "raw_material",
unit: "can",
barcode_value: "RM0006",
createdAt: "2025-11-03",
},
{
id: "7",
product_code: "RM0007",
name: "鮮奶油",
type: "raw_material",
unit: "can",
barcode_value: "RM0007",
createdAt: "2025-11-04",
},
{
id: "8",
product_code: "RM0008",
name: "粉圓原料(生珍珠)",
type: "raw_material",
unit: "pack",
barcode_value: "RM0008",
createdAt: "2025-11-04",
},
{
id: "9",
product_code: "RM0009",
name: "仙草凍粉",
type: "raw_material",
unit: "pack",
barcode_value: "RM0009",
createdAt: "2025-11-05",
},
{
id: "10",
product_code: "RM0010",
name: "芋頭塊(原料)",
type: "raw_material",
unit: "kg",
barcode_value: "RM0010",
createdAt: "2025-11-05",
},
{
id: "11",
product_code: "RM0011",
name: "地瓜塊(原料)",
type: "raw_material",
unit: "kg",
barcode_value: "RM0011",
createdAt: "2025-11-06",
},
{
id: "12",
product_code: "RM0012",
name: "綠豆(乾)",
type: "raw_material",
unit: "kg",
barcode_value: "RM0012",
createdAt: "2025-11-06",
},
{
id: "13",
product_code: "RM0013",
name: "紅豆(乾)",
type: "raw_material",
unit: "kg",
barcode_value: "RM0013",
createdAt: "2025-11-07",
},
{
id: "14",
product_code: "RM0014",
name: "檸檬原汁",
type: "raw_material",
unit: "bottle",
barcode_value: "RM0014",
createdAt: "2025-11-07",
},
{
id: "15",
product_code: "RM0015",
name: "抹茶粉",
type: "raw_material",
unit: "can",
barcode_value: "RM0015",
createdAt: "2025-11-08",
},
{
id: "16",
product_code: "RM0016",
name: "可可粉",
type: "raw_material",
unit: "can",
barcode_value: "RM0016",
createdAt: "2025-11-08",
},
{
id: "17",
product_code: "RM0017",
name: "蜂蜜",
type: "raw_material",
unit: "bottle",
barcode_value: "RM0017",
createdAt: "2025-11-09",
},
{
id: "18",
product_code: "RM0018",
name: "果糖",
type: "raw_material",
unit: "can",
barcode_value: "RM0018",
createdAt: "2025-11-09",
},
// 半成品
{
id: "19",
product_code: "SF0001",
name: "粉粿原漿",
type: "finished_product",
unit: "bag",
barcode_value: "SF0001",
createdAt: "2025-11-10",
},
{
id: "20",
product_code: "SF0002",
name: "黑糖粉圓(未加糖)",
type: "finished_product",
unit: "basin",
barcode_value: "SF0002",
createdAt: "2025-11-10",
},
{
id: "21",
product_code: "SF0003",
name: "熟紅豆(未加糖)",
type: "finished_product",
unit: "basin",
barcode_value: "SF0003",
createdAt: "2025-11-11",
},
{
id: "22",
product_code: "SF0004",
name: "熟綠豆(未加糖)",
type: "finished_product",
unit: "basin",
barcode_value: "SF0004",
createdAt: "2025-11-11",
},
{
id: "23",
product_code: "SF0005",
name: "芋頭泥(無調味)",
type: "finished_product",
unit: "box",
barcode_value: "SF0005",
createdAt: "2025-11-12",
},
{
id: "24",
product_code: "SF0006",
name: "地瓜泥(無調味)",
type: "finished_product",
unit: "box",
barcode_value: "SF0006",
createdAt: "2025-11-12",
},
{
id: "25",
product_code: "SF0007",
name: "仙草原凍(整塊)",
type: "finished_product",
unit: "basin",
barcode_value: "SF0007",
createdAt: "2025-11-13",
},
]);
const [searchTerm, setSearchTerm] = useState("");
const [typeFilter, setTypeFilter] = useState<string>("all");
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const filteredProducts = products.filter((product) => {
const matchesSearch =
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.product_code.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.barcode_value.includes(searchTerm);
const matchesType =
typeFilter === "all" || product.type === typeFilter;
return matchesSearch && matchesType;
});
const handleAddProduct = () => {
setEditingProduct(null);
setIsDialogOpen(true);
};
const handleEditProduct = (product: Product) => {
setEditingProduct(product);
setIsDialogOpen(true);
};
// 生成新的商品編號(根據類型使用不同前綴)
const generateProductCode = (productType: ProductType): string => {
if (productType === "raw_material") {
// 原物料使用 RM 前綴 + 4 位數字
const rawMaterialProducts = products.filter(p => p.type === "raw_material");
if (rawMaterialProducts.length === 0) {
return "RM0001";
}
const maxNumber = rawMaterialProducts.reduce((max, product) => {
const match = product.product_code.match(/RM(\d{4})/);
if (match) {
const num = parseInt(match[1], 10);
return num > max ? num : max;
}
return max;
}, 0);
const nextNumber = maxNumber + 1;
return `RM${nextNumber.toString().padStart(4, "0")}`;
} else {
// 半成品使用 SF 前綴 + 4 位數字
const finishedProducts = products.filter(p => p.type === "finished_product");
if (finishedProducts.length === 0) {
return "SF0001";
}
const maxNumber = finishedProducts.reduce((max, product) => {
const match = product.product_code.match(/SF(\d{4})/);
if (match) {
const num = parseInt(match[1], 10);
return num > max ? num : max;
}
return max;
}, 0);
const nextNumber = maxNumber + 1;
return `SF${nextNumber.toString().padStart(4, "0")}`;
}
};
const handleSaveProduct = (product: Omit<Product, "id" | "createdAt" | "product_code">) => {
if (editingProduct) {
// Update existing product
setProducts(
products.map((p) =>
p.id === editingProduct.id
? { ...product, id: p.id, createdAt: p.createdAt, product_code: p.product_code }
: p
)
);
} else {
// Add new product with auto-generated product_code based on type
const product_code = generateProductCode(product.type);
const newProduct: Product = {
...product,
id: Date.now().toString(),
product_code,
createdAt: new Date().toISOString().split("T")[0],
};
setProducts([newProduct, ...products]);
}
setIsDialogOpen(false);
};
const handleDeleteProduct = (id: string) => {
setProducts(products.filter((p) => p.id !== id));
};
return (
<div className="container mx-auto p-6 max-w-7xl">
{/* Header */}
<div className="mb-6">
<h1 className="mb-2"></h1>
<p className="text-gray-600"></p>
</div>
{/* Toolbar */}
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
<div className="flex flex-col md:flex-row gap-4">
{/* Search */}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="搜尋商品名稱、商品編號或條碼..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* Type Filter */}
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="w-full md:w-[180px]">
<SelectValue placeholder="商品類型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="raw_material"></SelectItem>
<SelectItem value="finished_product"></SelectItem>
</SelectContent>
</Select>
{/* Add Button */}
<Button onClick={handleAddProduct} className="w-full md:w-auto button-filled-primary">
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
{/* Product Table */}
<ProductTable
products={filteredProducts}
onEdit={handleEditProduct}
onDelete={handleDeleteProduct}
/>
{/* Product Dialog */}
<ProductDialog
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
product={editingProduct}
onSave={handleSaveProduct}
/>
</div>
);
}