First template code generated from replit.com
This commit is contained in:
217
client/src/pages/reports.tsx
Normal file
217
client/src/pages/reports.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user