Fix and restyle stock and purchases components

This commit is contained in:
2025-01-17 21:10:35 -05:00
parent de5bd785c1
commit f38174ca2a
3 changed files with 113 additions and 56 deletions

View File

@@ -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' });

View File

@@ -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>

View File

@@ -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>