Compare commits
2 Commits
211cdc71f9
...
f7e768f6d6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7e768f6d6 | ||
|
|
7c7db0fc09 |
@@ -45,7 +45,7 @@ const ProfitAnalysisModal = dynamic(() => import("@/components/modals/profit-ana
|
|||||||
|
|
||||||
function ProductTableSkeleton() {
|
function ProductTableSkeleton() {
|
||||||
return (
|
return (
|
||||||
<Card className="animate-in fade-in duration-500">
|
<Card className="animate-in fade-in duration-500 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Skeleton className="h-6 w-32" />
|
<Skeleton className="h-6 w-32" />
|
||||||
@@ -60,8 +60,8 @@ function ProductTableSkeleton() {
|
|||||||
<div className="border-b p-4">
|
<div className="border-b p-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{['Product', 'Category', 'Price', 'Stock', 'Status', 'Actions'].map((header, i) => (
|
{['Product', 'Category', 'Price', 'Stock', 'Status', 'Actions'].map((header, i) => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
key={i}
|
key={i}
|
||||||
className="h-4 w-20 flex-1 animate-in fade-in"
|
className="h-4 w-20 flex-1 animate-in fade-in"
|
||||||
style={{
|
style={{
|
||||||
animationDelay: `${i * 50}ms`,
|
animationDelay: `${i * 50}ms`,
|
||||||
@@ -72,10 +72,10 @@ function ProductTableSkeleton() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{[...Array(8)].map((_, i) => (
|
{[...Array(8)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="border-b last:border-b-0 p-4 animate-in fade-in"
|
className="border-b last:border-b-0 p-4 animate-in fade-in"
|
||||||
style={{
|
style={{
|
||||||
animationDelay: `${300 + i * 50}ms`,
|
animationDelay: `${300 + i * 50}ms`,
|
||||||
@@ -152,7 +152,7 @@ export default function ProductsPage() {
|
|||||||
const [importModalOpen, setImportModalOpen] = useState(false);
|
const [importModalOpen, setImportModalOpen] = useState(false);
|
||||||
const [addProductOpen, setAddProductOpen] = useState(false);
|
const [addProductOpen, setAddProductOpen] = useState(false);
|
||||||
const [profitAnalysisOpen, setProfitAnalysisOpen] = useState(false);
|
const [profitAnalysisOpen, setProfitAnalysisOpen] = useState(false);
|
||||||
const [selectedProductForAnalysis, setSelectedProductForAnalysis] = useState<{id: string, name: string} | null>(null);
|
const [selectedProductForAnalysis, setSelectedProductForAnalysis] = useState<{ id: string, name: string } | null>(null);
|
||||||
|
|
||||||
// Fetch products and categories
|
// Fetch products and categories
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -169,7 +169,7 @@ export default function ProductsPage() {
|
|||||||
const fetchDataAsync = async () => {
|
const fetchDataAsync = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const [fetchedProducts, fetchedCategories] = await Promise.all([
|
const [fetchedProducts, fetchedCategories] = await Promise.all([
|
||||||
clientFetch('/products'),
|
clientFetch('/products'),
|
||||||
clientFetch('/categories'),
|
clientFetch('/categories'),
|
||||||
@@ -210,7 +210,7 @@ export default function ProductsPage() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle input changes
|
// Handle input changes
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
) => setProductData({ ...productData, [e.target.name]: e.target.value });
|
) => setProductData({ ...productData, [e.target.name]: e.target.value });
|
||||||
@@ -226,7 +226,7 @@ export default function ProductsPage() {
|
|||||||
setProductData({ ...productData, pricing: updatedPricing });
|
setProductData({ ...productData, pricing: updatedPricing });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleSaveProduct = async (data: Product, file?: File | null) => {
|
const handleSaveProduct = async (data: Product, file?: File | null) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -247,7 +247,7 @@ export default function ProductsPage() {
|
|||||||
// Save the product data
|
// Save the product data
|
||||||
const endpoint = editing ? `/products/${data._id}` : "/products";
|
const endpoint = editing ? `/products/${data._id}` : "/products";
|
||||||
const method = editing ? "PUT" : "POST";
|
const method = editing ? "PUT" : "POST";
|
||||||
|
|
||||||
const productResponse = await clientFetch(endpoint, {
|
const productResponse = await clientFetch(endpoint, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -259,10 +259,10 @@ export default function ProductsPage() {
|
|||||||
// If there's a new image to upload
|
// If there's a new image to upload
|
||||||
if (file) {
|
if (file) {
|
||||||
const imageEndpoint = `/products/${productResponse._id || data._id}/image`;
|
const imageEndpoint = `/products/${productResponse._id || data._id}/image`;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
|
||||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL}${imageEndpoint}`, {
|
await fetch(`${process.env.NEXT_PUBLIC_API_URL}${imageEndpoint}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -279,10 +279,10 @@ export default function ProductsPage() {
|
|||||||
// Refresh products list
|
// Refresh products list
|
||||||
const fetchedProducts = await clientFetch('/products');
|
const fetchedProducts = await clientFetch('/products');
|
||||||
setProducts(fetchedProducts);
|
setProducts(fetchedProducts);
|
||||||
|
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
toast.success(
|
toast.success(
|
||||||
editing ? "Product updated successfully" : "Product added successfully"
|
editing ? "Product updated successfully" : "Product added successfully"
|
||||||
);
|
);
|
||||||
@@ -296,18 +296,18 @@ export default function ProductsPage() {
|
|||||||
// Handle delete product
|
// Handle delete product
|
||||||
const handleDeleteProduct = async (productId: string) => {
|
const handleDeleteProduct = async (productId: string) => {
|
||||||
if (!confirm("Are you sure you want to delete this product?")) return;
|
if (!confirm("Are you sure you want to delete this product?")) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
await clientFetch(`/products/${productId}`, {
|
await clientFetch(`/products/${productId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Refresh products list
|
// Refresh products list
|
||||||
const fetchedProducts = await clientFetch('/products');
|
const fetchedProducts = await clientFetch('/products');
|
||||||
setProducts(fetchedProducts);
|
setProducts(fetchedProducts);
|
||||||
|
|
||||||
toast.success("Product deleted successfully");
|
toast.success("Product deleted successfully");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -323,9 +323,9 @@ export default function ProductsPage() {
|
|||||||
...product,
|
...product,
|
||||||
pricing: product.pricing
|
pricing: product.pricing
|
||||||
? product.pricing.map((tier) => ({
|
? product.pricing.map((tier) => ({
|
||||||
minQuantity: tier.minQuantity,
|
minQuantity: tier.minQuantity,
|
||||||
pricePerUnit: tier.pricePerUnit,
|
pricePerUnit: tier.pricePerUnit,
|
||||||
}))
|
}))
|
||||||
: [{ minQuantity: 1, pricePerUnit: 0 }],
|
: [{ minQuantity: 1, pricePerUnit: 0 }],
|
||||||
costPerUnit: product.costPerUnit || 0,
|
costPerUnit: product.costPerUnit || 0,
|
||||||
});
|
});
|
||||||
@@ -343,16 +343,16 @@ export default function ProductsPage() {
|
|||||||
image: null, // Clear image so user can upload a new one
|
image: null, // Clear image so user can upload a new one
|
||||||
pricing: product.pricing
|
pricing: product.pricing
|
||||||
? product.pricing.map((tier) => ({
|
? product.pricing.map((tier) => ({
|
||||||
minQuantity: tier.minQuantity,
|
minQuantity: tier.minQuantity,
|
||||||
pricePerUnit: tier.pricePerUnit,
|
pricePerUnit: tier.pricePerUnit,
|
||||||
}))
|
}))
|
||||||
: [{ minQuantity: 1, pricePerUnit: 0 }],
|
: [{ minQuantity: 1, pricePerUnit: 0 }],
|
||||||
costPerUnit: product.costPerUnit || 0,
|
costPerUnit: product.costPerUnit || 0,
|
||||||
// Reset stock to defaults for cloned product
|
// Reset stock to defaults for cloned product
|
||||||
currentStock: 0,
|
currentStock: 0,
|
||||||
stockStatus: 'out_of_stock' as const,
|
stockStatus: 'out_of_stock' as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
setProductData(clonedProduct);
|
setProductData(clonedProduct);
|
||||||
setEditing(false); // Set to false so it creates a new product
|
setEditing(false); // Set to false so it creates a new product
|
||||||
setAddProductOpen(true);
|
setAddProductOpen(true);
|
||||||
@@ -390,19 +390,19 @@ export default function ProductsPage() {
|
|||||||
// Filter products based on search term
|
// Filter products based on search term
|
||||||
const filteredProducts = products.filter(product => {
|
const filteredProducts = products.filter(product => {
|
||||||
if (!searchTerm) return true;
|
if (!searchTerm) return true;
|
||||||
|
|
||||||
const searchLower = searchTerm.toLowerCase();
|
const searchLower = searchTerm.toLowerCase();
|
||||||
|
|
||||||
// Search in product name
|
// Search in product name
|
||||||
if (product.name.toLowerCase().includes(searchLower)) return true;
|
if (product.name.toLowerCase().includes(searchLower)) return true;
|
||||||
|
|
||||||
// Search in product description if it exists
|
// Search in product description if it exists
|
||||||
if (product.description && product.description.toLowerCase().includes(searchLower)) return true;
|
if (product.description && product.description.toLowerCase().includes(searchLower)) return true;
|
||||||
|
|
||||||
// Search in category name
|
// Search in category name
|
||||||
const categoryName = getCategoryNameById(product.category).toLowerCase();
|
const categoryName = getCategoryNameById(product.category).toLowerCase();
|
||||||
if (categoryName.includes(searchLower)) return true;
|
if (categoryName.includes(searchLower)) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -437,19 +437,19 @@ export default function ProductsPage() {
|
|||||||
const handleToggleEnabled = async (productId: string, enabled: boolean) => {
|
const handleToggleEnabled = async (productId: string, enabled: boolean) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
await clientFetch(`/products/${productId}`, {
|
await clientFetch(`/products/${productId}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ enabled }),
|
body: JSON.stringify({ enabled }),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the local state
|
// Update the local state
|
||||||
setProducts(products.map(product =>
|
setProducts(products.map(product =>
|
||||||
product._id === productId
|
product._id === productId
|
||||||
? { ...product, enabled }
|
? { ...product, enabled }
|
||||||
: product
|
: product
|
||||||
));
|
));
|
||||||
|
|
||||||
toast.success(`Product ${enabled ? 'enabled' : 'disabled'} successfully`);
|
toast.success(`Product ${enabled ? 'enabled' : 'disabled'} successfully`);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -489,9 +489,9 @@ export default function ProductsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setImportModalOpen(true)}
|
onClick={() => setImportModalOpen(true)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="gap-2 flex-1 sm:flex-none"
|
className="gap-2 flex-1 sm:flex-none"
|
||||||
>
|
>
|
||||||
<Upload className="h-4 w-4" />
|
<Upload className="h-4 w-4" />
|
||||||
|
|||||||
@@ -14,11 +14,22 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
Calculator,
|
Calculator,
|
||||||
Copy,
|
Copy,
|
||||||
|
PackageX,
|
||||||
|
Archive
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Product } from "@/models/products";
|
import { Product } from "@/models/products";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
|
||||||
|
const getProductImageUrl = (product: Product) => {
|
||||||
|
if (!product.image) return null;
|
||||||
|
if (typeof product.image === 'string' && product.image.startsWith('http')) return product.image;
|
||||||
|
// Use the API endpoint to serve the image
|
||||||
|
return `${process.env.NEXT_PUBLIC_API_URL}/products/${product._id}/image`;
|
||||||
|
};
|
||||||
|
|
||||||
interface ProductTableProps {
|
interface ProductTableProps {
|
||||||
products: Product[];
|
products: Product[];
|
||||||
@@ -68,35 +79,57 @@ const ProductTable = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderProductRow = (product: Product, isDisabled: boolean = false) => (
|
const renderProductRow = (product: Product, index: number, isDisabled: boolean = false) => (
|
||||||
<TableRow
|
<motion.tr
|
||||||
key={product._id}
|
key={product._id}
|
||||||
className={`transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70 ${isDisabled ? "opacity-60" : ""}`}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2, delay: index * 0.05 }}
|
||||||
|
className={`group hover:bg-muted/40 border-b border-border/50 transition-colors ${isDisabled ? "opacity-60 bg-muted/20" : ""}`}
|
||||||
>
|
>
|
||||||
<TableCell>
|
<TableCell className="font-medium">
|
||||||
<div className="font-medium truncate max-w-[180px]">{product.name}</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="hidden sm:block text-sm text-muted-foreground mt-1">
|
<div className="h-8 w-8 rounded bg-muted/50 flex items-center justify-center text-muted-foreground overflow-hidden relative">
|
||||||
{getCategoryNameById(product.category)}
|
{getProductImageUrl(product) ? (
|
||||||
|
<img
|
||||||
|
src={getProductImageUrl(product)!}
|
||||||
|
alt={product.name}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs font-bold">{product.name.charAt(0).toUpperCase()}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="truncate max-w-[180px]">{product.name}</div>
|
||||||
|
<div className="sm:hidden text-xs text-muted-foreground">
|
||||||
|
{getCategoryNameById(product.category)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden sm:table-cell text-center">
|
<TableCell className="hidden sm:table-cell text-center">
|
||||||
{getCategoryNameById(product.category)}
|
<Badge variant="outline" className="font-normal bg-background/50">
|
||||||
|
{getCategoryNameById(product.category)}
|
||||||
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden md:table-cell text-center">
|
<TableCell className="hidden md:table-cell text-center text-muted-foreground text-sm">
|
||||||
{product.unitType}
|
{product.unitType}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{product.stockTracking ? (
|
{product.stockTracking ? (
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1.5">
|
||||||
{getStockIcon(product)}
|
{getStockIcon(product)}
|
||||||
<span className="text-sm">
|
<span className={`text-sm font-medium ${product.stockStatus === 'out_of_stock' ? 'text-destructive' :
|
||||||
{product.currentStock !== undefined ? product.currentStock : 0}{" "}
|
product.stockStatus === 'low_stock' ? 'text-amber-500' : 'text-foreground'
|
||||||
{product.unitType}
|
}`}>
|
||||||
|
{product.currentStock !== undefined ? product.currentStock : 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="secondary" className="text-[10px] h-5 px-1.5 text-muted-foreground bg-muted/50">
|
||||||
Not Tracked
|
Unlimited
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -106,57 +139,61 @@ const ProductTable = ({
|
|||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
onToggleEnabled(product._id as string, checked)
|
onToggleEnabled(product._id as string, checked)
|
||||||
}
|
}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right flex justify-end space-x-1">
|
<TableCell className="text-right">
|
||||||
{onProfitAnalysis && (
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
{onProfitAnalysis && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
onProfitAnalysis(product._id as string, product.name)
|
||||||
|
}
|
||||||
|
className="h-8 w-8 text-emerald-500 hover:text-emerald-600 hover:bg-emerald-500/10"
|
||||||
|
title="Profit Analysis"
|
||||||
|
>
|
||||||
|
<Calculator className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onClone && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onClone(product)}
|
||||||
|
className="h-8 w-8 text-blue-500 hover:text-blue-600 hover:bg-blue-500/10"
|
||||||
|
title="Clone Listing"
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="icon"
|
||||||
onClick={() =>
|
onClick={() => onEdit(product)}
|
||||||
onProfitAnalysis(product._id as string, product.name)
|
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||||
}
|
title="Edit Product"
|
||||||
className="text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-950/20"
|
|
||||||
title="Profit Analysis"
|
|
||||||
>
|
>
|
||||||
<Calculator className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
{onClone && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="icon"
|
||||||
onClick={() => onClone(product)}
|
onClick={() => onDelete(product._id as string)}
|
||||||
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/20"
|
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
|
||||||
title="Clone Listing"
|
title="Delete Product"
|
||||||
>
|
>
|
||||||
<Copy className="h-4 w-4" />
|
<Trash className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onEdit(product)}
|
|
||||||
title="Edit Product"
|
|
||||||
>
|
|
||||||
<Edit className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onDelete(product._id as string)}
|
|
||||||
className="text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20"
|
|
||||||
title="Delete Product"
|
|
||||||
>
|
|
||||||
<Trash className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</motion.tr>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderTableHeader = () => (
|
const renderTableHeader = () => (
|
||||||
<TableHeader className="bg-gray-50 dark:bg-zinc-800/50">
|
<TableHeader className="bg-muted/50 sticky top-0 z-10">
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableRow className="hover:bg-transparent border-border/50">
|
||||||
<TableHead className="w-[200px]">Product</TableHead>
|
<TableHead className="w-[200px]">Product</TableHead>
|
||||||
<TableHead className="hidden sm:table-cell text-center">
|
<TableHead className="hidden sm:table-cell text-center">
|
||||||
Category
|
Category
|
||||||
@@ -166,57 +203,86 @@ const ProductTable = ({
|
|||||||
<TableHead className="hidden lg:table-cell text-center">
|
<TableHead className="hidden lg:table-cell text-center">
|
||||||
Enabled
|
Enabled
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right pr-6">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
{/* Enabled Products Table */}
|
{/* Enabled Products Table */}
|
||||||
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden">
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
|
||||||
<Table className="relative">
|
<CardHeader className="py-4 px-6 border-b border-border/50 bg-muted/30">
|
||||||
{renderTableHeader()}
|
<CardTitle className="text-lg font-medium flex items-center gap-2">
|
||||||
<TableBody>
|
<CheckCircle className="h-5 w-5 text-primary" />
|
||||||
{loading ? (
|
Active Products
|
||||||
Array.from({ length: 1 }).map((_, index) => (
|
<Badge variant="secondary" className="ml-2 bg-background/80 backdrop-blur-sm">
|
||||||
<TableRow key={index}>
|
{sortedEnabledProducts.length}
|
||||||
<TableCell>Loading...</TableCell>
|
</Badge>
|
||||||
<TableCell>Loading...</TableCell>
|
</CardTitle>
|
||||||
<TableCell>Loading...</TableCell>
|
</CardHeader>
|
||||||
<TableCell>Loading...</TableCell>
|
<CardContent className="p-0">
|
||||||
<TableCell>Loading...</TableCell>
|
<div className="max-h-[600px] overflow-auto">
|
||||||
<TableCell>Loading...</TableCell>
|
<Table>
|
||||||
</TableRow>
|
{renderTableHeader()}
|
||||||
))
|
<TableBody>
|
||||||
) : sortedEnabledProducts.length > 0 ? (
|
<AnimatePresence mode="popLayout">
|
||||||
sortedEnabledProducts.map((product) => renderProductRow(product))
|
{loading ? (
|
||||||
) : (
|
<TableRow>
|
||||||
<TableRow>
|
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
|
||||||
<TableCell colSpan={6} className="h-24 text-center">
|
Loading products...
|
||||||
No enabled products found.
|
</TableCell>
|
||||||
</TableCell>
|
</TableRow>
|
||||||
</TableRow>
|
) : sortedEnabledProducts.length > 0 ? (
|
||||||
)}
|
sortedEnabledProducts.map((product, index) => renderProductRow(product, index))
|
||||||
</TableBody>
|
) : (
|
||||||
</Table>
|
<TableRow>
|
||||||
</div>
|
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
|
||||||
|
<div className="flex flex-col items-center justify-center gap-2">
|
||||||
|
<PackageX className="h-8 w-8 opacity-50" />
|
||||||
|
<p>No active products found</p>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Disabled Products Section */}
|
{/* Disabled Products Section */}
|
||||||
{!loading && disabledProducts.length > 0 && (
|
{!loading && disabledProducts.length > 0 && (
|
||||||
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden bg-gray-50/30 dark:bg-zinc-900/30">
|
<Card className="border-border/40 bg-background/30 backdrop-blur-sm shadow-sm overflow-hidden opacity-90">
|
||||||
<Table className="relative">
|
<CardHeader className="py-4 px-6 border-b border-border/50 bg-muted/20">
|
||||||
{renderTableHeader()}
|
<CardTitle className="text-lg font-medium flex items-center gap-2 text-muted-foreground">
|
||||||
<TableBody>
|
<Archive className="h-5 w-5" />
|
||||||
{sortedDisabledProducts.map((product) =>
|
Archived / Disabled
|
||||||
renderProductRow(product, true),
|
<Badge variant="outline" className="ml-2">
|
||||||
)}
|
{sortedDisabledProducts.length}
|
||||||
</TableBody>
|
</Badge>
|
||||||
</Table>
|
</CardTitle>
|
||||||
</div>
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="max-h-[400px] overflow-auto">
|
||||||
|
<Table>
|
||||||
|
{renderTableHeader()}
|
||||||
|
<TableBody>
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{sortedDisabledProducts.map((product, index) =>
|
||||||
|
renderProductRow(product, index, true),
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProductTable;
|
export default ProductTable;
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ export interface Product {
|
|||||||
pricePerUnit: number;
|
pricePerUnit: number;
|
||||||
}>;
|
}>;
|
||||||
image?: string | File | null | undefined;
|
image?: string | File | null | undefined;
|
||||||
|
costPerUnit?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user