"use client"; import { useState, useEffect } from "react"; import { LayoutGrid, TrendingUp, CheckCircle2, DollarSign, Clock, Calendar, Users, Target, PiggyBank, AlertCircle, BarChart3, Lock, } from "lucide-react"; import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, Legend, RadialBarChart, RadialBar, CartesianGrid, } from "recharts"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Skeleton } from "@/components/ui/skeleton"; import { Badge } from "@/components/ui/badge"; import { getProjects } from "@/lib/projectsService"; import { Project } from "@/types/project"; // Colores consistentes const COLORS = { primary: "#3b82f6", secondary: "#8b5cf6", success: "#22c55e", warning: "#f59e0b", danger: "#ef4444", gray: "#6b7280", orange: "#f97316", cyan: "#06b6d4", }; const PROGRESS_COLORS = ["#ef4444", "#f97316", "#f59e0b", "#22c55e"]; // Paleta de colores para proyectos individuales (variada y distinguible) const PROJECT_COLORS = [ "#3b82f6", // blue "#8b5cf6", // violet "#06b6d4", // cyan "#10b981", // emerald "#f59e0b", // amber "#ef4444", // red "#ec4899", // pink "#6366f1", // indigo "#14b8a6", // teal "#f97316", // orange "#84cc16", // lime "#a855f7", // purple ]; // Obtener color por índice (cíclico) function getProjectColor(index: number): string { return PROJECT_COLORS[index % PROJECT_COLORS.length]; } // ==================== COMPONENTES DE CARDS ==================== interface StatCardProps { title: string; value: string | number; subtitle?: string; icon: React.ReactNode; highlight?: boolean; } function StatCard({ title, value, subtitle, icon, highlight }: StatCardProps) { return ( {title} {icon}
{value}
{subtitle &&

{subtitle}

}
); } // ==================== COMPONENTES DE GRAFICOS ==================== // Pie Chart de distribucion por estado interface StatusPieChartProps { borradores: number; abiertos: number; cerrados: number; } function StatusPieChart({ borradores, abiertos, cerrados }: StatusPieChartProps) { const data = [ { name: "Borrador", value: borradores, color: COLORS.gray }, { name: "Abierto", value: abiertos, color: COLORS.primary }, { name: "Cerrado", value: cerrados, color: COLORS.success }, ].filter(d => d.value > 0); const total = borradores + abiertos + cerrados; return ( Distribucion por Estado
{data.map((entry, index) => ( ))} [`${value ?? 0} proyectos`, '']} contentStyle={{ fontSize: '12px' }} />

{total}

Total

{data.map((entry) => (
{entry.name}: {entry.value}
))}
); } // Donut Chart para progreso interface ProgressDonutProps { data: { name: string; value: number; color: string }[]; title: string; centerValue?: string | number; centerLabel?: string; } function ProgressDonut({ data, title, centerValue, centerLabel }: ProgressDonutProps) { return ( {title}
{data.map((entry, index) => ( ))} [`${value ?? 0} proyectos`, '']} contentStyle={{ fontSize: '12px' }} /> {centerValue !== undefined && (

{centerValue}

{centerLabel &&

{centerLabel}

}
)}
{data.map((entry) => (
{entry.name}
))}
); } // Radial Bar Chart para metricas interface RadialMetricProps { value: number; title: string; subtitle?: string; color?: string; } function RadialMetric({ value, title, subtitle, color = COLORS.primary }: RadialMetricProps) { const data = [{ name: title, value: Math.min(value, 100), fill: color }]; return ( {title}

{value}%

