Files
inventory/inventory/src/components/analytics/DiscountImpact.tsx
T

160 lines
5.7 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
ResponsiveContainer,
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
CartesianGrid,
Legend,
} from 'recharts';
import config from '../../config';
import { METRIC_COLORS } from '@/lib/dashboard/designTokens';
import { formatCurrency } from '@/utils/formatCurrency';
interface DiscountRow {
abcClass: string;
discountBucket: string;
productCount: number;
avgSellThrough: number;
revenue: number;
discountAmount: number;
profit: number;
}
const CLASS_COLORS: Record<string, string> = {
A: METRIC_COLORS.revenue,
B: METRIC_COLORS.orders,
C: METRIC_COLORS.comparison,
};
export function DiscountImpact() {
const { data, isLoading, isError } = useQuery<DiscountRow[]>({
queryKey: ['discount-impact'],
queryFn: async () => {
const response = await fetch(`${config.apiUrl}/analytics/discounts`);
if (!response.ok) throw new Error('Failed to fetch discount data');
return response.json();
},
});
if (isError) {
return (
<Card>
<CardHeader><CardTitle>Discount Impact</CardTitle></CardHeader>
<CardContent>
<div className="h-[300px] flex items-center justify-center">
<p className="text-sm text-destructive">Failed to load discount data</p>
</div>
</CardContent>
</Card>
);
}
if (isLoading || !data) {
return (
<Card>
<CardHeader><CardTitle>Discount Impact</CardTitle></CardHeader>
<CardContent>
<div className="h-[300px] flex items-center justify-center">
<div className="animate-pulse text-muted-foreground">Loading discount data...</div>
</div>
</CardContent>
</Card>
);
}
// Pivot: for each discount bucket, show avg sell-through by ABC class
const buckets = ['No Discount', '1-10%', '11-20%', '21-30%', '30%+'];
const chartData = buckets.map(bucket => {
const row: Record<string, string | number> = { bucket };
['A', 'B', 'C'].forEach(cls => {
const match = data.find(d => d.discountBucket === bucket && d.abcClass === cls);
row[`Class ${cls}`] = match?.avgSellThrough || 0;
});
return row;
});
// Summary by ABC class
const classSummary = ['A', 'B', 'C'].map(cls => {
const rows = data.filter(d => d.abcClass === cls);
return {
abcClass: cls,
totalProducts: rows.reduce((s, r) => s + r.productCount, 0),
totalDiscounts: rows.reduce((s, r) => s + r.discountAmount, 0),
totalRevenue: rows.reduce((s, r) => s + r.revenue, 0),
totalProfit: rows.reduce((s, r) => s + r.profit, 0),
};
});
return (
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Sell-Through by Discount Level</CardTitle>
<p className="text-xs text-muted-foreground">
Avg 30-day sell-through % at each discount bracket, by ABC class
</p>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={350}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis dataKey="bucket" tick={{ fontSize: 11 }} />
<YAxis tickFormatter={(v) => `${v}%`} tick={{ fontSize: 11 }} />
<Tooltip formatter={(value: number) => [`${value}%`]} />
<Legend />
<Bar dataKey="Class A" fill={CLASS_COLORS.A} radius={[2, 2, 0, 0]} />
<Bar dataKey="Class B" fill={CLASS_COLORS.B} radius={[2, 2, 0, 0]} />
<Bar dataKey="Class C" fill={CLASS_COLORS.C} radius={[2, 2, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Discount Leakage by Class</CardTitle>
<p className="text-xs text-muted-foreground">
How much discount is given relative to revenue per ABC class
</p>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<th className="px-4 py-2 text-left font-medium">Class</th>
<th className="px-4 py-2 text-right font-medium">Products</th>
<th className="px-4 py-2 text-right font-medium">Revenue</th>
<th className="px-4 py-2 text-right font-medium">Discounts</th>
<th className="px-4 py-2 text-right font-medium">Disc %</th>
<th className="px-4 py-2 text-right font-medium">Profit</th>
</tr>
</thead>
<tbody>
{classSummary.map((row) => (
<tr key={row.abcClass} className="border-b">
<td className="px-4 py-2 font-medium">Class {row.abcClass}</td>
<td className="px-4 py-2 text-right">{row.totalProducts.toLocaleString()}</td>
<td className="px-4 py-2 text-right">{formatCurrency(row.totalRevenue)}</td>
<td className="px-4 py-2 text-right">{formatCurrency(row.totalDiscounts)}</td>
<td className="px-4 py-2 text-right">
{row.totalRevenue > 0
? ((row.totalDiscounts / (row.totalRevenue + row.totalDiscounts)) * 100).toFixed(1)
: '0'}%
</td>
<td className="px-4 py-2 text-right">{formatCurrency(row.totalProfit)}</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
}