Files
star-erp/source-code/ERP(A-a)-商品建檔管理/src/components/ProductTable.tsx
2025-12-30 15:03:19 +08:00

262 lines
8.6 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 {
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}
/>
)}
</>
);
}