{subtitle &&

{subtitle}

}
); } // Bar Chart horizontal para presupuestos por proyecto interface BudgetBarChartProps { projects: Project[]; title: string; maxItems?: number; } function BudgetBarChart({ projects, title, maxItems = 6 }: BudgetBarChartProps) { const data = projects .slice(0, maxItems) .map((p, index) => ({ name: p.name.length > 15 ? p.name.substring(0, 15) + "..." : p.name, fullName: p.name, presupuesto: p.budget, gastado: p.spent, color: getProjectColor(index), })); return ( {title}
`${(v / 1000).toFixed(0)}k`} fontSize={11} /> { const label = name === 'presupuesto' ? 'Presupuesto' : name === 'gastado' ? 'Gastado' : String(name); return [`€${Number(value ?? 0).toLocaleString("es-ES")}`, label]; }} labelFormatter={(label, payload) => payload?.[0]?.payload?.fullName || label} contentStyle={{ fontSize: '12px' }} /> {data.map((entry, index) => ( ))} {data.map((entry, index) => ( ))}
); } // Bar Chart vertical para comparativas interface ComparisonBarChartProps { data: { name: string; value: number; color?: string }[]; title: string; formatter?: (value: number) => string; } function ComparisonBarChart({ data, title, formatter }: ComparisonBarChartProps) { return ( {title}
[formatter ? formatter(Number(value) || 0) : (value ?? 0), '']} contentStyle={{ fontSize: '12px' }} /> {data.map((entry, index) => ( ))}
); } // Multi Radial para comparar metricas interface MultiRadialProps { data: { name: string; value: number; fill: string }[]; title: string; valueLabel?: string; } function MultiRadialChart({ data, title, valueLabel = "%" }: MultiRadialProps) { // Asignar colores únicos a cada proyecto const coloredData = data.map((item, index) => ({ ...item, fill: getProjectColor(index), })); return ( {title}
[`${value ?? 0}${valueLabel}`, '']} contentStyle={{ fontSize: '12px' }} />
); } // ==================== FUNCIONES HELPER ==================== function calculateStats(projects: Project[]) { const total = projects.length; if (total === 0) { return { total: 0, avgProgress: 0, totalBudget: 0, totalSpent: 0, avgBudget: 0, budgetConsumed: 0, uniqueClients: 0, avgDuration: 0, }; } const totalBudget = projects.reduce((acc, p) => acc + p.budget, 0); const totalSpent = projects.reduce((acc, p) => acc + p.spent, 0); const avgProgress = Math.round(projects.reduce((acc, p) => acc + p.progress, 0) / total); const avgBudget = Math.round(totalBudget / total); const budgetConsumed = totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0; const uniqueClients = new Set(projects.map((p) => p.client)).size; const durations = projects .map((p) => { const start = new Date(p.startDate).getTime(); const end = new Date(p.endDate).getTime(); if (isNaN(start) || isNaN(end)) return null; return Math.ceil((end - start) / (1000 * 60 * 60 * 24)); }) .filter((d): d is number => d !== null && d > 0); const avgDuration = durations.length > 0 ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0; return { total, avgProgress, totalBudget, totalSpent, avgBudget, budgetConsumed, uniqueClients, avgDuration }; } function formatCurrency(value: number): string { if (value >= 1000000) return `€${(value / 1000000).toFixed(1)}M`; if (value >= 1000) return `€${(value / 1000).toFixed(0)}k`; return `€${value.toLocaleString("es-ES")}`; } // Calcular días restantes hasta fecha fin function getDaysRemaining(endDate: string): number { const end = new Date(endDate).getTime(); const now = new Date().getTime(); return Math.ceil((end - now) / (1000 * 60 * 60 * 24)); } // Proyectos próximos a vencer (menos de 30 días) function getUpcomingDeadlines(projects: Project[], days: number = 30): Project[] { return projects .filter(p => p.status === "1" && p.progress < 100) .filter(p => { const remaining = getDaysRemaining(p.endDate); return remaining > 0 && remaining <= days; }) .sort((a, b) => getDaysRemaining(a.endDate) - getDaysRemaining(b.endDate)); } // Proyectos retrasados (fecha fin pasada pero no completados) function getOverdueProjects(projects: Project[]): Project[] { return projects .filter(p => p.status === "1" && p.progress < 100) .filter(p => getDaysRemaining(p.endDate) < 0) .sort((a, b) => getDaysRemaining(a.endDate) - getDaysRemaining(b.endDate)); } // ==================== PANEL: TODOS LOS PROYECTOS ==================== function AllProjectsStats({ projects }: { projects: Project[] }) { const stats = calculateStats(projects); const borradores = projects.filter(p => p.status === "0").length; const abiertos = projects.filter(p => p.status === "1").length; const cerrados = projects.filter(p => p.status === "2").length; // Nuevas métricas const overdueProjects = getOverdueProjects(projects); const upcomingDeadlines = getUpcomingDeadlines(projects, 14); // próximos 14 días const progressData = [ { name: "0-25%", value: projects.filter(p => p.progress < 25).length, color: PROGRESS_COLORS[0] }, { name: "25-50%", value: projects.filter(p => p.progress >= 25 && p.progress < 50).length, color: PROGRESS_COLORS[1] }, { name: "50-75%", value: projects.filter(p => p.progress >= 50 && p.progress < 75).length, color: PROGRESS_COLORS[2] }, { name: "75-100%", value: projects.filter(p => p.progress >= 75).length, color: PROGRESS_COLORS[3] }, ]; const budgetByStatus = [ { name: "Borrador", value: projects.filter(p => p.status === "0").reduce((a, p) => a + p.budget, 0), color: COLORS.gray }, { name: "Abierto", value: projects.filter(p => p.status === "1").reduce((a, p) => a + p.budget, 0), color: COLORS.primary }, { name: "Cerrado", value: projects.filter(p => p.status === "2").reduce((a, p) => a + p.budget, 0), color: COLORS.success }, ]; // Top proyectos por presupuesto con colores únicos const topProjects = [...projects] .sort((a, b) => b.budget - a.budget) .slice(0, 5) .map((p, index) => ({ name: p.name.length > 12 ? p.name.substring(0, 12) + "..." : p.name, value: p.progress, fill: getProjectColor(index), })); return (
{/* KPIs principales */}
} highlight /> } /> } /> } />
{/* Alertas de fechas */} {(overdueProjects.length > 0 || upcomingDeadlines.length > 0) && (
{overdueProjects.length > 0 && (

Proyectos Retrasados

{overdueProjects.length}

Fecha límite superada

{overdueProjects.slice(0, 2).map(p => (

{p.name}

))}
)} {upcomingDeadlines.length > 0 && (

Próximos a Vencer

{upcomingDeadlines.length}

En los próximos 14 días

{upcomingDeadlines.slice(0, 2).map(p => (

{p.name} ({getDaysRemaining(p.endDate)}d)

))}
)}
)} {/* Gráficos principales */}
80 ? COLORS.danger : COLORS.primary} />
{/* Comparativas */}
formatCurrency(v)} />
{/* Timeline de fechas - Próximos vencimientos y retrasados */} {(upcomingDeadlines.length > 0 || overdueProjects.length > 0) && ( Timeline de Proyectos
{/* Proyectos retrasados */} {overdueProjects.length > 0 && (

Retrasados ({overdueProjects.length})

{overdueProjects.slice(0, 6).map((p, index) => { const daysOverdue = Math.abs(getDaysRemaining(p.endDate)); return (

{p.name}

{p.progress}% completado

-{daysOverdue}d
); })}
)} {/* Próximos vencimientos */} {upcomingDeadlines.length > 0 && (

Próximos 14 días ({upcomingDeadlines.length})

{upcomingDeadlines.slice(0, 6).map((p, index) => { const days = getDaysRemaining(p.endDate); return (

{p.name}

{p.progress}% completado

{days}d
); })}
)} {/* Resumen de fechas */}
Retrasados: {overdueProjects.length}
Vencen en 7 días: {upcomingDeadlines.filter(p => getDaysRemaining(p.endDate) <= 7).length}
En tiempo: {projects.filter(p => p.status === "1" && getDaysRemaining(p.endDate) > 14).length}
)} {/* Presupuesto detallado */} b.budget - a.budget)} title="Presupuesto vs Gastado por Proyecto" maxItems={6} />
); } // ==================== PANEL: PROYECTOS ACTIVOS ==================== function ActiveProjectsStats({ projects }: { projects: Project[] }) { const activeProjects = projects.filter(p => p.status === "1" && p.progress < 100); const stats = calculateStats(activeProjects); if (activeProjects.length === 0) { return (

