218 lines
8.2 KiB
TypeScript
218 lines
8.2 KiB
TypeScript
import { useQuery } from "@tanstack/react-query";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { BarChart3, Download, TrendingUp, Package, AlertTriangle, FileText } from "lucide-react";
|
|
import type { Product, StockMovement } from "@shared/schema";
|
|
|
|
export default function Reports() {
|
|
const { data: products } = useQuery<Product[]>({
|
|
queryKey: ["/api/products"],
|
|
});
|
|
|
|
const { data: stockMovements } = useQuery<StockMovement[]>({
|
|
queryKey: ["/api/stock-movements"],
|
|
});
|
|
|
|
const { data: stats } = useQuery<{
|
|
totalItems: number;
|
|
inStockItems: number;
|
|
lowStockItems: number;
|
|
outOfStockItems: number;
|
|
}>({
|
|
queryKey: ["/api/dashboard/stats"],
|
|
});
|
|
|
|
const totalValue = products?.reduce((sum, product) => {
|
|
const price = parseFloat(product.price || "0");
|
|
return sum + (price * product.currentStock);
|
|
}, 0) || 0;
|
|
|
|
const recentMovements = stockMovements?.slice(0, 10) || [];
|
|
|
|
const generateReport = (type: string) => {
|
|
// In a real app, this would generate and download a proper report
|
|
console.log(`Generating ${type} report...`);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-3xl font-bold text-gray-800" data-testid="text-reports-title">
|
|
Reports & Analytics
|
|
</h1>
|
|
<Button
|
|
onClick={() => generateReport('comprehensive')}
|
|
className="bg-primary hover:bg-primary-dark"
|
|
data-testid="button-generate-report"
|
|
>
|
|
<Download className="w-4 h-4 mr-2" />
|
|
Generate Report
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-blue-600 uppercase tracking-wide">Total Items</p>
|
|
<p className="text-3xl font-bold text-blue-800" data-testid="text-total-items">
|
|
{stats?.totalItems || 0}
|
|
</p>
|
|
</div>
|
|
<Package className="w-8 h-8 text-blue-400" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-green-600 uppercase tracking-wide">Total Value</p>
|
|
<p className="text-3xl font-bold text-green-800" data-testid="text-total-value">
|
|
${totalValue.toFixed(2)}
|
|
</p>
|
|
</div>
|
|
<TrendingUp className="w-8 h-8 text-green-400" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-orange-600 uppercase tracking-wide">Low Stock</p>
|
|
<p className="text-3xl font-bold text-orange-800" data-testid="text-low-stock-count">
|
|
{stats?.lowStockItems || 0}
|
|
</p>
|
|
</div>
|
|
<AlertTriangle className="w-8 h-8 text-orange-400" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-red-600 uppercase tracking-wide">Out of Stock</p>
|
|
<p className="text-3xl font-bold text-red-800" data-testid="text-out-of-stock-count">
|
|
{stats?.outOfStockItems || 0}
|
|
</p>
|
|
</div>
|
|
<AlertTriangle className="w-8 h-8 text-red-400" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Report Types */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center space-x-2">
|
|
<FileText className="w-5 h-5" />
|
|
<span>Available Reports</span>
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div className="border border-gray-200 rounded-lg p-4 hover:border-primary hover:bg-blue-50 transition-all cursor-pointer">
|
|
<div className="flex items-center space-x-3 mb-2">
|
|
<BarChart3 className="w-6 h-6 text-primary" />
|
|
<h3 className="font-semibold">Inventory Summary</h3>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
Complete overview of all products, stock levels, and values
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => generateReport('inventory')}
|
|
data-testid="button-generate-inventory-report"
|
|
>
|
|
Generate
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="border border-gray-200 rounded-lg p-4 hover:border-primary hover:bg-blue-50 transition-all cursor-pointer">
|
|
<div className="flex items-center space-x-3 mb-2">
|
|
<TrendingUp className="w-6 h-6 text-primary" />
|
|
<h3 className="font-semibold">Stock Movements</h3>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
Detailed log of all stock ins and outs over time
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => generateReport('movements')}
|
|
data-testid="button-generate-movements-report"
|
|
>
|
|
Generate
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="border border-gray-200 rounded-lg p-4 hover:border-primary hover:bg-blue-50 transition-all cursor-pointer">
|
|
<div className="flex items-center space-x-3 mb-2">
|
|
<AlertTriangle className="w-6 h-6 text-primary" />
|
|
<h3 className="font-semibold">Stock Alerts</h3>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
Products that need attention due to low or zero stock
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => generateReport('alerts')}
|
|
data-testid="button-generate-alerts-report"
|
|
>
|
|
Generate
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Recent Activity */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Recent Stock Movements</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{recentMovements.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{recentMovements.map((movement) => {
|
|
const product = products?.find(p => p.id === movement.productId);
|
|
return (
|
|
<div key={movement.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
|
<div>
|
|
<p className="font-medium" data-testid={`text-movement-product-${movement.id}`}>
|
|
{product?.name || 'Unknown Product'}
|
|
</p>
|
|
<p className="text-sm text-gray-600">
|
|
{movement.type === 'in' ? 'Added' : movement.type === 'out' ? 'Removed' : 'Adjusted'} {movement.quantity} units
|
|
</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-sm text-gray-500">
|
|
{movement.createdAt ? new Date(movement.createdAt).toLocaleDateString() : 'Unknown'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<p className="text-center text-gray-500 py-8" data-testid="text-no-movements">
|
|
No recent stock movements
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|