Files
wareApp/client/src/pages/reports.tsx

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