Add year selection to admin analytics and split product table
AdminAnalytics now supports selecting historical years and updates available years dynamically from the API. The product table is refactored to display enabled and disabled products in separate tables, improving clarity. Minor formatting and code style improvements are also included.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -32,3 +32,4 @@ export function NotificationProviderWrapper({ children }: NotificationProviderWr
|
|||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import {
|
||||||
import { Edit, Trash, AlertTriangle, CheckCircle, AlertCircle, Calculator, Copy } from "lucide-react";
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Edit,
|
||||||
|
Trash,
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle,
|
||||||
|
AlertCircle,
|
||||||
|
Calculator,
|
||||||
|
Copy,
|
||||||
|
} 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";
|
||||||
@@ -24,128 +39,182 @@ const ProductTable = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onToggleEnabled,
|
onToggleEnabled,
|
||||||
onProfitAnalysis,
|
onProfitAnalysis,
|
||||||
getCategoryNameById
|
getCategoryNameById,
|
||||||
}: ProductTableProps) => {
|
}: ProductTableProps) => {
|
||||||
|
// Separate enabled and disabled products
|
||||||
|
const enabledProducts = products.filter((p) => p.enabled !== false);
|
||||||
|
const disabledProducts = products.filter((p) => p.enabled === false);
|
||||||
|
|
||||||
const sortedProducts = [...products].sort((a, b) => {
|
const sortByCategory = (productList: Product[]) => {
|
||||||
const categoryNameA = getCategoryNameById(a.category);
|
return [...productList].sort((a, b) => {
|
||||||
const categoryNameB = getCategoryNameById(b.category);
|
const categoryNameA = getCategoryNameById(a.category);
|
||||||
return categoryNameA.localeCompare(categoryNameB);
|
const categoryNameB = getCategoryNameById(b.category);
|
||||||
});
|
return categoryNameA.localeCompare(categoryNameB);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedEnabledProducts = sortByCategory(enabledProducts);
|
||||||
|
const sortedDisabledProducts = sortByCategory(disabledProducts);
|
||||||
|
|
||||||
const getStockIcon = (product: Product) => {
|
const getStockIcon = (product: Product) => {
|
||||||
if (!product.stockTracking) return null;
|
if (!product.stockTracking) return null;
|
||||||
|
|
||||||
if (product.stockStatus === 'out_of_stock') {
|
if (product.stockStatus === "out_of_stock") {
|
||||||
return <AlertTriangle className="h-4 w-4 text-red-500" />;
|
return <AlertTriangle className="h-4 w-4 text-red-500" />;
|
||||||
} else if (product.stockStatus === 'low_stock') {
|
} else if (product.stockStatus === "low_stock") {
|
||||||
return <AlertCircle className="h-4 w-4 text-amber-500" />;
|
return <AlertCircle className="h-4 w-4 text-amber-500" />;
|
||||||
} else {
|
} else {
|
||||||
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderProductRow = (product: Product, isDisabled: boolean = false) => (
|
||||||
|
<TableRow
|
||||||
|
key={product._id}
|
||||||
|
className={`transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70 ${isDisabled ? "opacity-60" : ""}`}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<div className="font-medium truncate max-w-[180px]">{product.name}</div>
|
||||||
|
<div className="hidden sm:block text-sm text-muted-foreground mt-1">
|
||||||
|
{getCategoryNameById(product.category)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden sm:table-cell text-center">
|
||||||
|
{getCategoryNameById(product.category)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden md:table-cell text-center">
|
||||||
|
{product.unitType}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{product.stockTracking ? (
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
{getStockIcon(product)}
|
||||||
|
<span className="text-sm">
|
||||||
|
{product.currentStock !== undefined ? product.currentStock : 0}{" "}
|
||||||
|
{product.unitType}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
Not Tracked
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="hidden lg:table-cell text-center">
|
||||||
|
<Switch
|
||||||
|
checked={product.enabled !== false}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onToggleEnabled(product._id as string, checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right flex justify-end space-x-1">
|
||||||
|
{onProfitAnalysis && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
onProfitAnalysis(product._id as string, product.name)
|
||||||
|
}
|
||||||
|
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" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onClone && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onClone(product)}
|
||||||
|
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/20"
|
||||||
|
title="Clone Listing"
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTableHeader = () => (
|
||||||
|
<TableHeader className="bg-gray-50 dark:bg-zinc-800/50">
|
||||||
|
<TableRow className="hover:bg-transparent">
|
||||||
|
<TableHead className="w-[200px]">Product</TableHead>
|
||||||
|
<TableHead className="hidden sm:table-cell text-center">
|
||||||
|
Category
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="hidden md:table-cell text-center">Unit</TableHead>
|
||||||
|
<TableHead className="text-center">Stock</TableHead>
|
||||||
|
<TableHead className="hidden lg:table-cell text-center">
|
||||||
|
Enabled
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden">
|
<div className="space-y-6">
|
||||||
<Table className="relative">
|
{/* Enabled Products Table */}
|
||||||
<TableHeader className="bg-gray-50 dark:bg-zinc-800/50">
|
<div className="rounded-lg border dark:border-zinc-700 shadow-sm overflow-hidden">
|
||||||
<TableRow className="hover:bg-transparent">
|
<Table className="relative">
|
||||||
<TableHead className="w-[200px]">Product</TableHead>
|
{renderTableHeader()}
|
||||||
<TableHead className="hidden sm:table-cell text-center">Category</TableHead>
|
<TableBody>
|
||||||
<TableHead className="hidden md:table-cell text-center">Unit</TableHead>
|
{loading ? (
|
||||||
<TableHead className="text-center">Stock</TableHead>
|
Array.from({ length: 1 }).map((_, index) => (
|
||||||
<TableHead className="hidden lg:table-cell text-center">Enabled</TableHead>
|
<TableRow key={index}>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableCell>Loading...</TableCell>
|
||||||
</TableRow>
|
<TableCell>Loading...</TableCell>
|
||||||
</TableHeader>
|
<TableCell>Loading...</TableCell>
|
||||||
<TableBody>
|
<TableCell>Loading...</TableCell>
|
||||||
{loading ? (
|
<TableCell>Loading...</TableCell>
|
||||||
Array.from({ length: 1 }).map((_, index) => (
|
<TableCell>Loading...</TableCell>
|
||||||
<TableRow key={index}>
|
</TableRow>
|
||||||
<TableCell>Loading...</TableCell>
|
))
|
||||||
<TableCell>Loading...</TableCell>
|
) : sortedEnabledProducts.length > 0 ? (
|
||||||
<TableCell>Loading...</TableCell>
|
sortedEnabledProducts.map((product) => renderProductRow(product))
|
||||||
<TableCell>Loading...</TableCell>
|
) : (
|
||||||
<TableCell>Loading...</TableCell>
|
<TableRow>
|
||||||
<TableCell>Loading...</TableCell>
|
<TableCell colSpan={6} className="h-24 text-center">
|
||||||
</TableRow>
|
No enabled products found.
|
||||||
))
|
|
||||||
) : sortedProducts.length > 0 ? (
|
|
||||||
sortedProducts.map((product) => (
|
|
||||||
<TableRow key={product._id} className="transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70">
|
|
||||||
<TableCell>
|
|
||||||
<div className="font-medium truncate max-w-[180px]">{product.name}</div>
|
|
||||||
<div className="hidden sm:block text-sm text-muted-foreground mt-1">
|
|
||||||
{getCategoryNameById(product.category)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="hidden sm:table-cell text-center">{getCategoryNameById(product.category)}</TableCell>
|
|
||||||
<TableCell className="hidden md:table-cell text-center">{product.unitType}</TableCell>
|
|
||||||
<TableCell className="text-center">
|
|
||||||
{product.stockTracking ? (
|
|
||||||
<div className="flex items-center justify-center gap-1">
|
|
||||||
{getStockIcon(product)}
|
|
||||||
<span className="text-sm">
|
|
||||||
{product.currentStock !== undefined ? product.currentStock : 0} {product.unitType}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Badge variant="outline" className="text-xs">Not Tracked</Badge>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="hidden lg:table-cell text-center">
|
|
||||||
<Switch
|
|
||||||
checked={product.enabled !== false}
|
|
||||||
onCheckedChange={(checked) => onToggleEnabled(product._id as string, checked)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right flex justify-end space-x-1">
|
|
||||||
{onProfitAnalysis && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onProfitAnalysis(product._id as string, product.name)}
|
|
||||||
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" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{onClone && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onClone(product)}
|
|
||||||
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/20"
|
|
||||||
title="Clone Listing"
|
|
||||||
>
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<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>
|
</TableRow>
|
||||||
))
|
)}
|
||||||
) : (
|
</TableBody>
|
||||||
<TableRow>
|
</Table>
|
||||||
<TableCell colSpan={6} className="h-24 text-center">
|
</div>
|
||||||
No products found.
|
|
||||||
</TableCell>
|
{/* Disabled Products Section */}
|
||||||
</TableRow>
|
{!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">
|
||||||
</TableBody>
|
<Table className="relative">
|
||||||
</Table>
|
{renderTableHeader()}
|
||||||
|
<TableBody>
|
||||||
|
{sortedDisabledProducts.map((product) =>
|
||||||
|
renderProductRow(product, true),
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ export interface Product {
|
|||||||
_id?: string;
|
_id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
unitType: 'pcs' | 'gr' | 'kg' | 'ml' | 'oz' | 'lb';
|
unitType: "pcs" | "gr" | "kg" | "ml" | "oz" | "lb";
|
||||||
category: string;
|
category: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
// Stock management fields
|
// Stock management fields
|
||||||
stockTracking?: boolean;
|
stockTracking?: boolean;
|
||||||
currentStock?: number;
|
currentStock?: number;
|
||||||
lowStockThreshold?: number;
|
lowStockThreshold?: number;
|
||||||
stockStatus?: 'in_stock' | 'low_stock' | 'out_of_stock';
|
stockStatus?: "in_stock" | "low_stock" | "out_of_stock";
|
||||||
pricing: Array<{
|
pricing: Array<{
|
||||||
minQuantity: number;
|
minQuantity: number;
|
||||||
pricePerUnit: number;
|
pricePerUnit: number;
|
||||||
}>;
|
}>;
|
||||||
image?: string | File | null | undefined;
|
image?: string | File | null | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"commitHash": "66e9543",
|
"commitHash": "74f3a2c",
|
||||||
"buildTime": "2025-12-31T07:04:51.067Z"
|
"buildTime": "2026-01-05T21:55:11.573Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user