No hay proyectos activos

Todos los proyectos están cerrados o en borrador

); } const overdueProjects = getOverdueProjects(projects); const upcomingDeadlines = getUpcomingDeadlines(projects, 14); const needsAttention = activeProjects.filter(p => p.progress < 25).length; const nearCompletion = activeProjects.filter(p => p.progress >= 75).length; const progressData = [ { name: "Inicio (0-25%)", value: activeProjects.filter(p => p.progress < 25).length, color: PROGRESS_COLORS[0] }, { name: "En curso (25-50%)", value: activeProjects.filter(p => p.progress >= 25 && p.progress < 50).length, color: PROGRESS_COLORS[1] }, { name: "Avanzado (50-75%)", value: activeProjects.filter(p => p.progress >= 50 && p.progress < 75).length, color: PROGRESS_COLORS[2] }, { name: "Casi listo (75-99%)", value: activeProjects.filter(p => p.progress >= 75).length, color: PROGRESS_COLORS[3] }, ]; // Top proyectos con colores únicos const topByProgress = activeProjects .sort((a, b) => b.progress - a.progress) .slice(0, 5) .map((p, index) => ({ name: p.name.length > 12 ? p.name.substring(0, 12) + "..." : p.name, value: p.progress, fill: getProjectColor(index), })); return (
{/* KPIs */}
} highlight /> } /> } /> } />
{/* Alertas */}
0 ? 'bg-red-50' : 'bg-gray-50 border-gray-200'}`}>

0 ? 'text-red-700' : 'text-gray-500'}`}> Retrasados

