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) => { router.get('/stock/metrics', async (req, res) => {
try { try {
// Get stock metrics // Get stock metrics
const [stockMetrics] = await executeQuery(` const [rows] = await executeQuery(`
SELECT SELECT
COALESCE(COUNT(*), 0) as total_products, COALESCE(COUNT(*), 0) as total_products,
COALESCE(COUNT(CASE WHEN stock_quantity > 0 THEN 1 END), 0) as products_in_stock, 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(CASE WHEN stock_quantity > 0 THEN stock_quantity END), 0) as total_units,
COALESCE(SUM(stock_quantity * cost_price), 0) as total_cost, COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity * cost_price END), 0) as total_cost,
COALESCE(SUM(stock_quantity * price), 0) as total_retail COALESCE(SUM(CASE WHEN stock_quantity > 0 THEN stock_quantity * price END), 0) as total_retail
FROM products FROM products
`); `);
const stockMetrics = rows[0];
// Get vendor stock values console.log('Raw stockMetrics from database:', stockMetrics);
const [vendorValues] = await executeQuery(` console.log('stockMetrics.total_products:', stockMetrics.total_products);
SELECT console.log('stockMetrics.products_in_stock:', stockMetrics.products_in_stock);
vendor, console.log('stockMetrics.total_units:', stockMetrics.total_units);
COUNT(DISTINCT product_id) as variant_count, console.log('stockMetrics.total_cost:', stockMetrics.total_cost);
COALESCE(SUM(stock_quantity), 0) as stock_units, console.log('stockMetrics.total_retail:', stockMetrics.total_retail);
COALESCE(SUM(stock_quantity * cost_price), 0) as stock_cost,
COALESCE(SUM(stock_quantity * price), 0) as stock_retail // Get brand stock values with Other category
FROM products const [brandValues] = await executeQuery(`
WHERE vendor IS NOT NULL WITH brand_totals AS (
AND stock_quantity > 0 SELECT
GROUP BY vendor brand,
HAVING stock_cost > 0 COUNT(DISTINCT product_id) as variant_count,
ORDER BY stock_cost DESC 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 // Format the response with explicit type conversion
@@ -49,8 +79,8 @@ router.get('/stock/metrics', async (req, res) => {
totalStockUnits: parseInt(stockMetrics.total_units) || 0, totalStockUnits: parseInt(stockMetrics.total_units) || 0,
totalStockCost: parseFloat(stockMetrics.total_cost) || 0, totalStockCost: parseFloat(stockMetrics.total_cost) || 0,
totalStockRetail: parseFloat(stockMetrics.total_retail) || 0, totalStockRetail: parseFloat(stockMetrics.total_retail) || 0,
vendorStock: vendorValues.map(v => ({ brandStock: brandValues.map(v => ({
vendor: v.vendor, brand: v.brand,
variants: parseInt(v.variant_count) || 0, variants: parseInt(v.variant_count) || 0,
units: parseInt(v.stock_units) || 0, units: parseInt(v.stock_units) || 0,
cost: parseFloat(v.stock_cost) || 0, cost: parseFloat(v.stock_cost) || 0,
@@ -69,7 +99,7 @@ router.get('/stock/metrics', async (req, res) => {
// Returns purchase order metrics by vendor // Returns purchase order metrics by vendor
router.get('/purchase/metrics', async (req, res) => { router.get('/purchase/metrics', async (req, res) => {
try { try {
const [poMetrics] = await executeQuery(` const [rows] = await executeQuery(`
SELECT SELECT
COALESCE(COUNT(DISTINCT CASE WHEN po.status = 'open' THEN po.po_id END), 0) as active_pos, COALESCE(COUNT(DISTINCT CASE WHEN po.status = 'open' THEN po.po_id END), 0) as active_pos,
COALESCE(COUNT(DISTINCT CASE COALESCE(COUNT(DISTINCT CASE
@@ -90,6 +120,14 @@ router.get('/purchase/metrics', async (req, res) => {
FROM purchase_orders po FROM purchase_orders po
JOIN products p ON po.product_id = p.product_id 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(` const [vendorOrders] = await executeQuery(`
SELECT SELECT
@@ -106,7 +144,7 @@ router.get('/purchase/metrics', async (req, res) => {
ORDER BY order_cost DESC ORDER BY order_cost DESC
`); `);
res.json({ const response = {
activePurchaseOrders: parseInt(poMetrics.active_pos) || 0, activePurchaseOrders: parseInt(poMetrics.active_pos) || 0,
overduePurchaseOrders: parseInt(poMetrics.overdue_pos) || 0, overduePurchaseOrders: parseInt(poMetrics.overdue_pos) || 0,
onOrderUnits: parseInt(poMetrics.total_units) || 0, onOrderUnits: parseInt(poMetrics.total_units) || 0,
@@ -119,7 +157,9 @@ router.get('/purchase/metrics', async (req, res) => {
cost: parseFloat(v.order_cost) || 0, cost: parseFloat(v.order_cost) || 0,
retail: parseFloat(v.order_retail) || 0 retail: parseFloat(v.order_retail) || 0
})) }))
}); };
res.json(response);
} catch (err) { } catch (err) {
console.error('Error fetching purchase metrics:', err); console.error('Error fetching purchase metrics:', err);
res.status(500).json({ error: 'Failed to fetch purchase metrics' }); res.status(500).json({ error: 'Failed to fetch purchase metrics' });

View File

@@ -83,17 +83,25 @@ const renderActiveShape = (props: any) => {
export function PurchaseMetrics() { export function PurchaseMetrics() {
const [activeIndex, setActiveIndex] = useState<number | undefined>(); const [activeIndex, setActiveIndex] = useState<number | undefined>();
const { data } = useQuery<PurchaseMetricsData>({ const { data, error, isLoading } = useQuery<PurchaseMetricsData>({
queryKey: ["purchase-metrics"], queryKey: ["purchase-metrics"],
queryFn: async () => { queryFn: async () => {
console.log('Fetching from:', `${config.apiUrl}/dashboard/purchase/metrics`);
const response = await fetch(`${config.apiUrl}/dashboard/purchase/metrics`) const response = await fetch(`${config.apiUrl}/dashboard/purchase/metrics`)
if (!response.ok) { 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 ( return (
<> <>
<CardHeader> <CardHeader>
@@ -108,41 +116,41 @@ export function PurchaseMetrics() {
<ClipboardList className="h-4 w-4 text-muted-foreground" /> <ClipboardList className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Active Purchase Orders</p> <p className="text-sm font-medium text-muted-foreground">Active Purchase Orders</p>
</div> </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>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-muted-foreground" /> <AlertCircle className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Overdue Purchase Orders</p> <p className="text-sm font-medium text-muted-foreground">Overdue Purchase Orders</p>
</div> </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>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-muted-foreground" /> <Layers className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">On Order Units</p> <p className="text-sm font-medium text-muted-foreground">On Order Units</p>
</div> </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>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<DollarSign className="h-4 w-4 text-muted-foreground" /> <DollarSign className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">On Order Cost</p> <p className="text-sm font-medium text-muted-foreground">On Order Cost</p>
</div> </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>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ShoppingCart className="h-4 w-4 text-muted-foreground" /> <ShoppingCart className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">On Order Retail</p> <p className="text-sm font-medium text-muted-foreground">On Order Retail</p>
</div> </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>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex flex-col"> <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]"> <div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <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 { PieChart, Pie, ResponsiveContainer, Cell, Tooltip, Sector } from "recharts"
import config from "@/config" import config from "@/config"
import { formatCurrency } from "@/lib/utils" 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" import { useState } from "react"
interface StockMetricsData { interface StockMetricsData {
@@ -12,8 +12,8 @@ interface StockMetricsData {
totalStockUnits: number totalStockUnits: number
totalStockCost: number totalStockCost: number
totalStockRetail: number totalStockRetail: number
vendorStock: { brandStock: {
vendor: string brand: string
variants: number variants: number
units: number units: number
cost: number cost: number
@@ -33,10 +33,10 @@ const COLORS = [
] ]
const renderActiveShape = (props: any) => { 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 // Split brand name into words and create lines of max 12 chars
const words = vendor.split(' '); const words = brand.split(' ');
const lines: string[] = []; const lines: string[] = [];
let currentLine = ''; let currentLine = '';
@@ -73,7 +73,7 @@ const renderActiveShape = (props: any) => {
fill="#000000" fill="#000000"
className="text-base font-medium" className="text-base font-medium"
> >
{formatCurrency(cost)} {formatCurrency(retail)}
</text> </text>
{props.children} {props.children}
</g> </g>
@@ -83,16 +83,24 @@ const renderActiveShape = (props: any) => {
export function StockMetrics() { export function StockMetrics() {
const [activeIndex, setActiveIndex] = useState<number | undefined>(); const [activeIndex, setActiveIndex] = useState<number | undefined>();
const { data } = useQuery<StockMetricsData>({ const { data, error, isLoading } = useQuery<StockMetricsData>({
queryKey: ["stock-metrics"], queryKey: ["stock-metrics"],
queryFn: async () => { 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) { 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 ( return (
<> <>
@@ -108,48 +116,48 @@ export function StockMetrics() {
<Package className="h-4 w-4 text-muted-foreground" /> <Package className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Products</p> <p className="text-sm font-medium text-muted-foreground">Products</p>
</div> </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>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-muted-foreground" /> <Layers className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Products In Stock</p> <p className="text-sm font-medium text-muted-foreground">Products In Stock</p>
</div> </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>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-muted-foreground" /> <Layers className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Stock Units</p> <p className="text-sm font-medium text-muted-foreground">Stock Units</p>
</div> </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>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<DollarSign className="h-4 w-4 text-muted-foreground" /> <DollarSign className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Stock Cost</p> <p className="text-sm font-medium text-muted-foreground">Stock Cost</p>
</div> </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>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ShoppingCart className="h-4 w-4 text-muted-foreground" /> <ShoppingCart className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Stock Retail</p> <p className="text-sm font-medium text-muted-foreground">Stock Retail</p>
</div> </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>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex flex-col"> <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]"> <div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<PieChart> <PieChart>
<Pie <Pie
data={data?.vendorStock || []} data={data?.brandStock || []}
dataKey="cost" dataKey="retail"
nameKey="vendor" nameKey="brand"
cx="50%" cx="50%"
cy="50%" cy="50%"
innerRadius={60} innerRadius={60}
@@ -160,10 +168,11 @@ export function StockMetrics() {
onMouseEnter={(_, index) => setActiveIndex(index)} onMouseEnter={(_, index) => setActiveIndex(index)}
onMouseLeave={() => setActiveIndex(undefined)} onMouseLeave={() => setActiveIndex(undefined)}
> >
{data?.vendorStock?.map((entry, index) => ( {data?.brandStock?.map((entry, index) => (
<Cell <Cell
key={entry.vendor} key={entry.brand}
fill={COLORS[index % COLORS.length]} fill={COLORS[index % COLORS.length]}
style={{ filter: activeIndex === index ? 'brightness(1.2)' : undefined }}
/> />
))} ))}
</Pie> </Pie>