659 lines
26 KiB
TypeScript
659 lines
26 KiB
TypeScript
'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>
|
|
)
|
|
}
|