0 ? 'text-red-600' : 'text-gray-400'}`}>{overdueProjects.length}

0 ? 'text-red-600' : 'text-gray-400'}`}>Fecha superada

0 ? 'bg-orange-50' : 'bg-gray-50 border-gray-200'}`}>

0 ? 'text-orange-700' : 'text-gray-500'}`}> Requieren Atención

0 ? 'text-orange-600' : 'text-gray-400'}`}>{needsAttention}

0 ? 'text-orange-600' : 'text-gray-400'}`}>Menos del 25%

Próximos a Completar

{nearCompletion}

75% o más

{/* Gráficos */}
{/* Próximos vencimientos */} {upcomingDeadlines.length > 0 && ( Próximos Vencimientos (14 días)
{upcomingDeadlines.slice(0, 6).map((p, index) => { const days = getDaysRemaining(p.endDate); return (

{p.name}

{p.progress}% completado

{days}d
); })}
)} {/* Presupuesto */} b.budget - a.budget)} title="Presupuesto vs Gastado por Proyecto" maxItems={6} />
); } // ==================== PANEL: PROYECTOS CERRADOS ==================== function ClosedProjectsStats({ projects }: { projects: Project[] }) { const closedProjects = projects.filter(p => p.status === "2"); const stats = calculateStats(closedProjects); if (closedProjects.length === 0) { return (

No hay proyectos cerrados

Aún no se ha cerrado ningún proyecto

); } const withinBudget = closedProjects.filter(p => p.spent <= p.budget).length; const overBudget = closedProjects.filter(p => p.spent > p.budget).length; const fullyCompleted = closedProjects.filter(p => p.progress >= 100).length; const savings = Math.max(0, stats.totalBudget - stats.totalSpent); const budgetComplianceData = [ { name: "Dentro presupuesto", value: withinBudget, color: COLORS.success }, { name: "Sobre presupuesto", value: overBudget, color: COLORS.danger }, ].filter(d => d.value > 0); const completionData = [ { name: "100% completado", value: fullyCompleted, color: COLORS.success }, { name: "Cerrado parcial", value: closedProjects.length - fullyCompleted, color: COLORS.warning }, ].filter(d => d.value > 0); // Eficiencia por proyecto con colores únicos const efficiencyData = closedProjects .slice(0, 5) .map((p, index) => { const efficiency = p.budget > 0 ? Math.round(((p.budget - p.spent) / p.budget) * 100) : 0; return { name: p.name.length > 12 ? p.name.substring(0, 12) + "..." : p.name, value: Math.max(0, Math.min(efficiency, 100)), fill: getProjectColor(index), }; }); return (
{/* KPIs */}
} highlight /> } /> 0 ? Math.round((withinBudget / stats.total) * 100) : 0}%`} subtitle={`${withinBudget} dentro de presupuesto`} icon={} /> } />
{/* Cumplimiento visual */}

Dentro del Presupuesto

{withinBudget}

{stats.total > 0 ? Math.round((withinBudget / stats.total) * 100) : 0}% de los cerrados

0 ? 'bg-gradient-to-br from-red-50 to-orange-50' : 'bg-gray-50 border-gray-200'}`}>
0 ? 'bg-red-100' : 'bg-gray-100'}`}> 0 ? 'text-red-600' : 'text-gray-400'}`} />

