"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 // 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) => ( ))}
); } // 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; const overdueProjects = getOverdueProjects(projects); const upcomingDeadlines = getUpcomingDeadlines(projects, 14); 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] }, ]; return (
{/* KPIs principales */}
} highlight /> } /> } /> 0 ? "Proyectos retrasados" : "Todo en orden"} icon={ 0 ? "text-red-500" : "text-green-500"}`} />} />
{/* Graficos: Estado + Progreso */}
{/* Presupuesto por proyecto */} b.budget - a.budget)} title="Presupuesto vs Gastado por Proyecto" maxItems={8} /> {/* Alertas: retrasados y proximos a vencer */} {(overdueProjects.length > 0 || upcomingDeadlines.length > 0) && (
{overdueProjects.length > 0 && ( Retrasados ({overdueProjects.length})
{overdueProjects.slice(0, 4).map(p => { const daysOverdue = Math.abs(getDaysRemaining(p.endDate)); return (

{p.name}

{p.progress}%

-{daysOverdue}d
); })}
)} {upcomingDeadlines.length > 0 && ( Proximos a vencer ({upcomingDeadlines.length})
{upcomingDeadlines.slice(0, 4).map(p => { const days = getDaysRemaining(p.endDate); return (

{p.name}

{p.progress}%

{days}d
); })}
)}
)}
); } // ==================== 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] }, ]; return (
{/* KPIs */}
} highlight /> } /> } /> } />
{/* Gráficos */}
b.budget - a.budget)} title="Presupuesto vs Gastado" maxItems={6} />
{/* 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
); })}
)}
); } // ==================== 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); // Distribución de duración de los proyectos cerrados const durationRanges = closedProjects.reduce( (acc, p) => { const start = new Date(p.startDate); const end = new Date(p.endDate); const months = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24 * 30.44); if (months < 3) acc.short++; else if (months < 6) acc.medium++; else if (months < 12) acc.long++; else acc.veryLong++; return acc; }, { short: 0, medium: 0, long: 0, veryLong: 0 } ); const durationData = [ { name: "< 3 meses", value: durationRanges.short, color: COLORS.success }, { name: "3-6 meses", value: durationRanges.medium, color: COLORS.primary }, { name: "6-12 meses", value: durationRanges.long, color: COLORS.warning }, { name: "> 12 meses", value: durationRanges.veryLong, color: COLORS.danger }, ].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 ? 'border-red-200 dark:border-red-900 bg-gradient-to-br from-red-50 to-orange-50 dark:from-red-950/30 dark:to-orange-950/30' : 'border bg-muted/30'}`}>
0 ? 'bg-red-100 dark:bg-red-900/50' : 'bg-muted'}`}> 0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50'}`} />

0 ? 'text-red-700 dark:text-red-400' : 'text-muted-foreground'}`}>Excedieron Presupuesto

0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50'}`}>{overBudget}

0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50'}`}>{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); return (
{/* KPIs */}
} highlight /> } /> } /> } />
{/* Gráficos */}
b.budget - a.budget)} title="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} )}
); }