first commit
This commit is contained in:
262
source-code/ERP(A-a)-商品建檔管理/src/components/ProductTable.tsx
Normal file
262
source-code/ERP(A-a)-商品建檔管理/src/components/ProductTable.tsx
Normal file
@@ -0,0 +1,262 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "./ui/table";
|
||||
import { Button } from "./ui/button";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Pencil, Trash2, Eye, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "./ui/alert-dialog";
|
||||
import type { Product, ProductType, ProductUnit } from "./ProductManagement";
|
||||
import { useState } from "react";
|
||||
import BarcodeViewDialog from "./BarcodeViewDialog";
|
||||
|
||||
interface ProductTableProps {
|
||||
products: Product[];
|
||||
onEdit: (product: Product) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
type SortField = "product_code" | "name" | "type" | "unit" | "barcode_value";
|
||||
type SortDirection = "asc" | "desc" | null;
|
||||
|
||||
const productTypeLabels: Record<ProductType, string> = {
|
||||
raw_material: "原物料",
|
||||
finished_product: "半成品",
|
||||
};
|
||||
|
||||
const productUnitLabels: Record<ProductUnit, string> = {
|
||||
kg: "公斤",
|
||||
g: "公克",
|
||||
l: "公升",
|
||||
ml: "毫升",
|
||||
piece: "個",
|
||||
box: "盒/箱",
|
||||
pack: "包",
|
||||
bottle: "瓶",
|
||||
can: "罐",
|
||||
jar: "瓶罐",
|
||||
bag: "袋",
|
||||
basin: "盆",
|
||||
container: "容器",
|
||||
};
|
||||
|
||||
export default function ProductTable({
|
||||
products,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: ProductTableProps) {
|
||||
const [sortField, setSortField] = useState<SortField | null>(null);
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
|
||||
const [barcodeDialogOpen, setBarcodeDialogOpen] = useState(false);
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||
|
||||
// 排序邏輯
|
||||
const handleSort = (field: SortField) => {
|
||||
if (sortField === field) {
|
||||
// 循環:asc -> desc -> null
|
||||
if (sortDirection === "asc") {
|
||||
setSortDirection("desc");
|
||||
} else if (sortDirection === "desc") {
|
||||
setSortDirection(null);
|
||||
setSortField(null);
|
||||
}
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection("asc");
|
||||
}
|
||||
};
|
||||
|
||||
// 排序圖示
|
||||
const getSortIcon = (field: SortField) => {
|
||||
if (sortField !== field) {
|
||||
return <ArrowUpDown className="ml-1 h-3 w-3 inline opacity-40" />;
|
||||
}
|
||||
if (sortDirection === "asc") {
|
||||
return <ArrowUp className="ml-1 h-3 w-3 inline text-primary" />;
|
||||
}
|
||||
if (sortDirection === "desc") {
|
||||
return <ArrowDown className="ml-1 h-3 w-3 inline text-primary" />;
|
||||
}
|
||||
return <ArrowUpDown className="ml-1 h-3 w-3 inline opacity-40" />;
|
||||
};
|
||||
|
||||
// 排序後的商品列表
|
||||
const sortedProducts = [...products].sort((a, b) => {
|
||||
if (!sortField || !sortDirection) return 0;
|
||||
|
||||
let aValue: string | number = a[sortField];
|
||||
let bValue: string | number = b[sortField];
|
||||
|
||||
// 字串比較(不區分大小寫)
|
||||
if (typeof aValue === "string" && typeof bValue === "string") {
|
||||
aValue = aValue.toLowerCase();
|
||||
bValue = bValue.toLowerCase();
|
||||
}
|
||||
|
||||
if (aValue < bValue) {
|
||||
return sortDirection === "asc" ? -1 : 1;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return sortDirection === "asc" ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
// 查看條碼
|
||||
const handleViewBarcode = (product: Product) => {
|
||||
setSelectedProduct(product);
|
||||
setBarcodeDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead
|
||||
className="cursor-pointer select-none hover:bg-gray-50"
|
||||
onClick={() => handleSort("product_code")}
|
||||
>
|
||||
商品編號
|
||||
{getSortIcon("product_code")}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer select-none hover:bg-gray-50"
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
商品名稱
|
||||
{getSortIcon("name")}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer select-none hover:bg-gray-50"
|
||||
onClick={() => handleSort("type")}
|
||||
>
|
||||
類型
|
||||
{getSortIcon("type")}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer select-none hover:bg-gray-50"
|
||||
onClick={() => handleSort("unit")}
|
||||
>
|
||||
單位
|
||||
{getSortIcon("unit")}
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer select-none hover:bg-gray-50"
|
||||
onClick={() => handleSort("barcode_value")}
|
||||
>
|
||||
商品條碼
|
||||
{getSortIcon("barcode_value")}
|
||||
</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedProducts.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-8 text-gray-500">
|
||||
無符合條件的商品資料
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
sortedProducts.map((product) => (
|
||||
<TableRow key={product.id}>
|
||||
<TableCell className="font-mono text-sm text-gray-700">
|
||||
{product.product_code}
|
||||
</TableCell>
|
||||
<TableCell>{product.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
product.type === "raw_material"
|
||||
? "secondary"
|
||||
: product.type === "finished_product"
|
||||
? "default"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
{productTypeLabels[product.type]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{productUnitLabels[product.unit]}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleViewBarcode(product)}
|
||||
className="h-8 px-2 text-primary hover:text-primary-dark hover:bg-primary-lightest"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onEdit(product)}
|
||||
className="button-outlined-primary"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="button-outlined-error">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確認刪除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
確定要刪除「{product.name}」嗎?此操作無法復原。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => onDelete(product.id)}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
刪除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 條碼查看對話框 */}
|
||||
{selectedProduct && (
|
||||
<BarcodeViewDialog
|
||||
open={barcodeDialogOpen}
|
||||
onOpenChange={setBarcodeDialogOpen}
|
||||
productName={selectedProduct.name}
|
||||
productCode={selectedProduct.product_code}
|
||||
barcodeValue={selectedProduct.barcode_value}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user