0 ? 'text-red-700' : 'text-gray-500'}`}>Excedieron Presupuesto

0 ? 'text-red-600' : 'text-gray-400'}`}>{overBudget}

0 ? 'text-red-600' : 'text-gray-400'}`}>{stats.total > 0 ? Math.round((overBudget / stats.total) * 100) : 0}% de los cerrados

{/* Gráficos */}
0 ? Math.round((withinBudget / stats.total) * 100) : 0}%`} centerLabel="Éxito" />
{/* Eficiencia por proyecto */}
b.budget - a.budget)} title="Presupuesto vs Gastado" maxItems={5} />
); } // ==================== PANEL: PROYECTOS COMPLETADOS ==================== function CompletedProjectsStats({ projects }: { projects: Project[] }) { const completedProjects = projects.filter(p => p.progress >= 100); const stats = calculateStats(completedProjects); if (completedProjects.length === 0) { return (

No hay proyectos completados

Aún no se ha completado ningún proyecto al 100%

); } const withinBudget = completedProjects.filter(p => p.spent <= p.budget).length; const savings = Math.max(0, stats.totalBudget - stats.totalSpent); const successRate = stats.total > 0 ? Math.round((withinBudget / stats.total) * 100) : 0; const statusData = [ { name: "Abierto (pendiente cierre)", value: completedProjects.filter(p => p.status === "1").length, color: COLORS.primary }, { name: "Cerrado", value: completedProjects.filter(p => p.status === "2").length, color: COLORS.success }, ].filter(d => d.value > 0); // Proyectos completados con colores únicos para el gráfico const budgetData = completedProjects .sort((a, b) => b.budget - a.budget) .slice(0, 6) .map((p, index) => ({ name: p.name.length > 15 ? p.name.substring(0, 15) + "..." : p.name, value: p.budget, color: getProjectColor(index), })); return (
{/* Banner de éxito */}

{stats.total}

Proyectos al 100%

{successRate}%

Tasa de éxito

{formatCurrency(savings)}

Ahorro total

{/* KPIs */}
} /> } /> } /> } />
{/* Gráficos */}
formatCurrency(v)} />
{/* Detalle de presupuesto */} b.budget - a.budget)} title="Detalle: Presupuesto vs Gastado" maxItems={6} />
); } // ==================== SKELETON ==================== function StatisticsSkeleton() { return (
{Array.from({ length: 4 }).map((_, i) => ( ))}
{Array.from({ length: 3 }).map((_, i) => ( ))}
); } // ==================== COMPONENTE PRINCIPAL ==================== export default function StatisticsPage() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function loadProjects() { try { setLoading(true); const data = await getProjects(); setProjects(data); setError(null); } catch (err) { console.error("Error loading projects:", err); setError("Error al cargar los proyectos"); } finally { setLoading(false); } } loadProjects(); }, []); if (error) { return (

{error}

); } const activeCount = projects.filter(p => p.status === "1" && p.progress < 100).length; const closedCount = projects.filter(p => p.status === "2").length; const completedCount = projects.filter(p => p.progress >= 100).length; return (

Estadisticas

Analisis de {projects.length} proyectos en Dolibarr

{loading ? ( ) : ( Todos {projects.length} Activos {activeCount} Cerrados {closedCount} Completados {completedCount} )}
); }