Initial commit
This commit is contained in:
658
src/components/reports/ReportsModule.tsx
Normal file
658
src/components/reports/ReportsModule.tsx
Normal file
@@ -0,0 +1,658 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@/components/ui/chart'
|
||||
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Legend, ResponsiveContainer } from 'recharts'
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
DollarSign,
|
||||
Users,
|
||||
Package,
|
||||
AlertTriangle,
|
||||
Download,
|
||||
Calendar,
|
||||
ArrowRight
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { format, startOfDay, endOfDay, startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfYear, endOfYear, subDays } from 'date-fns'
|
||||
import { fr } from 'date-fns/locale'
|
||||
|
||||
// Types
|
||||
interface KPICardProps {
|
||||
title: string
|
||||
value: string | number
|
||||
change?: number
|
||||
icon: React.ReactNode
|
||||
trend?: 'up' | 'down' | 'neutral'
|
||||
}
|
||||
|
||||
interface DashboardData {
|
||||
totalSales: {
|
||||
today: number
|
||||
week: number
|
||||
month: number
|
||||
year: number
|
||||
}
|
||||
revenue: {
|
||||
htToday: number
|
||||
ttcToday: number
|
||||
htMonth: number
|
||||
ttcMonth: number
|
||||
}
|
||||
totalClients: number
|
||||
topProducts: {
|
||||
designation: string
|
||||
quantity: number
|
||||
revenue: number
|
||||
}[]
|
||||
lowStockItems: {
|
||||
id: string
|
||||
reference: string
|
||||
designation: string
|
||||
stock: number
|
||||
stockMin: number
|
||||
}[]
|
||||
pendingWorkshopOrders: number
|
||||
}
|
||||
|
||||
interface SalesReportData {
|
||||
salesByDate: {
|
||||
date: string
|
||||
sales: number
|
||||
revenue: number
|
||||
}[]
|
||||
salesByCategory: {
|
||||
category: string
|
||||
count: number
|
||||
revenue: number
|
||||
}[]
|
||||
salesByEmployee: {
|
||||
employee: string
|
||||
sales: number
|
||||
revenue: number
|
||||
}[]
|
||||
salesByPaymentMethod: {
|
||||
method: string
|
||||
count: number
|
||||
amount: number
|
||||
}[]
|
||||
}
|
||||
|
||||
interface InventoryReportData {
|
||||
stockValuation: {
|
||||
totalValue: number
|
||||
byCategory: {
|
||||
category: string
|
||||
value: number
|
||||
count: number
|
||||
}[]
|
||||
}
|
||||
lowStockItems: {
|
||||
id: string
|
||||
reference: string
|
||||
designation: string
|
||||
category: string
|
||||
stock: number
|
||||
stockMin: number
|
||||
value: number
|
||||
}[]
|
||||
categoryBreakdown: {
|
||||
category: string
|
||||
totalProducts: number
|
||||
activeProducts: number
|
||||
totalStock: number
|
||||
stockValue: number
|
||||
}[]
|
||||
}
|
||||
|
||||
// KPI Card Component
|
||||
function KPICard({ title, value, change, icon, trend = 'neutral' }: KPICardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
<div className="text-muted-foreground">{icon}</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{typeof value === 'number' ? value.toLocaleString() : value}</div>
|
||||
{change !== undefined && (
|
||||
<p className={`text-xs flex items-center gap-1 mt-1 ${trend === 'up' ? 'text-green-600' : trend === 'down' ? 'text-red-600' : 'text-muted-foreground'}`}>
|
||||
{trend === 'up' && <TrendingUp className="h-3 w-3" />}
|
||||
{trend === 'down' && <TrendingDown className="h-3 w-3" />}
|
||||
{change > 0 ? '+' : ''}{change.toFixed(1)}% par rapport à hier
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Chart colors
|
||||
const CHART_COLORS = {
|
||||
sales: '#10b981',
|
||||
revenue: '#3b82f6',
|
||||
monture: '#8b5cf6',
|
||||
verre: '#06b6d4',
|
||||
lentille: '#f59e0b',
|
||||
accessoire: '#ec4899',
|
||||
especes: '#10b981',
|
||||
carte: '#3b82f6',
|
||||
cheque: '#f59e0b',
|
||||
virement: '#8b5cf6',
|
||||
bonCaisse: '#ec4899',
|
||||
}
|
||||
|
||||
export default function ReportsModule() {
|
||||
const [activeTab, setActiveTab] = useState('dashboard')
|
||||
const [dateRange, setDateRange] = useState<'today' | 'week' | 'month' | 'year' | 'custom'>('month')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null)
|
||||
const [salesData, setSalesData] = useState<SalesReportData | null>(null)
|
||||
const [inventoryData, setInventoryData] = useState<InventoryReportData | null>(null)
|
||||
|
||||
// Fetch dashboard data
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await fetch('/api/reports/dashboard')
|
||||
if (!response.ok) throw new Error('Failed to fetch dashboard data')
|
||||
const data = await response.json()
|
||||
setDashboardData(data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard data:', error)
|
||||
toast.error('Erreur lors du chargement des données du tableau de bord')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch sales report data
|
||||
const fetchSalesData = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/reports/sales?range=${dateRange}`)
|
||||
if (!response.ok) throw new Error('Failed to fetch sales data')
|
||||
const data = await response.json()
|
||||
setSalesData(data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching sales data:', error)
|
||||
toast.error('Erreur lors du chargement des données de ventes')
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch inventory report data
|
||||
const fetchInventoryData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/reports/inventory')
|
||||
if (!response.ok) throw new Error('Failed to fetch inventory data')
|
||||
const data = await response.json()
|
||||
setInventoryData(data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching inventory data:', error)
|
||||
toast.error('Erreur lors du chargement des données de stock')
|
||||
}
|
||||
}
|
||||
|
||||
// Export data to CSV
|
||||
const exportToCSV = async (type: 'sales' | 'inventory' | 'lowStock') => {
|
||||
try {
|
||||
const response = await fetch(`/api/reports/export/${type}?range=${dateRange}`)
|
||||
if (!response.ok) throw new Error('Failed to export data')
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${type}_${format(new Date(), 'yyyy-MM-dd')}.csv`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
|
||||
toast.success('Export CSV réussi')
|
||||
} catch (error) {
|
||||
console.error('Error exporting data:', error)
|
||||
toast.error('Erreur lors de l\'export des données')
|
||||
}
|
||||
}
|
||||
|
||||
// Load data based on active tab
|
||||
useEffect(() => {
|
||||
if (activeTab === 'dashboard') {
|
||||
fetchDashboardData()
|
||||
} else if (activeTab === 'sales') {
|
||||
fetchSalesData()
|
||||
} else if (activeTab === 'inventory') {
|
||||
fetchInventoryData()
|
||||
}
|
||||
}, [activeTab, dateRange])
|
||||
|
||||
// Initial data load
|
||||
useEffect(() => {
|
||||
fetchDashboardData()
|
||||
}, [])
|
||||
|
||||
const chartConfig = {
|
||||
sales: { label: 'Ventes', color: CHART_COLORS.sales },
|
||||
revenue: { label: 'Chiffre d\'affaires', color: CHART_COLORS.revenue },
|
||||
monture: { label: 'Montures', color: CHART_COLORS.monture },
|
||||
verre: { label: 'Verres', color: CHART_COLORS.verre },
|
||||
lentille: { label: 'Lentilles', color: CHART_COLORS.lentille },
|
||||
accessoire: { label: 'Accessoires', color: CHART_COLORS.accessoire },
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Rapports</h1>
|
||||
<p className="text-muted-foreground">Tableau de bord et statistiques de votre magasin</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={dateRange} onValueChange={(value: any) => setDateRange(value)}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<Calendar className="mr-2 h-4 w-4" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="today">Aujourd'hui</SelectItem>
|
||||
<SelectItem value="week">Cette semaine</SelectItem>
|
||||
<SelectItem value="month">Ce mois</SelectItem>
|
||||
<SelectItem value="year">Cette année</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{activeTab !== 'dashboard' && (
|
||||
<Button onClick={() => exportToCSV(activeTab === 'sales' ? 'sales' : activeTab === 'inventory' ? 'inventory' : 'lowStock')}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Exporter CSV
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="dashboard">Tableau de bord</TabsTrigger>
|
||||
<TabsTrigger value="sales">Rapports de ventes</TabsTrigger>
|
||||
<TabsTrigger value="inventory">Rapports de stock</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Dashboard Tab */}
|
||||
<TabsContent value="dashboard" className="space-y-4">
|
||||
{loading && !dashboardData ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-muted-foreground">Chargement...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* KPI Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<KPICard
|
||||
title="Ventes du jour"
|
||||
value={dashboardData?.totalSales.today || 0}
|
||||
icon={<Package className="h-4 w-4" />}
|
||||
/>
|
||||
<KPICard
|
||||
title="CA du jour (TTC)"
|
||||
value={`${dashboardData?.revenue.ttcToday.toFixed(2) || 0} €`}
|
||||
icon={<DollarSign className="h-4 w-4" />}
|
||||
/>
|
||||
<KPICard
|
||||
title="Clients total"
|
||||
value={dashboardData?.totalClients || 0}
|
||||
icon={<Users className="h-4 w-4" />}
|
||||
/>
|
||||
<KPICard
|
||||
title="Commandes atelier en attente"
|
||||
value={dashboardData?.pendingWorkshopOrders || 0}
|
||||
icon={<AlertTriangle className="h-4 w-4" />}
|
||||
trend={dashboardData?.pendingWorkshopOrders && dashboardData.pendingWorkshopOrders > 0 ? 'down' : 'neutral'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Additional KPIs */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<KPICard
|
||||
title="Ventes de la semaine"
|
||||
value={dashboardData?.totalSales.week || 0}
|
||||
icon={<TrendingUp className="h-4 w-4" />}
|
||||
/>
|
||||
<KPICard
|
||||
title="CA de la semaine (TTC)"
|
||||
value={`${dashboardData?.revenue.htMonth.toFixed(2) || 0} €`}
|
||||
icon={<DollarSign className="h-4 w-4" />}
|
||||
/>
|
||||
<KPICard
|
||||
title="Ventes du mois"
|
||||
value={dashboardData?.totalSales.month || 0}
|
||||
icon={<TrendingUp className="h-4 w-4" />}
|
||||
/>
|
||||
<KPICard
|
||||
title="CA du mois (TTC)"
|
||||
value={`${dashboardData?.revenue.ttcMonth.toFixed(2) || 0} €`}
|
||||
icon={<DollarSign className="h-4 w-4" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Top Products and Low Stock */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* Top Selling Products */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Top 5 des produits vendus</CardTitle>
|
||||
<CardDescription>Les produits les plus populaires ce mois</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[300px]">
|
||||
<div className="space-y-4">
|
||||
{dashboardData?.topProducts.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">Aucune vente ce mois</p>
|
||||
) : (
|
||||
dashboardData?.topProducts.map((product, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{product.designation}</p>
|
||||
<p className="text-sm text-muted-foreground">{product.quantity} vendus</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-bold">{product.revenue.toFixed(2)} €</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Low Stock Alerts */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
Alertes de stock
|
||||
<Badge variant="destructive">{dashboardData?.lowStockItems.length || 0}</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>Produits en dessous du stock minimum</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[300px]">
|
||||
<div className="space-y-3">
|
||||
{dashboardData?.lowStockItems.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">Aucune alerte de stock</p>
|
||||
) : (
|
||||
dashboardData?.lowStockItems.map((item) => (
|
||||
<div key={item.id} className="flex items-center justify-between p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-destructive">{item.designation}</p>
|
||||
<p className="text-sm text-muted-foreground">{item.reference}</p>
|
||||
</div>
|
||||
<Badge variant="destructive" className="flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
{item.stock} / {item.stockMin}
|
||||
</Badge>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Sales Reports Tab */}
|
||||
<TabsContent value="sales" className="space-y-4">
|
||||
{loading && !salesData ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-muted-foreground">Chargement...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Sales by Date Chart */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Évolution des ventes</CardTitle>
|
||||
<CardDescription>Ventes et chiffre d'affaires par période</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-[350px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={salesData?.salesByDate || []}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis yAxisId="left" />
|
||||
<YAxis yAxisId="right" orientation="right" />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Legend />
|
||||
<Bar yAxisId="left" dataKey="sales" fill={CHART_COLORS.sales} name="Ventes" />
|
||||
<Bar yAxisId="right" dataKey="revenue" fill={CHART_COLORS.revenue} name="CA (€)" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* Sales by Category Pie Chart */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ventes par catégorie</CardTitle>
|
||||
<CardDescription>Répartition des ventes par type de produit</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={salesData?.salesByCategory || []}
|
||||
dataKey="revenue"
|
||||
nameKey="category"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ category, percent }) => `${category} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{salesData?.salesByCategory.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={Object.values(CHART_COLORS)[index % Object.values(CHART_COLORS).length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Sales by Payment Method */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Mode de paiement</CardTitle>
|
||||
<CardDescription>Répartition des paiements</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig} className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={salesData?.salesByPaymentMethod || []} layout="vertical">
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis type="number" />
|
||||
<YAxis dataKey="method" type="category" width={100} />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="amount" fill={CHART_COLORS.revenue} name="Montant (€)" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sales by Employee Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ventes par employé</CardTitle>
|
||||
<CardDescription>Performance de l'équipe</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Employé</TableHead>
|
||||
<TableHead className="text-right">Nombre de ventes</TableHead>
|
||||
<TableHead className="text-right">Chiffre d'affaires</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{salesData?.salesByEmployee.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center text-muted-foreground">
|
||||
Aucune donnée disponible
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
salesData?.salesByEmployee.map((employee, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="font-medium">{employee.employee}</TableCell>
|
||||
<TableCell className="text-right">{employee.sales}</TableCell>
|
||||
<TableCell className="text-right font-bold">{employee.revenue.toFixed(2)} €</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Inventory Reports Tab */}
|
||||
<TabsContent value="inventory" className="space-y-4">
|
||||
{loading && !inventoryData ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<p className="text-muted-foreground">Chargement...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Stock Valuation */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Valorisation du stock</CardTitle>
|
||||
<CardDescription>Valeur totale du stock par catégorie</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-muted-foreground">Valeur totale du stock</p>
|
||||
<p className="text-3xl font-bold">{inventoryData?.stockValuation.totalValue.toFixed(2)} € HT</p>
|
||||
</div>
|
||||
<ChartContainer config={chartConfig} className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={inventoryData?.stockValuation.byCategory || []}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="category" />
|
||||
<YAxis />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" fill={CHART_COLORS.monture} name="Valeur (€ HT)" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* Category Breakdown Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Répartition par catégorie</CardTitle>
|
||||
<CardDescription>Détails du stock par type de produit</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[300px]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Catégorie</TableHead>
|
||||
<TableHead className="text-right">Produits</TableHead>
|
||||
<TableHead className="text-right">Stock</TableHead>
|
||||
<TableHead className="text-right">Valeur</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{inventoryData?.categoryBreakdown.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground">
|
||||
Aucune donnée disponible
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
inventoryData?.categoryBreakdown.map((cat) => (
|
||||
<TableRow key={cat.category}>
|
||||
<TableCell className="font-medium">{cat.category}</TableCell>
|
||||
<TableCell className="text-right">{cat.activeProducts} / {cat.totalProducts}</TableCell>
|
||||
<TableCell className="text-right">{cat.totalStock}</TableCell>
|
||||
<TableCell className="text-right font-bold">{cat.stockValue.toFixed(2)} €</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Low Stock Details */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
Produits en stock faible
|
||||
<Badge variant="destructive">{inventoryData?.lowStockItems.length || 0}</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>Produits nécessitant un réapprovisionnement</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[300px]">
|
||||
<div className="space-y-2">
|
||||
{inventoryData?.lowStockItems.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">Tous les stocks sont OK</p>
|
||||
) : (
|
||||
inventoryData?.lowStockItems.map((item) => (
|
||||
<div key={item.id} className="p-3 border rounded-lg flex items-center justify-between hover:bg-muted/50">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-sm">{item.designation}</p>
|
||||
<p className="text-xs text-muted-foreground">{item.reference} • {item.category}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-bold text-destructive">{item.stock} unités</p>
|
||||
<p className="text-xs text-muted-foreground">Min: {item.stockMin}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user