2026-01-30 19:56:34 +00:00
"use client" ;
import { useState , useEffect } from "react" ;
import {
LayoutGrid ,
TrendingUp ,
CheckCircle2 ,
DollarSign ,
Clock ,
Calendar ,
Users ,
Target ,
PiggyBank ,
AlertCircle ,
BarChart3 ,
2026-01-30 21:44:22 +00:00
Lock ,
2026-01-30 19:56:34 +00:00
} from "lucide-react" ;
2026-01-30 21:44:22 +00:00
import {
PieChart ,
Pie ,
Cell ,
ResponsiveContainer ,
BarChart ,
Bar ,
XAxis ,
YAxis ,
Tooltip ,
Legend ,
RadialBarChart ,
RadialBar ,
CartesianGrid ,
} from "recharts" ;
2026-01-30 19:56:34 +00:00
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" ;
2026-01-30 21:44:22 +00:00
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" ] ;
2026-01-30 19:56:34 +00:00
2026-01-30 21:44:22 +00:00
// 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 ====================
2026-01-30 19:56:34 +00:00
interface StatCardProps {
title : string ;
value : string | number ;
subtitle? : string ;
icon : React.ReactNode ;
highlight? : boolean ;
}
2026-01-30 21:44:22 +00:00
function StatCard ( { title , value , subtitle , icon , highlight } : StatCardProps ) {
2026-01-30 19:56:34 +00:00
return (
2026-01-31 12:56:51 +00:00
< Card className = { ` border hover:shadow-md transition-shadow ${ highlight ? "ring-2 ring-blue-500/20" : "" } ` } >
2026-01-30 19:56:34 +00:00
< CardHeader className = "flex flex-row items-center justify-between pb-2" >
2026-01-31 12:56:51 +00:00
< CardTitle className = "text-sm font-medium text-muted-foreground" > { title } < / CardTitle >
2026-01-30 19:56:34 +00:00
{ icon }
< / CardHeader >
< CardContent >
2026-01-31 12:56:51 +00:00
< div className = "text-3xl font-bold text-foreground" > { value } < / div >
{ subtitle && < p className = "text-xs text-muted-foreground mt-1" > { subtitle } < / p > }
2026-01-30 19:56:34 +00:00
< / CardContent >
< / Card >
) ;
}
2026-01-30 21:44:22 +00:00
// ==================== COMPONENTES DE GRAFICOS ====================
// Pie Chart de distribucion por estado
interface StatusPieChartProps {
borradores : number ;
abiertos : number ;
cerrados : number ;
2026-01-30 19:56:34 +00:00
}
2026-01-30 21:44:22 +00:00
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 ) ;
2026-01-30 19:56:34 +00:00
2026-01-30 21:44:22 +00:00
const total = borradores + abiertos + cerrados ;
2026-01-30 19:56:34 +00:00
return (
2026-01-31 12:56:51 +00:00
< Card className = "border" >
2026-01-30 21:44:22 +00:00
< CardHeader className = "pb-2" >
2026-01-31 12:56:51 +00:00
< CardTitle className = "text-sm font-medium text-muted-foreground" > Distribucion por Estado < / CardTitle >
2026-01-30 19:56:34 +00:00
< / CardHeader >
2026-01-30 21:44:22 +00:00
< CardContent >
< div className = "h-64 relative" >
< ResponsiveContainer width = "100%" height = "100%" >
< PieChart >
< Pie
data = { data }
cx = "50%"
cy = "50%"
innerRadius = { 50 }
outerRadius = { 80 }
paddingAngle = { 3 }
dataKey = "value"
>
{ data . map ( ( entry , index ) = > (
< Cell key = { ` cell- ${ index } ` } fill = { entry . color } / >
) ) }
< / Pie >
< Tooltip
formatter = { ( value ) = > [ ` ${ value ? ? 0 } proyectos ` , '' ] }
contentStyle = { { fontSize : '12px' } }
/ >
< / PieChart >
< / ResponsiveContainer >
< div className = "absolute inset-0 flex items-center justify-center pointer-events-none" >
< div className = "text-center" >
2026-01-31 12:56:51 +00:00
< p className = "text-2xl font-bold text-foreground" > { total } < / p >
< p className = "text-xs text-muted-foreground" > Total < / p >
2026-01-30 21:44:22 +00:00
< / div >
< / div >
< / div >
< div className = "flex justify-center gap-4 mt-2" >
{ data . map ( ( entry ) = > (
< div key = { entry . name } className = "flex items-center gap-1.5" >
< div className = "w-3 h-3 rounded-full" style = { { backgroundColor : entry.color } } / >
2026-01-31 12:56:51 +00:00
< span className = "text-xs text-muted-foreground" > { entry . name } : { entry . value } < / span >
2026-01-30 21:44:22 +00:00
< / div >
) ) }
< / div >
< / CardContent >
< / Card >
) ;
}
// 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 (
2026-01-31 12:56:51 +00:00
< Card className = "border" >
2026-01-30 21:44:22 +00:00
< CardHeader className = "pb-2" >
2026-01-31 12:56:51 +00:00
< CardTitle className = "text-sm font-medium text-muted-foreground" > { title } < / CardTitle >
2026-01-30 21:44:22 +00:00
< / CardHeader >
< CardContent >
< div className = "h-64 relative" >
< ResponsiveContainer width = "100%" height = "100%" >
< PieChart >
< Pie
data = { data }
cx = "50%"
cy = "50%"
innerRadius = { 55 }
outerRadius = { 85 }
paddingAngle = { 2 }
dataKey = "value"
>
{ data . map ( ( entry , index ) = > (
< Cell key = { ` cell- ${ index } ` } fill = { entry . color } / >
) ) }
< / Pie >
< Tooltip
formatter = { ( value ) = > [ ` ${ value ? ? 0 } proyectos ` , '' ] }
contentStyle = { { fontSize : '12px' } }
/ >
< / PieChart >
< / ResponsiveContainer >
{ centerValue !== undefined && (
< div className = "absolute inset-0 flex items-center justify-center pointer-events-none" >
< div className = "text-center" >
2026-01-31 12:56:51 +00:00
< p className = "text-2xl font-bold text-foreground" > { centerValue } < / p >
{ centerLabel && < p className = "text-xs text-muted-foreground" > { centerLabel } < / p > }
2026-01-30 19:56:34 +00:00
< / div >
< / div >
2026-01-30 21:44:22 +00:00
) }
< / div >
< div className = "flex flex-wrap justify-center gap-3 mt-2" >
{ data . map ( ( entry ) = > (
< div key = { entry . name } className = "flex items-center gap-1.5" >
< div className = "w-3 h-3 rounded-full" style = { { backgroundColor : entry.color } } / >
2026-01-31 12:56:51 +00:00
< span className = "text-xs text-muted-foreground" > { entry . name } < / span >
2026-01-30 21:44:22 +00:00
< / div >
) ) }
< / div >
2026-01-30 19:56:34 +00:00
< / CardContent >
< / Card >
) ;
}
2026-01-30 21:44:22 +00:00
// Radial Bar Chart para metricas
interface RadialMetricProps {
value : number ;
title : string ;
subtitle? : string ;
color? : string ;
2026-01-30 19:56:34 +00:00
}
2026-01-30 21:44:22 +00:00
function RadialMetric ( { value , title , subtitle , color = COLORS . primary } : RadialMetricProps ) {
const data = [ { name : title , value : Math.min ( value , 100 ) , fill : color } ] ;
2026-01-30 19:56:34 +00:00
return (
2026-01-31 12:56:51 +00:00
< Card className = "border" >
2026-01-30 21:44:22 +00:00
< CardHeader className = "pb-0" >
2026-01-31 12:56:51 +00:00
< CardTitle className = "text-sm font-medium text-muted-foreground" > { title } < / CardTitle >
2026-01-30 19:56:34 +00:00
< / CardHeader >
2026-01-30 21:44:22 +00:00
< CardContent >
< div className = "h-40 relative" >
< ResponsiveContainer width = "100%" height = "100%" >
< RadialBarChart
cx = "50%"
cy = "50%"
innerRadius = "60%"
outerRadius = "90%"
data = { data }
startAngle = { 180 }
endAngle = { 0 }
>
< RadialBar
2026-01-31 12:56:51 +00:00
background = { { fill : "hsl(var(--muted))" } }
2026-01-30 21:44:22 +00:00
dataKey = "value"
cornerRadius = { 10 }
/ >
< / RadialBarChart >
< / ResponsiveContainer >
< div className = "absolute inset-0 flex items-center justify-center pointer-events-none" style = { { marginTop : "-15px" } } >
< div className = "text-center" >
2026-01-31 12:56:51 +00:00
< p className = "text-2xl font-bold text-foreground" > { value } % < / p >
{ subtitle && < p className = "text-xs text-muted-foreground" > { subtitle } < / p > }
2026-01-30 21:44:22 +00:00
< / div >
2026-01-30 19:56:34 +00:00
< / div >
< / div >
2026-01-30 21:44:22 +00:00
< / CardContent >
< / Card >
) ;
}
// 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 (
2026-01-31 12:56:51 +00:00
< Card className = "border" >
2026-01-30 21:44:22 +00:00
< CardHeader className = "pb-2" >
2026-01-31 12:56:51 +00:00
< CardTitle className = "text-sm font-medium text-muted-foreground" > { title } < / CardTitle >
2026-01-30 21:44:22 +00:00
< / CardHeader >
< CardContent >
< div className = "h-72" >
< ResponsiveContainer width = "100%" height = "100%" >
< BarChart data = { data } layout = "vertical" margin = { { left : 10 , right : 20 } } >
< CartesianGrid strokeDasharray = "3 3" horizontal = { false } / >
< XAxis type = "number" tickFormatter = { ( v ) = > ` ${ ( v / 1000 ) . toFixed ( 0 ) } k ` } fontSize = { 11 } / >
< YAxis type = "category" dataKey = "name" width = { 100 } fontSize = { 10 } / >
< Tooltip
2026-01-30 22:41:25 +00:00
formatter = { ( value , name ) = > {
const label = name === 'presupuesto' ? 'Presupuesto' : name === 'gastado' ? 'Gastado' : String ( name ) ;
return [ ` € ${ Number ( value ? ? 0 ) . toLocaleString ( "es-ES" ) } ` , label ] ;
} }
2026-01-30 21:44:22 +00:00
labelFormatter = { ( label , payload ) = > payload ? . [ 0 ] ? . payload ? . fullName || label }
contentStyle = { { fontSize : '12px' } }
/ >
< Legend wrapperStyle = { { fontSize : '11px' } } / >
< Bar dataKey = "presupuesto" name = "Presupuesto" radius = { [ 0 , 4 , 4 , 0 ] } >
{ data . map ( ( entry , index ) = > (
< Cell key = { ` cell-budget- ${ index } ` } fill = { entry . color } / >
) ) }
< / Bar >
< Bar dataKey = "gastado" name = "Gastado" radius = { [ 0 , 4 , 4 , 0 ] } >
{ data . map ( ( entry , index ) = > (
< Cell key = { ` cell-spent- ${ index } ` } fill = { entry . color } fillOpacity = { 0.5 } / >
) ) }
< / Bar >
< / BarChart >
< / ResponsiveContainer >
< / div >
< / CardContent >
< / Card >
) ;
}
// 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 (
2026-01-31 12:56:51 +00:00
< Card className = "border" >
2026-01-30 21:44:22 +00:00
< CardHeader className = "pb-2" >
2026-01-31 12:56:51 +00:00
< CardTitle className = "text-sm font-medium text-muted-foreground" > { title } < / CardTitle >
2026-01-30 21:44:22 +00:00
< / CardHeader >
< CardContent >
< div className = "h-64" >
< ResponsiveContainer width = "100%" height = "100%" >
< BarChart data = { data } margin = { { top : 10 , right : 10 , left : - 10 , bottom : 0 } } >
< CartesianGrid strokeDasharray = "3 3" vertical = { false } / >
< XAxis dataKey = "name" fontSize = { 11 } / >
< YAxis tickFormatter = { formatter } fontSize = { 11 } / >
< Tooltip
formatter = { ( value ) = > [ formatter ? formatter ( Number ( value ) || 0 ) : ( value ? ? 0 ) , '' ] }
contentStyle = { { fontSize : '12px' } }
/ >
< Bar dataKey = "value" radius = { [ 4 , 4 , 0 , 0 ] } >
{ data . map ( ( entry , index ) = > (
< Cell key = { ` cell- ${ index } ` } fill = { entry . color || COLORS . primary } / >
) ) }
< / Bar >
< / BarChart >
< / ResponsiveContainer >
2026-01-30 19:56:34 +00:00
< / div >
< / CardContent >
< / Card >
) ;
}
2026-01-30 21:44:22 +00:00
// 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 (
2026-01-31 12:56:51 +00:00
< Card className = "border" >
2026-01-30 21:44:22 +00:00
< CardHeader className = "pb-2" >
2026-01-31 12:56:51 +00:00
< CardTitle className = "text-sm font-medium text-muted-foreground" > { title } < / CardTitle >
2026-01-30 21:44:22 +00:00
< / CardHeader >
< CardContent >
< div className = "h-64" >
< ResponsiveContainer width = "100%" height = "100%" >
< RadialBarChart
cx = "50%"
cy = "50%"
innerRadius = "20%"
outerRadius = "90%"
data = { coloredData }
startAngle = { 90 }
endAngle = { - 270 }
>
< RadialBar
2026-01-31 12:56:51 +00:00
background = { { fill : "hsl(var(--muted))" } }
2026-01-30 21:44:22 +00:00
dataKey = "value"
cornerRadius = { 5 }
/ >
< Tooltip
formatter = { ( value ) = > [ ` ${ value ? ? 0 } ${ valueLabel } ` , '' ] }
contentStyle = { { fontSize : '12px' } }
/ >
< Legend
iconSize = { 10 }
layout = "vertical"
verticalAlign = "middle"
align = "right"
wrapperStyle = { { fontSize : "10px" , right : 0 } }
/ >
< / RadialBarChart >
< / ResponsiveContainer >
< / div >
< / CardContent >
< / Card >
) ;
}
// ==================== FUNCIONES HELPER ====================
2026-01-30 19:56:34 +00:00
function calculateStats ( projects : Project [ ] ) {
const total = projects . length ;
if ( total === 0 ) {
return {
2026-01-30 21:44:22 +00:00
total : 0 , avgProgress : 0 , totalBudget : 0 , totalSpent : 0 ,
avgBudget : 0 , budgetConsumed : 0 , uniqueClients : 0 , avgDuration : 0 ,
2026-01-30 19:56:34 +00:00
} ;
}
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 ;
2026-01-30 21:44:22 +00:00
return { total , avgProgress , totalBudget , totalSpent , avgBudget , budgetConsumed , uniqueClients , avgDuration } ;
2026-01-30 19:56:34 +00:00
}
2026-01-30 21:44:22 +00:00
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 ) ) ;
2026-01-30 19:56:34 +00:00
}
2026-01-30 21:44:22 +00:00
// ==================== PANEL: TODOS LOS PROYECTOS ====================
function AllProjectsStats ( { projects } : { projects : Project [ ] } ) {
2026-01-30 19:56:34 +00:00
const stats = calculateStats ( projects ) ;
2026-01-30 21:44:22 +00:00
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 ) ,
} ) ) ;
2026-01-30 19:56:34 +00:00
return (
< div className = "space-y-6" >
2026-01-30 21:44:22 +00:00
{ /* KPIs principales */ }
< div className = "grid grid-cols-2 md:grid-cols-4 gap-4" >
< StatCard title = "Total Proyectos" value = { stats . total } subtitle = { ` ${ abiertos } activos, ${ cerrados } cerrados ` } icon = { < LayoutGrid className = "w-4 h-4 text-gray-400" / > } highlight / >
< StatCard title = "Presupuesto Total" value = { formatCurrency ( stats . totalBudget ) } subtitle = { ` Media: ${ formatCurrency ( stats . avgBudget ) } ` } icon = { < DollarSign className = "w-4 h-4 text-green-500" / > } / >
< StatCard title = "Progreso Medio" value = { ` ${ stats . avgProgress } % ` } subtitle = { ` ${ stats . budgetConsumed } % presupuesto usado ` } icon = { < TrendingUp className = "w-4 h-4 text-blue-500" / > } / >
< StatCard title = "Clientes" value = { stats . uniqueClients } subtitle = { ` ${ stats . avgDuration } días duración media ` } icon = { < Users className = "w-4 h-4 text-indigo-500" / > } / >
2026-01-30 19:56:34 +00:00
< / div >
2026-01-30 21:44:22 +00:00
{ /* Alertas de fechas */ }
{ ( overdueProjects . length > 0 || upcomingDeadlines . length > 0 ) && (
< div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
{ overdueProjects . length > 0 && (
2026-01-31 12:56:51 +00:00
< Card className = "border-red-200 bg-red-50 dark:bg-red-950/30 dark:border-red-900" >
2026-01-30 21:44:22 +00:00
< CardContent className = "pt-6" >
< div className = "flex items-center justify-between" >
< div >
2026-01-31 12:56:51 +00:00
< p className = "text-sm font-medium text-red-700 dark:text-red-400 flex items-center gap-2" >
2026-01-30 21:44:22 +00:00
< AlertCircle className = "w-4 h-4" / > Proyectos Retrasados
< / p >
2026-01-31 12:56:51 +00:00
< p className = "text-3xl font-bold text-red-600 dark:text-red-400 mt-2" > { overdueProjects . length } < / p >
< p className = "text-xs text-red-600 dark:text-red-400" > Fecha límite superada < / p >
2026-01-30 21:44:22 +00:00
< / div >
< div className = "text-right" >
{ overdueProjects . slice ( 0 , 2 ) . map ( p = > (
2026-01-31 12:56:51 +00:00
< p key = { p . id } className = "text-xs text-red-500 dark:text-red-400 truncate max-w-32" > { p . name } < / p >
2026-01-30 21:44:22 +00:00
) ) }
< / div >
< / div >
< / CardContent >
< / Card >
) }
{ upcomingDeadlines . length > 0 && (
2026-01-31 12:56:51 +00:00
< Card className = "border-amber-200 bg-amber-50 dark:bg-amber-950/30 dark:border-amber-900" >
2026-01-30 21:44:22 +00:00
< CardContent className = "pt-6" >
< div className = "flex items-center justify-between" >
< div >
2026-01-31 12:56:51 +00:00
< p className = "text-sm font-medium text-amber-700 dark:text-amber-400 flex items-center gap-2" >
2026-01-30 21:44:22 +00:00
< Clock className = "w-4 h-4" / > Próximos a Vencer
< / p >
2026-01-31 12:56:51 +00:00
< p className = "text-3xl font-bold text-amber-600 dark:text-amber-400 mt-2" > { upcomingDeadlines . length } < / p >
< p className = "text-xs text-amber-600 dark:text-amber-400" > En los próximos 14 días < / p >
2026-01-30 21:44:22 +00:00
< / div >
< div className = "text-right" >
{ upcomingDeadlines . slice ( 0 , 2 ) . map ( p = > (
2026-01-31 12:56:51 +00:00
< p key = { p . id } className = "text-xs text-amber-600 dark:text-amber-400 truncate max-w-32" >
2026-01-30 21:44:22 +00:00
{ p . name } ( { getDaysRemaining ( p . endDate ) } d )
< / p >
) ) }
< / div >
< / div >
< / CardContent >
< / Card >
) }
< / div >
) }
2026-01-30 19:56:34 +00:00
2026-01-30 21:44:22 +00:00
{ /* Gráficos principales */ }
< div className = "grid grid-cols-1 lg:grid-cols-3 gap-4" >
< StatusPieChart borradores = { borradores } abiertos = { abiertos } cerrados = { cerrados } / >
< ProgressDonut data = { progressData } title = "Distribución por Progreso" centerValue = { ` ${ stats . avgProgress } % ` } centerLabel = "Promedio" / >
< RadialMetric value = { stats . budgetConsumed } title = "Consumo de Presupuesto" subtitle = { formatCurrency ( stats . totalSpent ) } color = { stats . budgetConsumed > 80 ? COLORS.danger : COLORS.primary } / >
2026-01-30 19:56:34 +00:00
< / div >
2026-01-30 21:44:22 +00:00
{ /* Comparativas */ }
2026-01-30 19:56:34 +00:00
< div className = "grid grid-cols-1 lg:grid-cols-2 gap-4" >
2026-01-30 21:44:22 +00:00
< ComparisonBarChart data = { budgetByStatus } title = "Presupuesto por Estado" formatter = { ( v ) = > formatCurrency ( v ) } / >
< MultiRadialChart data = { topProjects } title = "Progreso de Top 5 Proyectos" valueLabel = "%" / >
2026-01-30 19:56:34 +00:00
< / div >
2026-01-30 21:44:22 +00:00
{ /* Timeline de fechas - Próximos vencimientos y retrasados */ }
{ ( upcomingDeadlines . length > 0 || overdueProjects . length > 0 ) && (
2026-01-31 12:56:51 +00:00
< Card className = "border" >
2026-01-30 21:44:22 +00:00
< CardHeader className = "pb-2" >
2026-01-31 12:56:51 +00:00
< CardTitle className = "text-sm font-medium text-muted-foreground flex items-center gap-2" >
2026-01-30 21:44:22 +00:00
< Calendar className = "w-4 h-4" / > Timeline de Proyectos
2026-01-30 19:56:34 +00:00
< / CardTitle >
< / CardHeader >
< CardContent >
2026-01-30 21:44:22 +00:00
< div className = "space-y-4" >
{ /* Proyectos retrasados */ }
{ overdueProjects . length > 0 && (
< div >
2026-01-31 12:56:51 +00:00
< h4 className = "text-xs font-semibold text-red-600 dark:text-red-400 uppercase tracking-wide mb-2 flex items-center gap-1" >
2026-01-30 21:44:22 +00:00
< AlertCircle className = "w-3 h-3" / > Retrasados ( { overdueProjects . length } )
< / h4 >
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2" >
{ overdueProjects . slice ( 0 , 6 ) . map ( ( p , index ) = > {
const daysOverdue = Math . abs ( getDaysRemaining ( p . endDate ) ) ;
return (
2026-01-31 12:56:51 +00:00
< div key = { p . id } className = "flex items-center gap-3 p-3 bg-red-50 dark:bg-red-950/30 border border-red-100 dark:border-red-900 rounded-lg" >
2026-01-30 21:44:22 +00:00
< div className = "w-2 h-2 rounded-full bg-red-500" / >
< div className = "flex-1 min-w-0" >
2026-01-31 12:56:51 +00:00
< p className = "text-sm font-medium text-foreground truncate" > { p . name } < / p >
< p className = "text-xs text-muted-foreground" > { p . progress } % completado < / p >
2026-01-30 21:44:22 +00:00
< / div >
2026-01-31 12:56:51 +00:00
< Badge variant = "outline" className = "border-red-300 dark:border-red-700 text-red-600 dark:text-red-400 text-xs" >
2026-01-30 21:44:22 +00:00
- { daysOverdue } d
< / Badge >
< / div >
) ;
} ) }
< / div >
< / div >
) }
2026-01-30 19:56:34 +00:00
2026-01-30 21:44:22 +00:00
{ /* Próximos vencimientos */ }
{ upcomingDeadlines . length > 0 && (
< div >
2026-01-31 12:56:51 +00:00
< h4 className = "text-xs font-semibold text-amber-600 dark:text-amber-400 uppercase tracking-wide mb-2 flex items-center gap-1" >
2026-01-30 21:44:22 +00:00
< Clock className = "w-3 h-3" / > Próximos 14 días ( { upcomingDeadlines . length } )
< / h4 >
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2" >
{ upcomingDeadlines . slice ( 0 , 6 ) . map ( ( p , index ) = > {
const days = getDaysRemaining ( p . endDate ) ;
return (
2026-01-31 12:56:51 +00:00
< div key = { p . id } className = "flex items-center gap-3 p-3 bg-muted/50 border rounded-lg" >
2026-01-30 21:44:22 +00:00
< div className = "w-2 h-2 rounded-full" style = { { backgroundColor : getProjectColor ( index ) } } / >
< div className = "flex-1 min-w-0" >
2026-01-31 12:56:51 +00:00
< p className = "text-sm font-medium text-foreground truncate" > { p . name } < / p >
< p className = "text-xs text-muted-foreground" > { p . progress } % completado < / p >
2026-01-30 21:44:22 +00:00
< / div >
< Badge
variant = "outline"
className = { ` text-xs ${
2026-01-31 12:56:51 +00:00
days <= 3 ? 'border-red-300 dark:border-red-700 text-red-600 dark:text-red-400' :
days <= 7 ? 'border-amber-300 dark:border-amber-700 text-amber-600 dark:text-amber-400' :
'border text-muted-foreground'
2026-01-30 21:44:22 +00:00
} ` }
>
{ days } d
< / Badge >
< / div >
) ;
} ) }
< / div >
< / div >
) }
{ /* Resumen de fechas */ }
2026-01-31 12:56:51 +00:00
< div className = "pt-3 border-t" >
< div className = "flex flex-wrap gap-4 text-xs text-muted-foreground" >
2026-01-30 21:44:22 +00:00
< span className = "flex items-center gap-1" >
< div className = "w-2 h-2 rounded-full bg-red-500" / >
Retrasados : { overdueProjects . length }
< / span >
< span className = "flex items-center gap-1" >
< div className = "w-2 h-2 rounded-full bg-amber-500" / >
Vencen en 7 días : { upcomingDeadlines . filter ( p = > getDaysRemaining ( p . endDate ) <= 7 ) . length }
< / span >
< span className = "flex items-center gap-1" >
< div className = "w-2 h-2 rounded-full bg-green-500" / >
En tiempo : { projects . filter ( p = > p . status === "1" && getDaysRemaining ( p . endDate ) > 14 ) . length }
< / span >
< / div >
2026-01-30 19:56:34 +00:00
< / div >
< / div >
< / CardContent >
< / Card >
2026-01-30 21:44:22 +00:00
) }
{ /* Presupuesto detallado */ }
< BudgetBarChart projects = { [ . . . projects ] . sort ( ( a , b ) = > b . budget - a . budget ) } title = "Presupuesto vs Gastado por Proyecto" maxItems = { 6 } / >
2026-01-30 19:56:34 +00:00
< / div >
) ;
}
2026-01-30 21:44:22 +00:00
// ==================== PANEL: PROYECTOS ACTIVOS ====================
function ActiveProjectsStats ( { projects } : { projects : Project [ ] } ) {
const activeProjects = projects . filter ( p = > p . status === "1" && p . progress < 100 ) ;
2026-01-30 19:56:34 +00:00
const stats = calculateStats ( activeProjects ) ;
if ( activeProjects . length === 0 ) {
return (
2026-01-30 21:44:22 +00:00
< div className = "flex flex-col items-center justify-center py-16" >
2026-01-31 12:56:51 +00:00
< TrendingUp className = "w-16 h-16 text-muted-foreground/50 mb-4" / >
< h3 className = "text-xl font-semibold text-foreground" > No hay proyectos activos < / h3 >
< p className = "text-muted-foreground mt-2" > Todos los proyectos están cerrados o en borrador < / p >
2026-01-30 19:56:34 +00:00
< / div >
) ;
}
2026-01-30 21:44:22 +00:00
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 ] } ,
] ;
2026-01-30 19:56:34 +00:00
2026-01-30 21:44:22 +00:00
// 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 ) ,
} ) ) ;
2026-01-30 19:56:34 +00:00
return (
< div className = "space-y-6" >
2026-01-30 21:44:22 +00:00
{ /* KPIs */ }
< div className = "grid grid-cols-2 md:grid-cols-4 gap-4" >
< StatCard title = "Proyectos Activos" value = { stats . total } subtitle = "En progreso" icon = { < TrendingUp className = "w-4 h-4 text-blue-500" / > } highlight / >
< StatCard title = "Presupuesto Activo" value = { formatCurrency ( stats . totalBudget ) } subtitle = { ` ${ stats . budgetConsumed } % consumido ` } icon = { < DollarSign className = "w-4 h-4 text-green-500" / > } / >
< StatCard title = "Progreso Medio" value = { ` ${ stats . avgProgress } % ` } subtitle = { ` ${ nearCompletion } casi listos ` } icon = { < Target className = "w-4 h-4 text-purple-500" / > } / >
< StatCard title = "Clientes Activos" value = { stats . uniqueClients } subtitle = "Con proyectos en curso" icon = { < Users className = "w-4 h-4 text-indigo-500" / > } / >
2026-01-30 19:56:34 +00:00
< / div >
{ /* Alertas */ }
2026-01-30 21:44:22 +00:00
< div className = "grid grid-cols-1 md:grid-cols-3 gap-4" >
2026-01-31 12:56:51 +00:00
< Card className = { ` ${ overdueProjects . length > 0 ? 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-950/30' : 'border bg-muted/30' } ` } >
2026-01-30 21:44:22 +00:00
< CardContent className = "pt-6" >
< div className = "flex items-center justify-between" >
< div >
2026-01-31 12:56:51 +00:00
< p className = { ` text-sm font-medium flex items-center gap-2 ${ overdueProjects . length > 0 ? 'text-red-700 dark:text-red-400' : 'text-muted-foreground' } ` } >
2026-01-30 21:44:22 +00:00
< AlertCircle className = "w-4 h-4" / > Retrasados
< / p >
2026-01-31 12:56:51 +00:00
< p className = { ` text-3xl font-bold mt-2 ${ overdueProjects . length > 0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50' } ` } > { overdueProjects . length } < / p >
< p className = { ` text-xs ${ overdueProjects . length > 0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50' } ` } > Fecha superada < / p >
2026-01-30 21:44:22 +00:00
< / div >
< / div >
< / CardContent >
< / Card >
2026-01-31 12:56:51 +00:00
< Card className = { ` ${ needsAttention > 0 ? 'border-orange-200 dark:border-orange-900 bg-orange-50 dark:bg-orange-950/30' : 'border bg-muted/30' } ` } >
2026-01-30 21:44:22 +00:00
< CardContent className = "pt-6" >
< div className = "flex items-center justify-between" >
< div >
2026-01-31 12:56:51 +00:00
< p className = { ` text-sm font-medium flex items-center gap-2 ${ needsAttention > 0 ? 'text-orange-700 dark:text-orange-400' : 'text-muted-foreground' } ` } >
2026-01-30 21:44:22 +00:00
< Clock className = "w-4 h-4" / > Requieren Atención
< / p >
2026-01-31 12:56:51 +00:00
< p className = { ` text-3xl font-bold mt-2 ${ needsAttention > 0 ? 'text-orange-600 dark:text-orange-400' : 'text-muted-foreground/50' } ` } > { needsAttention } < / p >
< p className = { ` text-xs ${ needsAttention > 0 ? 'text-orange-600 dark:text-orange-400' : 'text-muted-foreground/50' } ` } > Menos del 25 % < / p >
2026-01-30 21:44:22 +00:00
< / div >
< / div >
2026-01-30 19:56:34 +00:00
< / CardContent >
< / Card >
2026-01-31 12:56:51 +00:00
< Card className = "border-green-200 dark:border-green-900 bg-green-50 dark:bg-green-950/30" >
2026-01-30 21:44:22 +00:00
< CardContent className = "pt-6" >
< div className = "flex items-center justify-between" >
< div >
2026-01-31 12:56:51 +00:00
< p className = "text-sm font-medium text-green-700 dark:text-green-400 flex items-center gap-2" >
2026-01-30 21:44:22 +00:00
< CheckCircle2 className = "w-4 h-4" / > Próximos a Completar
< / p >
2026-01-31 12:56:51 +00:00
< p className = "text-3xl font-bold text-green-600 dark:text-green-400 mt-2" > { nearCompletion } < / p >
< p className = "text-xs text-green-600 dark:text-green-400" > 75 % o más < / p >
2026-01-30 21:44:22 +00:00
< / div >
< / div >
2026-01-30 19:56:34 +00:00
< / CardContent >
< / Card >
< / div >
2026-01-30 21:44:22 +00:00
{ /* Gráficos */ }
2026-01-30 19:56:34 +00:00
< div className = "grid grid-cols-1 lg:grid-cols-2 gap-4" >
2026-01-30 21:44:22 +00:00
< ProgressDonut data = { progressData } title = "Estado de Avance" centerValue = { stats . total } centerLabel = "Activos" / >
< MultiRadialChart data = { topByProgress } title = "Top 5 Proyectos por Progreso" valueLabel = "%" / >
2026-01-30 19:56:34 +00:00
< / div >
2026-01-30 21:44:22 +00:00
{ /* Próximos vencimientos */ }
{ upcomingDeadlines . length > 0 && (
2026-01-31 12:56:51 +00:00
< Card className = "border" >
2026-01-30 21:44:22 +00:00
< CardHeader className = "pb-2" >
2026-01-31 12:56:51 +00:00
< CardTitle className = "text-sm font-medium text-muted-foreground flex items-center gap-2" >
2026-01-30 21:44:22 +00:00
< Calendar className = "w-4 h-4" / > Próximos Vencimientos ( 14 días )
< / CardTitle >
< / CardHeader >
< CardContent >
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3" >
{ upcomingDeadlines . slice ( 0 , 6 ) . map ( ( p , index ) = > {
const days = getDaysRemaining ( p . endDate ) ;
return (
2026-01-31 12:56:51 +00:00
< div key = { p . id } className = "flex items-center gap-3 p-3 bg-muted/50 rounded-lg" >
2026-01-30 21:44:22 +00:00
< div className = "w-2 h-2 rounded-full" style = { { backgroundColor : getProjectColor ( index ) } } / >
< div className = "flex-1 min-w-0" >
2026-01-31 12:56:51 +00:00
< p className = "text-sm font-medium text-foreground truncate" > { p . name } < / p >
< p className = "text-xs text-muted-foreground" > { p . progress } % completado < / p >
2026-01-30 21:44:22 +00:00
< / div >
2026-01-31 12:56:51 +00:00
< Badge variant = "outline" className = { days <= 3 ? 'border-red-300 dark:border-red-700 text-red-600 dark:text-red-400' : days <= 7 ? 'border-amber-300 dark:border-amber-700 text-amber-600 dark:text-amber-400' : '' } >
2026-01-30 21:44:22 +00:00
{ days } d
< / Badge >
< / div >
) ;
} ) }
2026-01-30 19:56:34 +00:00
< / div >
2026-01-30 21:44:22 +00:00
< / CardContent >
< / Card >
) }
{ /* Presupuesto */ }
< BudgetBarChart projects = { activeProjects . sort ( ( a , b ) = > b . budget - a . budget ) } title = "Presupuesto vs Gastado por Proyecto" maxItems = { 6 } / >
2026-01-30 19:56:34 +00:00
< / div >
) ;
}
2026-01-30 21:44:22 +00:00
// ==================== PANEL: PROYECTOS CERRADOS ====================
function ClosedProjectsStats ( { projects } : { projects : Project [ ] } ) {
const closedProjects = projects . filter ( p = > p . status === "2" ) ;
const stats = calculateStats ( closedProjects ) ;
2026-01-30 19:56:34 +00:00
2026-01-30 21:44:22 +00:00
if ( closedProjects . length === 0 ) {
2026-01-30 19:56:34 +00:00
return (
2026-01-30 21:44:22 +00:00
< div className = "flex flex-col items-center justify-center py-16" >
2026-01-31 12:56:51 +00:00
< Lock className = "w-16 h-16 text-muted-foreground/50 mb-4" / >
< h3 className = "text-xl font-semibold text-foreground" > No hay proyectos cerrados < / h3 >
< p className = "text-muted-foreground mt-2" > Aún no se ha cerrado ningún proyecto < / p >
2026-01-30 19:56:34 +00:00
< / div >
) ;
}
2026-01-30 21:44:22 +00:00
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 ) ,
} ;
} ) ;
2026-01-30 19:56:34 +00:00
return (
< div className = "space-y-6" >
2026-01-30 21:44:22 +00:00
{ /* KPIs */ }
< div className = "grid grid-cols-2 md:grid-cols-4 gap-4" >
< StatCard title = "Proyectos Cerrados" value = { stats . total } subtitle = { ` ${ fullyCompleted } al 100% ` } icon = { < Lock className = "w-4 h-4 text-gray-500" / > } highlight / >
< StatCard title = "Presupuesto Gestionado" value = { formatCurrency ( stats . totalBudget ) } subtitle = { ` Gastado: ${ formatCurrency ( stats . totalSpent ) } ` } icon = { < DollarSign className = "w-4 h-4 text-green-500" / > } / >
< StatCard title = "Tasa de Éxito" value = { ` ${ stats . total > 0 ? Math . round ( ( withinBudget / stats . total ) * 100 ) : 0 } % ` } subtitle = { ` ${ withinBudget } dentro de presupuesto ` } icon = { < CheckCircle2 className = "w-4 h-4 text-green-500" / > } / >
< StatCard title = "Ahorro Total" value = { formatCurrency ( savings ) } subtitle = { ` ${ 100 - stats . budgetConsumed } % no usado ` } icon = { < PiggyBank className = "w-4 h-4 text-emerald-500" / > } / >
2026-01-30 19:56:34 +00:00
< / div >
2026-01-30 21:44:22 +00:00
{ /* Cumplimiento visual */ }
2026-01-30 19:56:34 +00:00
< div className = "grid grid-cols-1 md:grid-cols-2 gap-4" >
2026-01-31 12:56:51 +00:00
< Card className = "border-green-200 dark:border-green-900 bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/30 dark:to-emerald-950/30" >
2026-01-30 21:44:22 +00:00
< CardContent className = "py-6" >
< div className = "flex items-center gap-6" >
2026-01-31 12:56:51 +00:00
< div className = "p-4 bg-green-100 dark:bg-green-900/50 rounded-full" >
< CheckCircle2 className = "w-8 h-8 text-green-600 dark:text-green-400" / >
2026-01-30 21:44:22 +00:00
< / div >
< div >
2026-01-31 12:56:51 +00:00
< p className = "text-sm font-medium text-green-700 dark:text-green-400" > Dentro del Presupuesto < / p >
< p className = "text-4xl font-bold text-green-600 dark:text-green-400" > { withinBudget } < / p >
< p className = "text-sm text-green-600 dark:text-green-400" > { stats . total > 0 ? Math . round ( ( withinBudget / stats . total ) * 100 ) : 0 } % de los cerrados < / p >
2026-01-30 21:44:22 +00:00
< / div >
< / div >
2026-01-30 19:56:34 +00:00
< / CardContent >
< / Card >
2026-01-31 12:56:51 +00:00
< Card className = { ` ${ overBudget > 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' } ` } >
2026-01-30 21:44:22 +00:00
< CardContent className = "py-6" >
< div className = "flex items-center gap-6" >
2026-01-31 12:56:51 +00:00
< div className = { ` p-4 rounded-full ${ overBudget > 0 ? 'bg-red-100 dark:bg-red-900/50' : 'bg-muted' } ` } >
< AlertCircle className = { ` w-8 h-8 ${ overBudget > 0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50' } ` } / >
2026-01-30 21:44:22 +00:00
< / div >
< div >
2026-01-31 12:56:51 +00:00
< p className = { ` text-sm font-medium ${ overBudget > 0 ? 'text-red-700 dark:text-red-400' : 'text-muted-foreground' } ` } > Excedieron Presupuesto < / p >
< p className = { ` text-4xl font-bold ${ overBudget > 0 ? 'text-red-600 dark:text-red-400' : 'text-muted-foreground/50' } ` } > { overBudget } < / p >
< p className = { ` text-sm ${ 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 < / p >
2026-01-30 21:44:22 +00:00
< / div >
< / div >
2026-01-30 19:56:34 +00:00
< / CardContent >
< / Card >
< / div >
2026-01-30 21:44:22 +00:00
{ /* Gráficos */ }
< div className = "grid grid-cols-1 lg:grid-cols-3 gap-4" >
< ProgressDonut data = { budgetComplianceData } title = "Cumplimiento Presupuestario" centerValue = { ` ${ stats . total > 0 ? Math . round ( ( withinBudget / stats . total ) * 100 ) : 0 } % ` } centerLabel = "Éxito" / >
< ProgressDonut data = { completionData } title = "Estado de Completitud" centerValue = { fullyCompleted } centerLabel = "Completos" / >
< RadialMetric value = { Math . min ( 100 , 100 - stats . budgetConsumed ) } title = "Eficiencia Global" subtitle = "ahorro promedio" color = { COLORS . success } / >
2026-01-30 19:56:34 +00:00
< / div >
2026-01-30 21:44:22 +00:00
{ /* Eficiencia por proyecto */ }
2026-01-30 19:56:34 +00:00
< div className = "grid grid-cols-1 lg:grid-cols-2 gap-4" >
2026-01-30 21:44:22 +00:00
< MultiRadialChart data = { efficiencyData } title = "Eficiencia por Proyecto (% Ahorrado)" valueLabel = "%" / >
< BudgetBarChart projects = { closedProjects . sort ( ( a , b ) = > b . budget - a . budget ) } title = "Presupuesto vs Gastado" maxItems = { 5 } / >
< / div >
< / div >
) ;
}
2026-01-30 19:56:34 +00:00
2026-01-30 21:44:22 +00:00
// ==================== 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 (
< div className = "flex flex-col items-center justify-center py-16" >
2026-01-31 12:56:51 +00:00
< CheckCircle2 className = "w-16 h-16 text-muted-foreground/50 mb-4" / >
< h3 className = "text-xl font-semibold text-foreground" > No hay proyectos completados < / h3 >
< p className = "text-muted-foreground mt-2" > Aún no se ha completado ningún proyecto al 100 % < / p >
2026-01-30 19:56:34 +00:00
< / div >
2026-01-30 21:44:22 +00:00
) ;
}
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 ;
2026-01-30 19:56:34 +00:00
2026-01-30 21:44:22 +00:00
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 (
< div className = "space-y-6" >
{ /* Banner de éxito */ }
< Card className = "border-green-300 bg-gradient-to-r from-green-500 to-emerald-600 text-white" >
< CardContent className = "py-8" >
< div className = "flex items-center justify-center gap-8 md:gap-16 flex-wrap" >
2026-01-30 19:56:34 +00:00
< div className = "text-center" >
2026-01-30 21:44:22 +00:00
< p className = "text-5xl font-bold" > { stats . total } < / p >
< p className = "text-sm text-green-100 mt-2" > Proyectos al 100 % < / p >
2026-01-30 19:56:34 +00:00
< / div >
2026-01-30 21:44:22 +00:00
< div className = "h-16 w-px bg-green-400 hidden md:block" / >
< div className = "text-center" >
< p className = "text-5xl font-bold" > { successRate } % < / p >
< p className = "text-sm text-green-100 mt-2" > Tasa de é xito < / p >
2026-01-30 19:56:34 +00:00
< / div >
2026-01-30 21:44:22 +00:00
< div className = "h-16 w-px bg-green-400 hidden md:block" / >
2026-01-30 19:56:34 +00:00
< div className = "text-center" >
2026-01-30 21:44:22 +00:00
< p className = "text-5xl font-bold" > { formatCurrency ( savings ) } < / p >
< p className = "text-sm text-green-100 mt-2" > Ahorro total < / p >
2026-01-30 19:56:34 +00:00
< / div >
< / div >
< / CardContent >
< / Card >
2026-01-30 21:44:22 +00:00
{ /* KPIs */ }
< div className = "grid grid-cols-2 md:grid-cols-4 gap-4" >
< StatCard title = "Presupuesto Total" value = { formatCurrency ( stats . totalBudget ) } subtitle = "Gestionado" icon = { < DollarSign className = "w-4 h-4 text-green-500" / > } / >
< StatCard title = "Total Gastado" value = { formatCurrency ( stats . totalSpent ) } subtitle = { ` ${ stats . budgetConsumed } % usado ` } icon = { < PiggyBank className = "w-4 h-4 text-blue-500" / > } / >
< StatCard title = "Dentro Presupuesto" value = { withinBudget } subtitle = { ` de ${ stats . total } completados ` } icon = { < CheckCircle2 className = "w-4 h-4 text-green-500" / > } / >
< StatCard title = "Clientes Satisfechos" value = { stats . uniqueClients } subtitle = "Proyectos entregados" icon = { < Users className = "w-4 h-4 text-indigo-500" / > } / >
< / div >
{ /* Gráficos */ }
< div className = "grid grid-cols-1 lg:grid-cols-2 gap-4" >
< ProgressDonut data = { statusData } title = "Estado Administrativo" centerValue = { stats . total } centerLabel = "Completados" / >
< ComparisonBarChart data = { budgetData } title = "Presupuesto por Proyecto Completado" formatter = { ( v ) = > formatCurrency ( v ) } / >
< / div >
{ /* Detalle de presupuesto */ }
< BudgetBarChart projects = { completedProjects . sort ( ( a , b ) = > b . budget - a . budget ) } title = "Detalle: Presupuesto vs Gastado" maxItems = { 6 } / >
2026-01-30 19:56:34 +00:00
< / div >
) ;
}
2026-01-30 21:44:22 +00:00
// ==================== SKELETON ====================
2026-01-30 19:56:34 +00:00
function StatisticsSkeleton() {
return (
< div className = "space-y-6" >
2026-01-30 21:44:22 +00:00
< div className = "grid grid-cols-2 md:grid-cols-4 gap-4" >
2026-01-30 19:56:34 +00:00
{ Array . from ( { length : 4 } ) . map ( ( _ , i ) = > (
2026-01-31 12:56:51 +00:00
< Card key = { i } className = "border" >
2026-01-30 21:44:22 +00:00
< CardHeader className = "pb-2" > < Skeleton className = "h-4 w-24" / > < / CardHeader >
< CardContent > < Skeleton className = "h-8 w-16 mb-2" / > < Skeleton className = "h-3 w-32" / > < / CardContent >
2026-01-30 19:56:34 +00:00
< / Card >
) ) }
< / div >
2026-01-30 21:44:22 +00:00
< div className = "grid grid-cols-1 lg:grid-cols-3 gap-4" >
{ Array . from ( { length : 3 } ) . map ( ( _ , i ) = > (
2026-01-31 12:56:51 +00:00
< Card key = { i } className = "border" >
2026-01-30 21:44:22 +00:00
< CardHeader className = "pb-2" > < Skeleton className = "h-4 w-32" / > < / CardHeader >
< CardContent > < Skeleton className = "h-64 w-full" / > < / CardContent >
2026-01-30 19:56:34 +00:00
< / Card >
) ) }
< / div >
< / div >
) ;
}
2026-01-30 21:44:22 +00:00
// ==================== COMPONENTE PRINCIPAL ====================
2026-01-30 19:56:34 +00:00
export default function StatisticsPage() {
const [ projects , setProjects ] = useState < Project [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ error , setError ] = useState < string | null > ( 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 (
2026-01-31 12:56:51 +00:00
< div className = "min-h-screen bg-background flex items-center justify-center" >
2026-01-30 19:56:34 +00:00
< div className = "text-center" >
2026-01-31 12:56:51 +00:00
< AlertCircle className = "w-16 h-16 text-red-400 dark:text-red-500 mx-auto mb-4" / >
< p className = "text-red-600 dark:text-red-400 text-lg" > { error } < / p >
2026-01-30 21:44:22 +00:00
< button onClick = { ( ) = > window . location . reload ( ) } className = "mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" >
2026-01-30 19:56:34 +00:00
Reintentar
< / button >
< / div >
< / div >
) ;
}
2026-01-30 21:44:22 +00:00
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 ;
2026-01-30 19:56:34 +00:00
return (
2026-01-31 12:56:51 +00:00
< div className = "min-h-screen bg-background" >
< header className = "bg-card border-b sticky top-0 z-10" >
2026-01-30 21:44:22 +00:00
< div className = "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6" >
< div className = "flex items-center gap-3" >
< div className = "p-2 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg" >
< BarChart3 className = "w-6 h-6 text-white" / >
< / div >
< div >
2026-01-31 12:56:51 +00:00
< h1 className = "text-2xl font-bold text-foreground" > Estadisticas < / h1 >
< p className = "text-sm text-muted-foreground" > Analisis de { projects . length } proyectos en Dolibarr < / p >
2026-01-30 19:56:34 +00:00
< / div >
< / div >
< / div >
< / header >
< main className = "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" >
{ loading ? (
< StatisticsSkeleton / >
) : (
< Tabs defaultValue = "todos" className = "space-y-6" >
2026-01-30 21:44:22 +00:00
< TabsList className = "grid w-full grid-cols-4 lg:w-auto lg:inline-grid" >
2026-01-30 19:56:34 +00:00
< TabsTrigger value = "todos" className = "gap-2" >
< LayoutGrid className = "w-4 h-4" / >
< span className = "hidden sm:inline" > Todos < / span >
2026-01-30 21:44:22 +00:00
< Badge variant = "secondary" className = "ml-1" > { projects . length } < / Badge >
2026-01-30 19:56:34 +00:00
< / TabsTrigger >
< TabsTrigger value = "activos" className = "gap-2" >
< TrendingUp className = "w-4 h-4" / >
< span className = "hidden sm:inline" > Activos < / span >
2026-01-30 21:44:22 +00:00
< Badge variant = "secondary" className = "ml-1 bg-blue-100 text-blue-700" > { activeCount } < / Badge >
< / TabsTrigger >
< TabsTrigger value = "cerrados" className = "gap-2" >
< Lock className = "w-4 h-4" / >
< span className = "hidden sm:inline" > Cerrados < / span >
< Badge variant = "secondary" className = "ml-1 bg-gray-100 text-gray-700" > { closedCount } < / Badge >
2026-01-30 19:56:34 +00:00
< / TabsTrigger >
< TabsTrigger value = "completados" className = "gap-2" >
< CheckCircle2 className = "w-4 h-4" / >
< span className = "hidden sm:inline" > Completados < / span >
2026-01-30 21:44:22 +00:00
< Badge variant = "secondary" className = "ml-1 bg-green-100 text-green-700" > { completedCount } < / Badge >
2026-01-30 19:56:34 +00:00
< / TabsTrigger >
< / TabsList >
2026-01-30 21:44:22 +00:00
< TabsContent value = "todos" > < AllProjectsStats projects = { projects } / > < / TabsContent >
< TabsContent value = "activos" > < ActiveProjectsStats projects = { projects } / > < / TabsContent >
< TabsContent value = "cerrados" > < ClosedProjectsStats projects = { projects } / > < / TabsContent >
< TabsContent value = "completados" > < CompletedProjectsStats projects = { projects } / > < / TabsContent >
2026-01-30 19:56:34 +00:00
< / Tabs >
) }
< / main >
< / div >
) ;
}