Files
OpticZ/src/components/reports/ReportsModule.tsx
2026-05-30 14:33:11 +01:00

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