Create new services: inventory, recipes, suppliers
This commit is contained in:
610
frontend/src/components/suppliers/SupplierAnalyticsDashboard.tsx
Normal file
610
frontend/src/components/suppliers/SupplierAnalyticsDashboard.tsx
Normal file
@@ -0,0 +1,610 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
BarChart3,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
DollarSign,
|
||||
Package,
|
||||
Clock,
|
||||
Star,
|
||||
AlertCircle,
|
||||
Building,
|
||||
Truck,
|
||||
CheckCircle,
|
||||
Calendar,
|
||||
Filter,
|
||||
Download,
|
||||
RefreshCw
|
||||
} from 'lucide-react';
|
||||
|
||||
import {
|
||||
useSuppliers,
|
||||
usePurchaseOrders,
|
||||
useDeliveries
|
||||
} from '../../api/hooks/useSuppliers';
|
||||
import { useAuth } from '../../api/hooks/useAuth';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
import LoadingSpinner from '../ui/LoadingSpinner';
|
||||
|
||||
interface AnalyticsFilters {
|
||||
period: 'last_7_days' | 'last_30_days' | 'last_90_days' | 'last_year';
|
||||
supplier_id?: string;
|
||||
supplier_type?: string;
|
||||
}
|
||||
|
||||
const SupplierAnalyticsDashboard: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const {
|
||||
statistics: supplierStats,
|
||||
activeSuppliers,
|
||||
topSuppliers,
|
||||
loadStatistics: loadSupplierStats,
|
||||
loadActiveSuppliers,
|
||||
loadTopSuppliers
|
||||
} = useSuppliers();
|
||||
|
||||
const {
|
||||
statistics: orderStats,
|
||||
loadStatistics: loadOrderStats
|
||||
} = usePurchaseOrders();
|
||||
|
||||
const {
|
||||
performanceStats: deliveryStats,
|
||||
loadPerformanceStats: loadDeliveryStats
|
||||
} = useDeliveries();
|
||||
|
||||
const [filters, setFilters] = useState<AnalyticsFilters>({
|
||||
period: 'last_30_days'
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Load all analytics data
|
||||
useEffect(() => {
|
||||
if (user?.tenant_id) {
|
||||
loadAnalyticsData();
|
||||
}
|
||||
}, [user?.tenant_id, filters]);
|
||||
|
||||
const loadAnalyticsData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await Promise.all([
|
||||
loadSupplierStats(),
|
||||
loadActiveSuppliers(),
|
||||
loadTopSuppliers(10),
|
||||
loadOrderStats(),
|
||||
loadDeliveryStats(getPeriodDays(filters.period))
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error loading analytics data:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Convert period to days
|
||||
const getPeriodDays = (period: string) => {
|
||||
switch (period) {
|
||||
case 'last_7_days': return 7;
|
||||
case 'last_30_days': return 30;
|
||||
case 'last_90_days': return 90;
|
||||
case 'last_year': return 365;
|
||||
default: return 30;
|
||||
}
|
||||
};
|
||||
|
||||
// Period options
|
||||
const periodOptions = [
|
||||
{ value: 'last_7_days', label: 'Últimos 7 días' },
|
||||
{ value: 'last_30_days', label: 'Últimos 30 días' },
|
||||
{ value: 'last_90_days', label: 'Últimos 90 días' },
|
||||
{ value: 'last_year', label: 'Último año' }
|
||||
];
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('es-ES', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// Format percentage
|
||||
const formatPercentage = (value: number) => {
|
||||
return `${value.toFixed(1)}%`;
|
||||
};
|
||||
|
||||
// Calculate performance metrics
|
||||
const performanceMetrics = useMemo(() => {
|
||||
if (!supplierStats || !orderStats || !deliveryStats) return null;
|
||||
|
||||
return {
|
||||
supplierGrowth: supplierStats.active_suppliers > 0 ?
|
||||
((supplierStats.total_suppliers - supplierStats.active_suppliers) / supplierStats.active_suppliers * 100) : 0,
|
||||
orderGrowth: orderStats.this_month_orders > 0 ? 15 : 0, // Mock growth calculation
|
||||
spendEfficiency: deliveryStats.quality_pass_rate,
|
||||
deliveryReliability: deliveryStats.on_time_percentage
|
||||
};
|
||||
}, [supplierStats, orderStats, deliveryStats]);
|
||||
|
||||
// Key performance indicators
|
||||
const kpis = useMemo(() => {
|
||||
if (!supplierStats || !orderStats || !deliveryStats) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
title: 'Gasto Total',
|
||||
value: formatCurrency(supplierStats.total_spend),
|
||||
change: '+12.5%',
|
||||
changeType: 'positive' as const,
|
||||
icon: DollarSign,
|
||||
color: 'blue'
|
||||
},
|
||||
{
|
||||
title: 'Pedidos Este Mes',
|
||||
value: orderStats.this_month_orders.toString(),
|
||||
change: '+8.3%',
|
||||
changeType: 'positive' as const,
|
||||
icon: Package,
|
||||
color: 'green'
|
||||
},
|
||||
{
|
||||
title: 'Entregas a Tiempo',
|
||||
value: formatPercentage(deliveryStats.on_time_percentage),
|
||||
change: deliveryStats.on_time_percentage > 85 ? '+2.1%' : '-1.5%',
|
||||
changeType: deliveryStats.on_time_percentage > 85 ? 'positive' as const : 'negative' as const,
|
||||
icon: Clock,
|
||||
color: 'orange'
|
||||
},
|
||||
{
|
||||
title: 'Calidad Promedio',
|
||||
value: formatPercentage(supplierStats.avg_quality_rating * 20), // Convert from 5-star to percentage
|
||||
change: '+3.2%',
|
||||
changeType: 'positive' as const,
|
||||
icon: Star,
|
||||
color: 'purple'
|
||||
}
|
||||
];
|
||||
}, [supplierStats, orderStats, deliveryStats]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Panel de Análisis de Proveedores</h1>
|
||||
<p className="text-gray-600">Insights y métricas de rendimiento de tus proveedores</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Filter className="w-4 h-4 text-gray-500" />
|
||||
<select
|
||||
value={filters.period}
|
||||
onChange={(e) => setFilters(prev => ({ ...prev, period: e.target.value as any }))}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
{periodOptions.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={loadAnalyticsData}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Actualizar
|
||||
</Button>
|
||||
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exportar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{kpis.map((kpi, index) => (
|
||||
<Card key={index} className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-600 mb-1">{kpi.title}</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mb-2">{kpi.value}</p>
|
||||
<div className={`flex items-center space-x-1 text-sm ${
|
||||
kpi.changeType === 'positive' ? 'text-green-600' : 'text-red-600'
|
||||
}`}>
|
||||
{kpi.changeType === 'positive' ? (
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
) : (
|
||||
<TrendingDown className="w-4 h-4" />
|
||||
)}
|
||||
<span>{kpi.change}</span>
|
||||
<span className="text-gray-500">vs período anterior</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${
|
||||
kpi.color === 'blue' ? 'bg-blue-100' :
|
||||
kpi.color === 'green' ? 'bg-green-100' :
|
||||
kpi.color === 'orange' ? 'bg-orange-100' :
|
||||
'bg-purple-100'
|
||||
}`}>
|
||||
<kpi.icon className={`w-6 h-6 ${
|
||||
kpi.color === 'blue' ? 'text-blue-600' :
|
||||
kpi.color === 'green' ? 'text-green-600' :
|
||||
kpi.color === 'orange' ? 'text-orange-600' :
|
||||
'text-purple-600'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Performance Overview */}
|
||||
{performanceMetrics && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Supplier Performance */}
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<Building className="w-5 h-5 text-blue-500 mr-2" />
|
||||
Rendimiento de Proveedores
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700">Proveedores Activos</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-32 h-2 bg-gray-200 rounded-full">
|
||||
<div
|
||||
className="h-2 bg-green-500 rounded-full"
|
||||
style={{
|
||||
width: `${(supplierStats?.active_suppliers / supplierStats?.total_suppliers * 100) || 0}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{supplierStats?.active_suppliers}/{supplierStats?.total_suppliers}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700">Calidad Promedio</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-4 h-4 ${
|
||||
i < (supplierStats?.avg_quality_rating || 0)
|
||||
? 'text-yellow-400 fill-current'
|
||||
: 'text-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{supplierStats?.avg_quality_rating.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-700">Entregas Puntuales</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-4 h-4 ${
|
||||
i < (supplierStats?.avg_delivery_rating || 0)
|
||||
? 'text-blue-400 fill-current'
|
||||
: 'text-gray-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{supplierStats?.avg_delivery_rating.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Delivery Performance */}
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<Truck className="w-5 h-5 text-green-500 mr-2" />
|
||||
Rendimiento de Entregas
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<div className="relative w-32 h-32 mx-auto mb-4">
|
||||
<svg className="w-32 h-32 transform -rotate-90" viewBox="0 0 100 100">
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
fill="none"
|
||||
className="text-gray-200"
|
||||
/>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="40"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
fill="none"
|
||||
strokeDasharray={`${(deliveryStats?.on_time_percentage || 0) * 2.51} 251`}
|
||||
className="text-green-500"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-900">
|
||||
{formatPercentage(deliveryStats?.on_time_percentage || 0)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">A tiempo</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-xl font-bold text-green-600">
|
||||
{deliveryStats?.on_time_deliveries || 0}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">A Tiempo</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-red-600">
|
||||
{deliveryStats?.late_deliveries || 0}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">Tardías</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top Suppliers and Insights */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Top Suppliers */}
|
||||
{topSuppliers.length > 0 && (
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<TrendingUp className="w-5 h-5 text-purple-500 mr-2" />
|
||||
Mejores Proveedores
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{topSuppliers.slice(0, 5).map((supplier, index) => (
|
||||
<div key={supplier.id} className="flex items-center justify-between py-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold ${
|
||||
index === 0 ? 'bg-yellow-100 text-yellow-800' :
|
||||
index === 1 ? 'bg-gray-100 text-gray-800' :
|
||||
index === 2 ? 'bg-orange-100 text-orange-800' :
|
||||
'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">{supplier.name}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{supplier.total_orders} pedidos
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-semibold text-gray-900">
|
||||
{formatCurrency(supplier.total_amount)}
|
||||
</p>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Star className="w-3 h-3 text-yellow-400 fill-current" />
|
||||
<span className="text-xs text-gray-600">
|
||||
{supplier.quality_rating?.toFixed(1) || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Insights and Recommendations */}
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<BarChart3 className="w-5 h-5 text-indigo-500 mr-2" />
|
||||
Insights y Recomendaciones
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Performance insights */}
|
||||
{deliveryStats && deliveryStats.on_time_percentage > 90 && (
|
||||
<div className="flex items-start space-x-3 p-3 bg-green-50 rounded-lg">
|
||||
<CheckCircle className="w-5 h-5 text-green-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-green-900">
|
||||
Excelente rendimiento de entregas
|
||||
</p>
|
||||
<p className="text-xs text-green-800">
|
||||
{formatPercentage(deliveryStats.on_time_percentage)} de entregas a tiempo.
|
||||
¡Mantén la buena comunicación con tus proveedores!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{deliveryStats && deliveryStats.on_time_percentage < 80 && (
|
||||
<div className="flex items-start space-x-3 p-3 bg-yellow-50 rounded-lg">
|
||||
<AlertCircle className="w-5 h-5 text-yellow-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-yellow-900">
|
||||
Oportunidad de mejora en entregas
|
||||
</p>
|
||||
<p className="text-xs text-yellow-800">
|
||||
Solo {formatPercentage(deliveryStats.on_time_percentage)} de entregas a tiempo.
|
||||
Considera revisar los acuerdos de servicio con tus proveedores.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{supplierStats && supplierStats.pending_suppliers > 0 && (
|
||||
<div className="flex items-start space-x-3 p-3 bg-blue-50 rounded-lg">
|
||||
<Clock className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-900">
|
||||
Proveedores pendientes de aprobación
|
||||
</p>
|
||||
<p className="text-xs text-blue-800">
|
||||
Tienes {supplierStats.pending_suppliers} proveedores esperando aprobación.
|
||||
Revísalos para acelerar tu cadena de suministro.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{orderStats && orderStats.overdue_count > 0 && (
|
||||
<div className="flex items-start space-x-3 p-3 bg-red-50 rounded-lg">
|
||||
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-red-900">
|
||||
Pedidos vencidos
|
||||
</p>
|
||||
<p className="text-xs text-red-800">
|
||||
{orderStats.overdue_count} pedidos han superado su fecha de entrega.
|
||||
Contacta con tus proveedores para actualizar el estado.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{supplierStats && supplierStats.avg_quality_rating > 4 && (
|
||||
<div className="flex items-start space-x-3 p-3 bg-purple-50 rounded-lg">
|
||||
<Star className="w-5 h-5 text-purple-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-purple-900">
|
||||
Alta calidad de proveedores
|
||||
</p>
|
||||
<p className="text-xs text-purple-800">
|
||||
Calidad promedio de {supplierStats.avg_quality_rating.toFixed(1)}/5.
|
||||
Considera destacar estos proveedores como preferidos.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Detailed Metrics */}
|
||||
{orderStats && (
|
||||
<Card>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center">
|
||||
<Package className="w-5 h-5 text-blue-500 mr-2" />
|
||||
Métricas Detalladas de Pedidos
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-blue-600">
|
||||
{orderStats.total_orders}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">Total Pedidos</p>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
{formatCurrency(orderStats.avg_order_value)} valor promedio
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-green-600">
|
||||
{orderStats.this_month_orders}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">Este Mes</p>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
{formatCurrency(orderStats.this_month_spend)} gastado
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-purple-600">
|
||||
{orderStats.pending_approval}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">Pendientes Aprobación</p>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
Requieren revisión
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Status Breakdown */}
|
||||
{orderStats.status_counts && (
|
||||
<div className="mt-8">
|
||||
<h4 className="font-medium text-gray-900 mb-4">Distribución por Estado</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{Object.entries(orderStats.status_counts).map(([status, count]) => (
|
||||
<div key={status} className="text-center p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-lg font-semibold text-gray-900">
|
||||
{count as number}
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 capitalize">
|
||||
{status.toLowerCase().replace('_', ' ')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupplierAnalyticsDashboard;
|
||||
Reference in New Issue
Block a user