Frontend fixes - categories, deal with new hierarchy, misc fixes
This commit is contained in:
@@ -6,6 +6,7 @@ import config from '../../config';
|
||||
interface CategoryData {
|
||||
performance: {
|
||||
category: string;
|
||||
categoryPath: string; // Full hierarchy path
|
||||
revenue: number;
|
||||
profit: number;
|
||||
growth: number;
|
||||
@@ -13,10 +14,12 @@ interface CategoryData {
|
||||
}[];
|
||||
distribution: {
|
||||
category: string;
|
||||
categoryPath: string; // Full hierarchy path
|
||||
value: number;
|
||||
}[];
|
||||
trends: {
|
||||
category: string;
|
||||
categoryPath: string; // Full hierarchy path
|
||||
month: string;
|
||||
sales: number;
|
||||
}[];
|
||||
@@ -36,6 +39,7 @@ export function CategoryPerformance() {
|
||||
return {
|
||||
performance: rawData.performance.map((item: any) => ({
|
||||
...item,
|
||||
categoryPath: item.categoryPath || item.category,
|
||||
revenue: Number(item.revenue) || 0,
|
||||
profit: Number(item.profit) || 0,
|
||||
growth: Number(item.growth) || 0,
|
||||
@@ -43,10 +47,12 @@ export function CategoryPerformance() {
|
||||
})),
|
||||
distribution: rawData.distribution.map((item: any) => ({
|
||||
...item,
|
||||
categoryPath: item.categoryPath || item.category,
|
||||
value: Number(item.value) || 0
|
||||
})),
|
||||
trends: rawData.trends.map((item: any) => ({
|
||||
...item,
|
||||
categoryPath: item.categoryPath || item.category,
|
||||
sales: Number(item.sales) || 0
|
||||
}))
|
||||
};
|
||||
@@ -63,6 +69,8 @@ export function CategoryPerformance() {
|
||||
return <span className={color}>{value}</span>;
|
||||
};
|
||||
|
||||
const getShortCategoryName = (path: string) => path.split(' > ').pop() || path;
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
@@ -76,24 +84,34 @@ export function CategoryPerformance() {
|
||||
<Pie
|
||||
data={data.distribution}
|
||||
dataKey="value"
|
||||
nameKey="category"
|
||||
nameKey="categoryPath"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={100}
|
||||
fill="#8884d8"
|
||||
label={(entry) => entry.category}
|
||||
label={({ categoryPath }) => getShortCategoryName(categoryPath)}
|
||||
>
|
||||
{data.distribution.map((entry, index) => (
|
||||
<Cell
|
||||
key={entry.category}
|
||||
key={`${entry.category}-${index}`}
|
||||
fill={COLORS[index % COLORS.length]}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
formatter={(value: number) => [`$${value.toLocaleString()}`, 'Revenue']}
|
||||
formatter={(value: number, name: string, props: any) => [
|
||||
`$${value.toLocaleString()}`,
|
||||
<div key="tooltip">
|
||||
<div className="font-medium">Category Path:</div>
|
||||
<div className="text-sm text-muted-foreground">{props.payload.categoryPath}</div>
|
||||
<div className="mt-1">Revenue</div>
|
||||
</div>
|
||||
]}
|
||||
/>
|
||||
<Legend
|
||||
formatter={(value) => getShortCategoryName(value)}
|
||||
wrapperStyle={{ fontSize: '12px' }}
|
||||
/>
|
||||
<Legend />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
@@ -106,10 +124,33 @@ export function CategoryPerformance() {
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={data.performance}>
|
||||
<XAxis dataKey="category" />
|
||||
<XAxis
|
||||
dataKey="categoryPath"
|
||||
tick={({ x, y, payload }) => (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<text
|
||||
x={0}
|
||||
y={0}
|
||||
dy={16}
|
||||
textAnchor="end"
|
||||
fill="#888888"
|
||||
transform="rotate(-35)"
|
||||
>
|
||||
{getShortCategoryName(payload.value)}
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
/>
|
||||
<YAxis tickFormatter={(value) => `${value}%`} />
|
||||
<Tooltip
|
||||
formatter={(value: number) => [`${value.toFixed(1)}%`, 'Growth Rate']}
|
||||
formatter={(value: number, name: string, props: any) => [
|
||||
`${value.toFixed(1)}%`,
|
||||
<div key="tooltip">
|
||||
<div className="font-medium">Category Path:</div>
|
||||
<div className="text-sm text-muted-foreground">{props.payload.categoryPath}</div>
|
||||
<div className="mt-1">Growth Rate</div>
|
||||
</div>
|
||||
]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="growth"
|
||||
@@ -131,8 +172,11 @@ export function CategoryPerformance() {
|
||||
{data.performance.map((category) => (
|
||||
<div key={category.category} className="flex items-center">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{category.category}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">{getShortCategoryName(category.categoryPath)}</p>
|
||||
<p className="text-xs text-muted-foreground">{category.categoryPath}</p>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{category.productCount} products
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import config from '../../config';
|
||||
interface ProfitData {
|
||||
byCategory: {
|
||||
category: string;
|
||||
categoryPath: string; // Full hierarchy path
|
||||
profitMargin: number;
|
||||
revenue: number;
|
||||
cost: number;
|
||||
@@ -18,6 +19,8 @@ interface ProfitData {
|
||||
}[];
|
||||
topProducts: {
|
||||
product: string;
|
||||
category: string;
|
||||
categoryPath: string; // Full hierarchy path
|
||||
profitMargin: number;
|
||||
revenue: number;
|
||||
cost: number;
|
||||
@@ -36,6 +39,7 @@ export function ProfitAnalysis() {
|
||||
return {
|
||||
byCategory: rawData.byCategory.map((item: any) => ({
|
||||
...item,
|
||||
categoryPath: item.categoryPath || item.category,
|
||||
profitMargin: Number(item.profitMargin) || 0,
|
||||
revenue: Number(item.revenue) || 0,
|
||||
cost: Number(item.cost) || 0
|
||||
@@ -48,6 +52,7 @@ export function ProfitAnalysis() {
|
||||
})),
|
||||
topProducts: rawData.topProducts.map((item: any) => ({
|
||||
...item,
|
||||
categoryPath: item.categoryPath || item.category,
|
||||
profitMargin: Number(item.profitMargin) || 0,
|
||||
revenue: Number(item.revenue) || 0,
|
||||
cost: Number(item.cost) || 0
|
||||
@@ -60,6 +65,8 @@ export function ProfitAnalysis() {
|
||||
return <div>Loading profit analysis...</div>;
|
||||
}
|
||||
|
||||
const getShortCategoryName = (path: string) => path.split(' > ').pop() || path;
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
@@ -70,10 +77,33 @@ export function ProfitAnalysis() {
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={data.byCategory}>
|
||||
<XAxis dataKey="category" />
|
||||
<XAxis
|
||||
dataKey="categoryPath"
|
||||
tick={({ x, y, payload }) => (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<text
|
||||
x={0}
|
||||
y={0}
|
||||
dy={16}
|
||||
textAnchor="end"
|
||||
fill="#888888"
|
||||
transform="rotate(-35)"
|
||||
>
|
||||
{getShortCategoryName(payload.value)}
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
/>
|
||||
<YAxis tickFormatter={(value) => `${value}%`} />
|
||||
<Tooltip
|
||||
formatter={(value: number) => [`${value.toFixed(1)}%`, 'Profit Margin']}
|
||||
formatter={(value: number, name: string, props: any) => [
|
||||
`${value.toFixed(1)}%`,
|
||||
<div key="tooltip">
|
||||
<div className="font-medium">Category Path:</div>
|
||||
<div className="text-sm text-muted-foreground">{props.payload.categoryPath}</div>
|
||||
<div className="mt-1">Profit Margin</div>
|
||||
</div>
|
||||
]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="profitMargin"
|
||||
@@ -123,7 +153,11 @@ export function ProfitAnalysis() {
|
||||
<div key={product.product} className="flex items-center">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{product.product}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p className="font-medium">Category:</p>
|
||||
<p>{product.categoryPath}</p>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Revenue: ${product.revenue.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ interface Product {
|
||||
interface Category {
|
||||
cat_id: number;
|
||||
name: string;
|
||||
categoryPath: string;
|
||||
units_sold: number;
|
||||
revenue: string;
|
||||
profit: string;
|
||||
@@ -159,7 +160,14 @@ export function BestSellers() {
|
||||
<TableBody>
|
||||
{data?.categories.map((category) => (
|
||||
<TableRow key={category.cat_id}>
|
||||
<TableCell>{category.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-medium">{category.name}</div>
|
||||
{category.categoryPath && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{category.categoryPath}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{category.units_sold}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(Number(category.revenue))}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(Number(category.profit))}</TableCell>
|
||||
|
||||
@@ -17,6 +17,7 @@ interface Product {
|
||||
|
||||
export interface ForecastItem {
|
||||
category: string;
|
||||
categoryPath: string;
|
||||
avgDailySales: number;
|
||||
totalSold: number;
|
||||
numProducts: number;
|
||||
@@ -44,6 +45,16 @@ export const columns: ColumnDef<ForecastItem>[] = [
|
||||
{
|
||||
accessorKey: "category",
|
||||
header: "Category",
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<div className="font-medium">{row.original.category}</div>
|
||||
{row.original.categoryPath && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{row.original.categoryPath}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "avgDailySales",
|
||||
|
||||
@@ -123,6 +123,8 @@ interface Product {
|
||||
notes: string;
|
||||
lead_time_days: number | null;
|
||||
}>;
|
||||
|
||||
category_paths?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface ProductDetailProps {
|
||||
@@ -255,22 +257,28 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Categories</dt>
|
||||
<dd className="flex flex-wrap gap-2">
|
||||
{product?.categories?.map(category => (
|
||||
<span key={category} className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium ring-1 ring-inset ring-muted">
|
||||
{category}
|
||||
</span>
|
||||
)) || "N/A"}
|
||||
<dd className="flex flex-col gap-2">
|
||||
{product?.category_paths ?
|
||||
Object.entries(product.category_paths).map(([key, fullPath], index) => {
|
||||
const [, leafCategory] = key.split(':');
|
||||
return (
|
||||
<div key={key} className="flex flex-col">
|
||||
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium ring-1 ring-inset ring-muted">
|
||||
{leafCategory}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground ml-2 mt-1">
|
||||
{fullPath}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: "N/A"}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Tags</dt>
|
||||
<dd className="flex flex-wrap gap-2">
|
||||
{product?.tags?.map(tag => (
|
||||
<span key={tag} className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium ring-1 ring-inset ring-muted">
|
||||
{tag}
|
||||
</span>
|
||||
)) || "N/A"}
|
||||
N/A
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
@@ -307,11 +315,11 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Status</dt>
|
||||
<dd>{product?.metrics?.stock_status}</dd>
|
||||
<dd>{product?.stock_status || "N/A"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Days of Stock</dt>
|
||||
<dd>{product?.metrics?.days_of_inventory} days</dd>
|
||||
<dd>{product?.days_of_inventory || 0} days</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
@@ -321,15 +329,15 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Daily Sales</dt>
|
||||
<dd>{product?.metrics?.daily_sales_avg?.toFixed(1)} units</dd>
|
||||
<dd>{product?.daily_sales_avg?.toFixed(1) || "0.0"} units</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Weekly Sales</dt>
|
||||
<dd>{product?.metrics?.weekly_sales_avg?.toFixed(1)} units</dd>
|
||||
<dd>{product?.weekly_sales_avg?.toFixed(1) || "0.0"} units</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Monthly Sales</dt>
|
||||
<dd>{product?.metrics?.monthly_sales_avg?.toFixed(1)} units</dd>
|
||||
<dd>{product?.monthly_sales_avg?.toFixed(1) || "0.0"} units</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
@@ -356,19 +364,19 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Total Revenue</dt>
|
||||
<dd>${formatPrice(product?.metrics.total_revenue)}</dd>
|
||||
<dd>${formatPrice(product?.total_revenue)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Gross Profit</dt>
|
||||
<dd>${formatPrice(product?.metrics.gross_profit)}</dd>
|
||||
<dd>${formatPrice(product?.gross_profit)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Margin</dt>
|
||||
<dd>{product?.metrics.avg_margin_percent.toFixed(2)}%</dd>
|
||||
<dd>{product?.avg_margin_percent?.toFixed(2) || "0.00"}%</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">GMROI</dt>
|
||||
<dd>{product?.metrics.gmroi.toFixed(2)}</dd>
|
||||
<dd>{product?.gmroi?.toFixed(2) || "0.00"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
@@ -378,15 +386,15 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Current Lead Time</dt>
|
||||
<dd>{product?.metrics.current_lead_time}</dd>
|
||||
<dd>{product?.current_lead_time || "N/A"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Target Lead Time</dt>
|
||||
<dd>{product?.metrics.target_lead_time}</dd>
|
||||
<dd>{product?.target_lead_time || "N/A"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Lead Time Status</dt>
|
||||
<dd>{product?.metrics.lead_time_status}</dd>
|
||||
<dd>{product?.lead_time_status || "N/A"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
@@ -408,11 +416,11 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Days of Inventory</dt>
|
||||
<dd className="text-2xl font-semibold">{product?.metrics?.days_of_inventory || 0}</dd>
|
||||
<dd className="text-2xl font-semibold">{product?.days_of_inventory || 0}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Status</dt>
|
||||
<dd className="text-2xl font-semibold">{product?.metrics?.stock_status || "N/A"}</dd>
|
||||
<dd className="text-2xl font-semibold">{product?.stock_status || "N/A"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
@@ -422,15 +430,15 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
||||
<dl className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Reorder Point</dt>
|
||||
<dd>{product?.metrics?.reorder_point || 0}</dd>
|
||||
<dd>{product?.reorder_point || 0}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Safety Stock</dt>
|
||||
<dd>{product?.metrics?.safety_stock || 0}</dd>
|
||||
<dd>{product?.safety_stock || 0}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">ABC Class</dt>
|
||||
<dd>{product?.metrics?.abc_class || "N/A"}</dd>
|
||||
<dd>{product?.abc_class || "N/A"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
@@ -551,15 +559,15 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
||||
<dl className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Gross Profit</dt>
|
||||
<dd className="text-2xl font-semibold">${formatPrice(product?.metrics.gross_profit)}</dd>
|
||||
<dd className="text-2xl font-semibold">${formatPrice(product?.gross_profit)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">GMROI</dt>
|
||||
<dd className="text-2xl font-semibold">{product?.metrics.gmroi.toFixed(2)}</dd>
|
||||
<dd className="text-2xl font-semibold">{product?.gmroi?.toFixed(2) || "0.00"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Margin %</dt>
|
||||
<dd className="text-2xl font-semibold">{product?.metrics.avg_margin_percent.toFixed(2)}%</dd>
|
||||
<dd className="text-2xl font-semibold">{product?.avg_margin_percent?.toFixed(2) || "0.00"}%</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</Card>
|
||||
@@ -569,7 +577,7 @@ export function ProductDetail({ productId, onClose }: ProductDetailProps) {
|
||||
<dl className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Cost of Goods Sold</dt>
|
||||
<dd>${formatPrice(product?.metrics.cost_of_goods_sold)}</dd>
|
||||
<dd>${formatPrice(product?.cost_of_goods_sold)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-muted-foreground">Landing Cost</dt>
|
||||
|
||||
@@ -14,9 +14,9 @@ interface Category {
|
||||
name: string;
|
||||
type: number;
|
||||
parent_id: number | null;
|
||||
parent_name: string | null;
|
||||
parent_type: number | null;
|
||||
description: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
status: string;
|
||||
metrics?: {
|
||||
product_count: number;
|
||||
@@ -30,23 +30,41 @@ interface Category {
|
||||
|
||||
interface CategoryFilters {
|
||||
search: string;
|
||||
parent: string;
|
||||
type: string;
|
||||
performance: string;
|
||||
}
|
||||
|
||||
const TYPE_LABELS: Record<number, string> = {
|
||||
10: 'Section',
|
||||
11: 'Category',
|
||||
12: 'Subcategory',
|
||||
13: 'Sub-subcategory',
|
||||
20: 'Theme',
|
||||
21: 'Subtheme'
|
||||
};
|
||||
|
||||
function getCategoryStatusVariant(status: string): "default" | "secondary" | "destructive" | "outline" {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'active':
|
||||
return 'default';
|
||||
case 'inactive':
|
||||
return 'secondary';
|
||||
case 'archived':
|
||||
return 'destructive';
|
||||
default:
|
||||
return 'outline';
|
||||
}
|
||||
}
|
||||
|
||||
export function Categories() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [sortColumn, setSortColumn] = useState<keyof Category>("name");
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||
const [filters, setFilters] = useState<CategoryFilters>({
|
||||
search: "",
|
||||
parent: "all",
|
||||
type: "all",
|
||||
performance: "all",
|
||||
});
|
||||
const [] = useState({
|
||||
column: 'name',
|
||||
direction: 'asc'
|
||||
});
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["categories"],
|
||||
@@ -74,13 +92,9 @@ export function Categories() {
|
||||
);
|
||||
}
|
||||
|
||||
// Apply parent filter
|
||||
if (filters.parent !== 'all') {
|
||||
if (filters.parent === 'none') {
|
||||
filtered = filtered.filter(category => !category.parent_id);
|
||||
} else {
|
||||
filtered = filtered.filter(category => category.parent_id === Number(filters.parent));
|
||||
}
|
||||
// Apply type filter
|
||||
if (filters.type !== 'all') {
|
||||
filtered = filtered.filter(category => category.type === parseInt(filters.type));
|
||||
}
|
||||
|
||||
// Apply performance filter
|
||||
@@ -99,6 +113,19 @@ export function Categories() {
|
||||
|
||||
// Apply sorting
|
||||
filtered.sort((a, b) => {
|
||||
// First sort by type if not explicitly sorting by another column
|
||||
if (sortColumn === "name") {
|
||||
if (a.type !== b.type) {
|
||||
return a.type - b.type;
|
||||
}
|
||||
// Then by parent hierarchy
|
||||
if (a.parent_id !== b.parent_id) {
|
||||
if (!a.parent_id) return -1;
|
||||
if (!b.parent_id) return 1;
|
||||
return a.parent_id - b.parent_id;
|
||||
}
|
||||
}
|
||||
|
||||
const aVal = a[sortColumn];
|
||||
const bVal = b[sortColumn];
|
||||
|
||||
@@ -251,17 +278,18 @@ export function Categories() {
|
||||
className="h-8 w-[150px] lg:w-[250px]"
|
||||
/>
|
||||
<Select
|
||||
value={filters.parent}
|
||||
onValueChange={(value) => setFilters(prev => ({ ...prev, parent: value }))}
|
||||
value={filters.type}
|
||||
onValueChange={(value) => setFilters(prev => ({ ...prev, type: value }))}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[180px]">
|
||||
<SelectValue placeholder="Parent Category" />
|
||||
<SelectValue placeholder="Category Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
<SelectItem value="none">Top Level Only</SelectItem>
|
||||
{data?.parentCategories?.map((parent: string) => (
|
||||
<SelectItem key={parent} value={parent}>{parent}</SelectItem>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
{data?.typeCounts?.map(tc => (
|
||||
<SelectItem key={tc.type} value={tc.type.toString()}>
|
||||
{TYPE_LABELS[tc.type]} ({tc.count})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -287,8 +315,8 @@ export function Categories() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Parent</TableHead>
|
||||
<TableHead className="text-right">Products</TableHead>
|
||||
<TableHead className="text-right">Active</TableHead>
|
||||
@@ -302,15 +330,37 @@ export function Categories() {
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8">
|
||||
<TableCell colSpan={10} className="text-center py-8">
|
||||
Loading categories...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : paginatedData.map((category: Category) => (
|
||||
<TableRow key={category.cat_id}>
|
||||
<TableCell>{category.name}</TableCell>
|
||||
<TableCell>{getPerformanceBadge(category.metrics?.growth_rate ?? 0)}</TableCell>
|
||||
<TableCell>{category.parent_id ? getParentName(category.parent_id) : '-'}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">
|
||||
{TYPE_LABELS[category.type]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{category.name}</span>
|
||||
<Badge variant="outline" className="h-5">
|
||||
{TYPE_LABELS[category.type]}
|
||||
</Badge>
|
||||
</div>
|
||||
{category.description && (
|
||||
<div className="text-xs text-muted-foreground">{category.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{category.type === 10 ? category.name : // Section
|
||||
category.type === 11 ? `${category.parent_name}` : // Category
|
||||
category.type === 12 ? `${category.parent_name} > ${category.name}` : // Subcategory
|
||||
category.type === 13 ? `${category.parent_name} > ${category.name}` : // Sub-subcategory
|
||||
category.parent_name ? `${category.parent_name} > ${category.name}` : category.name}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{category.metrics?.product_count || 0}</TableCell>
|
||||
<TableCell className="text-right">{category.metrics?.active_products || 0}</TableCell>
|
||||
<TableCell className="text-right">{formatCurrency(category.metrics?.total_value || 0)}</TableCell>
|
||||
@@ -326,7 +376,7 @@ export function Categories() {
|
||||
))}
|
||||
{!isLoading && !paginatedData.length && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
||||
<TableCell colSpan={10} className="text-center py-8 text-muted-foreground">
|
||||
No categories found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -60,6 +60,7 @@ export default function Forecasting() {
|
||||
const data = await response.json();
|
||||
return data.map((item: any) => ({
|
||||
category: item.category_name,
|
||||
categoryPath: item.path,
|
||||
avgDailySales: Number(item.avg_daily_sales) || 0,
|
||||
totalSold: Number(item.total_sold) || 0,
|
||||
numProducts: Number(item.num_products) || 0,
|
||||
@@ -74,7 +75,8 @@ export default function Forecasting() {
|
||||
daily_sales_avg: Number(p.daily_sales_avg) || 0,
|
||||
forecast_units: Number(p.forecast_units) || 0,
|
||||
forecast_revenue: Number(p.forecast_revenue) || 0,
|
||||
confidence_level: Number(p.confidence_level) || 0
|
||||
confidence_level: Number(p.confidence_level) || 0,
|
||||
categoryPath: item.path
|
||||
}))
|
||||
}));
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user