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,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}
/>
)}
</>
);
}