Fix and restyle stock and purchases components
This commit is contained in:
@@ -16,30 +16,60 @@ async function executeQuery(sql, params = []) {
|
||||
router.get('/stock/metrics', async (req, res) => {
|
||||
try {
|
||||
// Get stock metrics
|
||||
const [stockMetrics] = await executeQuery(`
|
||||
const [rows] = await executeQuery(`
|
||||
SELECT
|
||||
COALESCE(COUNT(*), 0) as total_products,
|
||||
COALESCE(COUNT(CASE WHEN stock_quantity > 0 THEN 1 END), 0) as products_in_stock,
|
||||
COALESCE(SUM(stock_quantity), 0) as total_units,
|
||||
COALESCE(SUM(stock_quantity * cost_price), 0) as total_cost,
|
||||
COALESCE(SUM(stock_quantity * price), 0) as total_retail
|
||||
COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity END), 0) as total_units,
|
||||
COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity * cost_price END), 0) as total_cost,
|
||||
COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity * price END), 0) as total_retail
|
||||
FROM products
|
||||
`);
|
||||
const stockMetrics = rows[0];
|
||||
|
||||
// Get vendor stock values
|
||||
const [vendorValues] = await executeQuery(`
|
||||
SELECT
|
||||
vendor,
|
||||
COUNT(DISTINCT product_id) as variant_count,
|
||||
COALESCE(SUM(stock_quantity), 0) as stock_units,
|
||||
COALESCE(SUM(stock_quantity * cost_price), 0) as stock_cost,
|
||||
COALESCE(SUM(stock_quantity * price), 0) as stock_retail
|
||||
FROM products
|
||||
WHERE vendor IS NOT NULL
|
||||
AND stock_quantity > 0
|
||||
GROUP BY vendor
|
||||
HAVING stock_cost > 0
|
||||
ORDER BY stock_cost DESC
|
||||
console.log('Raw stockMetrics from database:', stockMetrics);
|
||||
console.log('stockMetrics.total_products:', stockMetrics.total_products);
|
||||
console.log('stockMetrics.products_in_stock:', stockMetrics.products_in_stock);
|
||||
console.log('stockMetrics.total_units:', stockMetrics.total_units);
|
||||
console.log('stockMetrics.total_cost:', stockMetrics.total_cost);
|
||||
console.log('stockMetrics.total_retail:', stockMetrics.total_retail);
|
||||
|
||||
// Get brand stock values with Other category
|
||||
const [brandValues] = await executeQuery(`
|
||||
WITH brand_totals AS (
|
||||
SELECT
|
||||
brand,
|
||||
COUNT(DISTINCT product_id) as variant_count,
|
||||
COALESCE(SUM(stock_quantity), 0) as stock_units,
|
||||
COALESCE(SUM(stock_quantity * cost_price), 0) as stock_cost,
|
||||
COALESCE(SUM(stock_quantity * price), 0) as stock_retail
|
||||
FROM products
|
||||
WHERE brand IS NOT NULL
|
||||
AND stock_quantity > 0
|
||||
GROUP BY brand
|
||||
HAVING stock_cost > 0
|
||||
),
|
||||
other_brands AS (
|
||||
SELECT
|
||||
'Other' as brand,
|
||||
SUM(variant_count) as variant_count,
|
||||
SUM(stock_units) as stock_units,
|
||||
SUM(stock_cost) as stock_cost,
|
||||
SUM(stock_retail) as stock_retail
|
||||
FROM brand_totals
|
||||
WHERE stock_cost <= 5000
|
||||
),
|
||||
main_brands AS (
|
||||
SELECT *
|
||||
FROM brand_totals
|
||||
WHERE stock_cost > 5000
|
||||
ORDER BY stock_cost DESC
|
||||
)
|
||||
SELECT * FROM main_brands
|
||||
UNION ALL
|
||||
SELECT * FROM other_brands
|
||||
WHERE stock_cost > 0
|
||||
ORDER BY CASE WHEN brand = 'Other' THEN 1 ELSE 0 END, stock_cost DESC
|
||||
`);
|
||||
|
||||
// Format the response with explicit type conversion
|
||||
@@ -49,8 +79,8 @@ router.get('/stock/metrics', async (req, res) => {
|
||||
totalStockUnits: parseInt(stockMetrics.total_units) || 0,
|
||||
totalStockCost: parseFloat(stockMetrics.total_cost) || 0,
|
||||
totalStockRetail: parseFloat(stockMetrics.total_retail) || 0,
|
||||
vendorStock: vendorValues.map(v => ({
|
||||
vendor: v.vendor,
|
||||
brandStock: brandValues.map(v => ({
|
||||
brand: v.brand,
|
||||
variants: parseInt(v.variant_count) || 0,
|
||||
units: parseInt(v.stock_units) || 0,
|
||||
cost: parseFloat(v.stock_cost) || 0,
|
||||
@@ -69,7 +99,7 @@ router.get('/stock/metrics', async (req, res) => {
|
||||
// Returns purchase order metrics by vendor
|
||||
router.get('/purchase/metrics', async (req, res) => {
|
||||
try {
|
||||
const [poMetrics] = await executeQuery(`
|
||||
const [rows] = await executeQuery(`
|
||||
SELECT
|
||||
COALESCE(COUNT(DISTINCT CASE WHEN po.status = 'open' THEN po.po_id END), 0) as active_pos,
|
||||
COALESCE(COUNT(DISTINCT CASE
|
||||
@@ -90,6 +120,14 @@ router.get('/purchase/metrics', async (req, res) => {
|
||||
FROM purchase_orders po
|
||||
JOIN products p ON po.product_id = p.product_id
|
||||
`);
|
||||
const poMetrics = rows[0];
|
||||
|
||||
console.log('Raw poMetrics from database:', poMetrics);
|
||||
console.log('poMetrics.active_pos:', poMetrics.active_pos);
|
||||
console.log('poMetrics.overdue_pos:', poMetrics.overdue_pos);
|
||||
console.log('poMetrics.total_units:', poMetrics.total_units);
|
||||
console.log('poMetrics.total_cost:', poMetrics.total_cost);
|
||||
console.log('poMetrics.total_retail:', poMetrics.total_retail);
|
||||
|
||||
const [vendorOrders] = await executeQuery(`
|
||||
SELECT
|
||||
@@ -106,7 +144,7 @@ router.get('/purchase/metrics', async (req, res) => {
|
||||
ORDER BY order_cost DESC
|
||||
`);
|
||||
|
||||
res.json({
|
||||
const response = {
|
||||
activePurchaseOrders: parseInt(poMetrics.active_pos) || 0,
|
||||
overduePurchaseOrders: parseInt(poMetrics.overdue_pos) || 0,
|
||||
onOrderUnits: parseInt(poMetrics.total_units) || 0,
|
||||
@@ -119,7 +157,9 @@ router.get('/purchase/metrics', async (req, res) => {
|
||||
cost: parseFloat(v.order_cost) || 0,
|
||||
retail: parseFloat(v.order_retail) || 0
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (err) {
|
||||
console.error('Error fetching purchase metrics:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch purchase metrics' });
|
||||
|
||||
@@ -83,17 +83,25 @@ const renderActiveShape = (props: any) => {
|
||||
export function PurchaseMetrics() {
|
||||
const [activeIndex, setActiveIndex] = useState<number | undefined>();
|
||||
|
||||
const { data } = useQuery<PurchaseMetricsData>({
|
||||
const { data, error, isLoading } = useQuery<PurchaseMetricsData>({
|
||||
queryKey: ["purchase-metrics"],
|
||||
queryFn: async () => {
|
||||
console.log('Fetching from:', `${config.apiUrl}/dashboard/purchase/metrics`);
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/purchase/metrics`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch purchase metrics")
|
||||
const text = await response.text();
|
||||
console.error('API Error:', text);
|
||||
throw new Error(`Failed to fetch purchase metrics: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json()
|
||||
const data = await response.json();
|
||||
console.log('API Response:', data);
|
||||
return data;
|
||||
},
|
||||
})
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error loading purchase metrics</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
@@ -108,41 +116,41 @@ export function PurchaseMetrics() {
|
||||
<ClipboardList className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Active Purchase Orders</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{data?.activePurchaseOrders.toLocaleString() || 0}</p>
|
||||
<p className="text-lg font-bold">{data?.activePurchaseOrders.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Overdue Purchase Orders</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{data?.overduePurchaseOrders.toLocaleString() || 0}</p>
|
||||
<p className="text-lg font-bold">{data?.overduePurchaseOrders.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">On Order Units</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{data?.onOrderUnits.toLocaleString() || 0}</p>
|
||||
<p className="text-lg font-bold">{data?.onOrderUnits.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">On Order Cost</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.onOrderCost || 0)}</p>
|
||||
<p className="text-lg font-bold">{formatCurrency(data?.onOrderCost || 0)}</p>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">On Order Retail</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.onOrderRetail || 0)}</p>
|
||||
<p className="text-lg font-bold">{formatCurrency(data?.onOrderRetail || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-lg flex justify-center font-medium mb-4">Purchase Orders By Vendor</div>
|
||||
<div className="text-md flex justify-center font-medium">Purchase Orders By Vendor</div>
|
||||
<div className="h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||
import { PieChart, Pie, ResponsiveContainer, Cell, Tooltip, Sector } from "recharts"
|
||||
import config from "@/config"
|
||||
import { formatCurrency } from "@/lib/utils"
|
||||
import { Package, Layers, DollarSign, ShoppingCart } from "lucide-react" // Importing icons
|
||||
import { Package, Layers, DollarSign, ShoppingCart } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
interface StockMetricsData {
|
||||
@@ -12,8 +12,8 @@ interface StockMetricsData {
|
||||
totalStockUnits: number
|
||||
totalStockCost: number
|
||||
totalStockRetail: number
|
||||
vendorStock: {
|
||||
vendor: string
|
||||
brandStock: {
|
||||
brand: string
|
||||
variants: number
|
||||
units: number
|
||||
cost: number
|
||||
@@ -33,10 +33,10 @@ const COLORS = [
|
||||
]
|
||||
|
||||
const renderActiveShape = (props: any) => {
|
||||
const { cx, cy, innerRadius, vendor, cost } = props;
|
||||
const { cx, cy, innerRadius, brand, retail } = props;
|
||||
|
||||
// Split vendor name into words and create lines of max 12 chars
|
||||
const words = vendor.split(' ');
|
||||
// Split brand name into words and create lines of max 12 chars
|
||||
const words = brand.split(' ');
|
||||
const lines: string[] = [];
|
||||
let currentLine = '';
|
||||
|
||||
@@ -73,7 +73,7 @@ const renderActiveShape = (props: any) => {
|
||||
fill="#000000"
|
||||
className="text-base font-medium"
|
||||
>
|
||||
{formatCurrency(cost)}
|
||||
{formatCurrency(retail)}
|
||||
</text>
|
||||
{props.children}
|
||||
</g>
|
||||
@@ -83,16 +83,24 @@ const renderActiveShape = (props: any) => {
|
||||
export function StockMetrics() {
|
||||
const [activeIndex, setActiveIndex] = useState<number | undefined>();
|
||||
|
||||
const { data } = useQuery<StockMetricsData>({
|
||||
const { data, error, isLoading } = useQuery<StockMetricsData>({
|
||||
queryKey: ["stock-metrics"],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/stock/metrics`)
|
||||
console.log('Fetching from:', `${config.apiUrl}/dashboard/stock/metrics`);
|
||||
const response = await fetch(`${config.apiUrl}/dashboard/stock/metrics`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch stock metrics")
|
||||
const text = await response.text();
|
||||
console.error('API Error:', text);
|
||||
throw new Error(`Failed to fetch stock metrics: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json()
|
||||
const data = await response.json();
|
||||
console.log('API Response:', data);
|
||||
return data;
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error loading stock metrics</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -108,48 +116,48 @@ export function StockMetrics() {
|
||||
<Package className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Products</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{data?.totalProducts.toLocaleString() || 0}</p>
|
||||
<p className="text-lg font-bold">{data?.totalProducts.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Products In Stock</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{data?.productsInStock.toLocaleString() || 0}</p>
|
||||
<p className="text-lg font-bold">{data?.productsInStock.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Layers className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Stock Units</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{data?.totalStockUnits.toLocaleString() || 0}</p>
|
||||
<p className="text-lg font-bold">{data?.totalStockUnits.toLocaleString() || 0}</p>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Stock Cost</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.totalStockCost || 0)}</p>
|
||||
<p className="text-lg font-bold">{formatCurrency(data?.totalStockCost || 0)}</p>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
|
||||
<p className="text-sm font-medium text-muted-foreground">Stock Retail</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold">{formatCurrency(data?.totalStockRetail || 0)}</p>
|
||||
<p className="text-lg font-bold">{formatCurrency(data?.totalStockRetail || 0)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-lg flex justify-center font-medium mb-4">Stock Retail By Brand</div>
|
||||
<div className="text-md flex justify-center font-medium ">Stock Retail By Brand</div>
|
||||
<div className="h-[200px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data?.vendorStock || []}
|
||||
dataKey="cost"
|
||||
nameKey="vendor"
|
||||
data={data?.brandStock || []}
|
||||
dataKey="retail"
|
||||
nameKey="brand"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
@@ -160,10 +168,11 @@ export function StockMetrics() {
|
||||
onMouseEnter={(_, index) => setActiveIndex(index)}
|
||||
onMouseLeave={() => setActiveIndex(undefined)}
|
||||
>
|
||||
{data?.vendorStock?.map((entry, index) => (
|
||||
{data?.brandStock?.map((entry, index) => (
|
||||
<Cell
|
||||
key={entry.vendor}
|
||||
key={entry.brand}
|
||||
fill={COLORS[index % COLORS.length]}
|
||||
style={{ filter: activeIndex === index ? 'brightness(1.2)' : undefined }}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
|
||||
Reference in New Issue
Block a user