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