216 lines
9.4 KiB
TypeScript
216 lines
9.4 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
export interface Product {
|
|
_id: string;
|
|
code: string;
|
|
name: string;
|
|
category: string;
|
|
subcategory: string;
|
|
quantity: number;
|
|
unit: string;
|
|
unitPrice: number;
|
|
vat: number;
|
|
supplier: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
interface ProductTableProps {
|
|
products: Product[];
|
|
}
|
|
|
|
export default function ProductTable({ products }: ProductTableProps) {
|
|
const [selectedProducts, setSelectedProducts] = useState<Set<string>>(new Set());
|
|
|
|
const toggleProduct = (id: string) => {
|
|
const newSelected = new Set(selectedProducts);
|
|
if (newSelected.has(id)) {
|
|
newSelected.delete(id);
|
|
} else {
|
|
newSelected.add(id);
|
|
}
|
|
setSelectedProducts(newSelected);
|
|
};
|
|
|
|
const toggleAll = () => {
|
|
if (selectedProducts.size === products.length) {
|
|
setSelectedProducts(new Set());
|
|
} else {
|
|
setSelectedProducts(new Set(products.map(p => p._id)));
|
|
}
|
|
};
|
|
|
|
const calculateNetPrice = (unitPrice: number, vat: number) => {
|
|
return unitPrice * (1 + vat / 100);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Desktop & Tablet Table View */}
|
|
<div className="hidden md:block bg-white rounded-lg border border-gray-200 overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-gray-50 border-b border-gray-200">
|
|
<th className="px-4 lg:px-6 py-3 text-left">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedProducts.size === products.length && products.length > 0}
|
|
onChange={toggleAll}
|
|
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
</th>
|
|
<th className="px-4 lg:px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Product
|
|
</th>
|
|
<th className="hidden lg:table-cell px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Category
|
|
</th>
|
|
<th className="hidden xl:table-cell px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Subcategory
|
|
</th>
|
|
<th className="px-4 lg:px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Quantity
|
|
</th>
|
|
<th className="hidden lg:table-cell px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Unit Price
|
|
</th>
|
|
<th className="hidden xl:table-cell px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
VAT
|
|
</th>
|
|
<th className="px-4 lg:px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Net Price
|
|
</th>
|
|
<th className="hidden lg:table-cell px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Supplier
|
|
</th>
|
|
<th className="px-4 lg:px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200">
|
|
{products.map((product) => (
|
|
<tr key={product._id} className="hover:bg-gray-50 transition-colors">
|
|
<td className="px-4 lg:px-6 py-4">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedProducts.has(product._id)}
|
|
onChange={() => toggleProduct(product._id)}
|
|
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
</td>
|
|
<td className="px-4 lg:px-6 py-4">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-900">{product.name}</p>
|
|
<p className="text-xs text-gray-500">ID: {product.code}</p>
|
|
<p className="text-xs text-gray-500 lg:hidden mt-1">
|
|
{product.category} • {product.quantity} {product.unit}
|
|
</p>
|
|
</div>
|
|
</td>
|
|
<td className="hidden lg:table-cell px-6 py-4 text-sm text-gray-700">{product.category}</td>
|
|
<td className="hidden xl:table-cell px-6 py-4 text-sm text-gray-700">{product.subcategory}</td>
|
|
<td className="px-4 lg:px-6 py-4 text-sm text-gray-700">
|
|
{product.quantity} {product.unit}
|
|
</td>
|
|
<td className="hidden lg:table-cell px-6 py-4 text-sm text-gray-700">€{product.unitPrice.toFixed(2)}</td>
|
|
<td className="hidden xl:table-cell px-6 py-4 text-sm text-gray-700">{product.vat}%</td>
|
|
<td className="px-4 lg:px-6 py-4 text-sm font-bold text-gray-900">
|
|
€{calculateNetPrice(product.unitPrice, product.vat).toFixed(2)}
|
|
</td>
|
|
<td className="hidden lg:table-cell px-6 py-4">
|
|
<a href="#" className="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
|
{product.supplier}
|
|
</a>
|
|
</td>
|
|
<td className="px-4 lg:px-6 py-4">
|
|
<button className="text-gray-400 hover:text-gray-600">
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
|
</svg>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Card View */}
|
|
<div className="md:hidden space-y-4">
|
|
{products.map((product) => (
|
|
<div key={product._id} className="bg-white rounded-lg border border-gray-200 p-4">
|
|
<div className="flex items-start gap-3">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedProducts.has(product._id)}
|
|
onChange={() => toggleProduct(product._id)}
|
|
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 mt-1"
|
|
/>
|
|
<div className="flex-1">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-gray-900">{product.name}</h3>
|
|
<p className="text-xs text-gray-500 mt-0.5">ID: {product.code}</p>
|
|
</div>
|
|
<button className="text-gray-400 hover:text-gray-600">
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mt-3 grid grid-cols-2 gap-3">
|
|
<div>
|
|
<p className="text-xs text-gray-500">Category</p>
|
|
<p className="text-sm text-gray-900 mt-0.5">{product.category}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500">Subcategory</p>
|
|
<p className="text-sm text-gray-900 mt-0.5">{product.subcategory}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500">Quantity</p>
|
|
<p className="text-sm text-gray-900 mt-0.5">{product.quantity} {product.unit}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500">Unit Price</p>
|
|
<p className="text-sm text-gray-900 mt-0.5">€{product.unitPrice.toFixed(2)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500">VAT</p>
|
|
<p className="text-sm text-gray-900 mt-0.5">{product.vat}%</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500">Net Price</p>
|
|
<p className="text-sm font-bold text-gray-900 mt-0.5">
|
|
€{calculateNetPrice(product.unitPrice, product.vat).toFixed(2)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 pt-3 border-t border-gray-200 flex items-center justify-between">
|
|
<div>
|
|
<p className="text-xs text-gray-500">Supplier</p>
|
|
<a href="#" className="text-sm text-blue-600 hover:text-blue-800 font-medium">
|
|
{product.supplier}
|
|
</a>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-xs text-gray-500">Updated</p>
|
|
<p className="text-xs text-gray-700 mt-0.5">{new Date(product.updatedAt).toLocaleDateString()}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|