trello_fake/components/statistics/statistics-page.tsx

886 lines
33 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import {
LayoutGrid,
TrendingUp,
CheckCircle2,
DollarSign,
Clock,
Calendar,
Users,
Target,
PiggyBank,
FileText,
AlertCircle,
BarChart3,
} from "lucide-react";
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, ProjectStatus, STATUS_CONFIG } from "@/types/project";
// Componente para una card de estadística individual
interface StatCardProps {
title: string;
value: string | number;
subtitle?: string;
icon: React.ReactNode;
trend?: {
value: number;
label: string;
positive?: boolean;
};
highlight?: boolean;
}
function StatCard({ title, value, subtitle, icon, trend, highlight }: StatCardProps) {
return (
<Card className={`border-gray-200 hover:shadow-md transition-shadow ${highlight ? "ring-2 ring-blue-500/20" : ""}`}>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle>
{icon}
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-gray-900">{value}</div>
{subtitle && <p className="text-xs text-gray-500 mt-1">{subtitle}</p>}
{trend && (
<div className={`flex items-center gap-1 mt-2 text-xs ${trend.positive ? "text-green-600" : "text-red-600"}`}>
<span>{trend.positive ? "+" : ""}{trend.value}%</span>
<span className="text-gray-500">{trend.label}</span>
</div>
)}
</CardContent>
</Card>
);
}
// Componente para mostrar la distribución por estado
interface StatusDistributionProps {
projects: Project[];
}
function StatusDistribution({ projects }: StatusDistributionProps) {
const statusCounts: Record<ProjectStatus, number> = {
"0": projects.filter((p) => p.status === "0").length,
"1": projects.filter((p) => p.status === "1").length,
"2": projects.filter((p) => p.status === "2").length,
};
const total = projects.length;
return (
<Card className="border-gray-200">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
<BarChart3 className="w-4 h-4" />
Distribucion por Estado
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{(Object.keys(statusCounts) as ProjectStatus[]).map((status) => {
const count = statusCounts[status];
const percentage = total > 0 ? Math.round((count / total) * 100) : 0;
const config = STATUS_CONFIG[status];
return (
<div key={status} className="space-y-2">
<div className="flex items-center justify-between">
<Badge variant="outline" className={config.getColorClasses()}>
{config.label}
</Badge>
<span className="text-sm font-medium text-gray-700">
{count} ({percentage}%)
</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${
status === "0" ? "bg-gray-400" : status === "1" ? "bg-blue-500" : "bg-green-500"
}`}
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
})}
</CardContent>
</Card>
);
}
// Componente para estadísticas de presupuesto
interface BudgetStatsProps {
projects: Project[];
title?: string;
}
function BudgetStats({ projects, title = "Resumen de Presupuesto" }: BudgetStatsProps) {
const totalBudget = projects.reduce((acc, p) => acc + p.budget, 0);
const totalSpent = projects.reduce((acc, p) => acc + p.spent, 0);
const remaining = totalBudget - totalSpent;
const spentPercentage = totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0;
return (
<Card className="border-gray-200">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
<PiggyBank className="w-4 h-4" />
{title}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<p className="text-2xl font-bold text-gray-900">
{totalBudget >= 1000 ? `${(totalBudget / 1000).toFixed(0)}k` : totalBudget.toLocaleString("es-ES")}
</p>
<p className="text-xs text-gray-500">Presupuesto Total</p>
</div>
<div>
<p className="text-2xl font-bold text-blue-600">
{totalSpent >= 1000 ? `${(totalSpent / 1000).toFixed(0)}k` : totalSpent.toLocaleString("es-ES")}
</p>
<p className="text-xs text-gray-500">Gastado</p>
</div>
<div>
<p className={`text-2xl font-bold ${remaining >= 0 ? "text-green-600" : "text-red-600"}`}>
{remaining >= 1000 ? `${(remaining / 1000).toFixed(0)}k` : remaining.toLocaleString("es-ES")}
</p>
<p className="text-xs text-gray-500">Restante</p>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Consumido</span>
<span className="font-medium">{spentPercentage}%</span>
</div>
<div className="w-full bg-gray-100 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all ${
spentPercentage > 90 ? "bg-red-500" : spentPercentage > 70 ? "bg-yellow-500" : "bg-blue-500"
}`}
style={{ width: `${Math.min(spentPercentage, 100)}%` }}
/>
</div>
</div>
</CardContent>
</Card>
);
}
// Funciones helper para calcular estadísticas
function calculateStats(projects: Project[]) {
const total = projects.length;
if (total === 0) {
return {
total: 0,
avgProgress: 0,
totalBudget: 0,
totalSpent: 0,
avgBudget: 0,
budgetConsumed: 0,
minBudget: 0,
maxBudget: 0,
uniqueClients: 0,
oldestProject: null as string | null,
newestProject: null as string | null,
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 budgets = projects.map((p) => p.budget);
const minBudget = Math.min(...budgets);
const maxBudget = Math.max(...budgets);
const uniqueClients = new Set(projects.map((p) => p.client)).size;
const dates = projects.map((p) => new Date(p.startDate).getTime()).filter((d) => !isNaN(d));
const oldestProject = dates.length > 0 ? new Date(Math.min(...dates)).toLocaleDateString("es-ES") : null;
const newestProject = dates.length > 0 ? new Date(Math.max(...dates)).toLocaleDateString("es-ES") : null;
// Calcular duracion promedio en dias
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,
minBudget,
maxBudget,
uniqueClients,
oldestProject,
newestProject,
avgDuration,
};
}
// Panel de estadísticas para "Todos los proyectos"
interface AllProjectsStatsProps {
projects: Project[];
}
function AllProjectsStats({ projects }: AllProjectsStatsProps) {
const stats = calculateStats(projects);
const activeProjects = projects.filter((p) => p.status !== "2" && p.progress < 100);
const completedProjects = projects.filter((p) => p.status === "2" || p.progress >= 100);
return (
<div className="space-y-6">
{/* Cards principales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="Total Proyectos"
value={stats.total}
subtitle="En toda la plataforma"
icon={<LayoutGrid className="w-4 h-4 text-gray-400" />}
highlight
/>
<StatCard
title="Proyectos Activos"
value={activeProjects.length}
subtitle={`${stats.total > 0 ? Math.round((activeProjects.length / stats.total) * 100) : 0}% del total`}
icon={<TrendingUp className="w-4 h-4 text-blue-500" />}
/>
<StatCard
title="Proyectos Completados"
value={completedProjects.length}
subtitle={`${stats.total > 0 ? Math.round((completedProjects.length / stats.total) * 100) : 0}% del total`}
icon={<CheckCircle2 className="w-4 h-4 text-green-500" />}
/>
<StatCard
title="Progreso Medio"
value={`${stats.avgProgress}%`}
subtitle="De todos los proyectos"
icon={<Clock className="w-4 h-4 text-purple-500" />}
/>
</div>
{/* Segunda fila: Presupuesto */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="Presupuesto Total"
value={`${(stats.totalBudget / 1000).toFixed(0)}k`}
subtitle="Suma de todos los proyectos"
icon={<DollarSign className="w-4 h-4 text-green-500" />}
/>
<StatCard
title="Gastado Total"
value={`${(stats.totalSpent / 1000).toFixed(0)}k`}
subtitle={`${stats.budgetConsumed}% del presupuesto`}
icon={<PiggyBank className="w-4 h-4 text-orange-500" />}
/>
<StatCard
title="Presupuesto Promedio"
value={`${(stats.avgBudget / 1000).toFixed(1)}k`}
subtitle="Por proyecto"
icon={<Target className="w-4 h-4 text-blue-500" />}
/>
<StatCard
title="Clientes Unicos"
value={stats.uniqueClients}
subtitle="Con proyectos activos"
icon={<Users className="w-4 h-4 text-indigo-500" />}
/>
</div>
{/* Tercera fila: Fechas y distribucion */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<StatCard
title="Duracion Promedio"
value={`${stats.avgDuration} dias`}
subtitle="Por proyecto"
icon={<Calendar className="w-4 h-4 text-teal-500" />}
/>
<StatCard
title="Proyecto mas Antiguo"
value={stats.oldestProject || "N/A"}
subtitle="Fecha de inicio"
icon={<FileText className="w-4 h-4 text-gray-500" />}
/>
<StatCard
title="Proyecto mas Reciente"
value={stats.newestProject || "N/A"}
subtitle="Fecha de inicio"
icon={<FileText className="w-4 h-4 text-blue-500" />}
/>
</div>
{/* Cuarta fila: Graficos/Distribuciones */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<StatusDistribution projects={projects} />
<BudgetStats projects={projects} />
</div>
{/* Info adicional: Rango de presupuestos */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="border-gray-200">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
<DollarSign className="w-4 h-4" />
Rango de Presupuestos
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-center">
<p className="text-sm text-gray-500">Minimo</p>
<p className="text-xl font-bold text-gray-900">{stats.minBudget.toLocaleString("es-ES")}</p>
</div>
<div className="flex-1 mx-4 h-px bg-gradient-to-r from-red-300 via-yellow-300 to-green-300" />
<div className="text-center">
<p className="text-sm text-gray-500">Maximo</p>
<p className="text-xl font-bold text-gray-900">{stats.maxBudget.toLocaleString("es-ES")}</p>
</div>
</div>
</CardContent>
</Card>
<Card className="border-gray-200">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
Proyectos por Progreso
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-4 gap-2 text-center">
<div>
<p className="text-lg font-bold text-red-600">
{projects.filter((p) => p.progress < 25).length}
</p>
<p className="text-xs text-gray-500">0-25%</p>
</div>
<div>
<p className="text-lg font-bold text-orange-600">
{projects.filter((p) => p.progress >= 25 && p.progress < 50).length}
</p>
<p className="text-xs text-gray-500">25-50%</p>
</div>
<div>
<p className="text-lg font-bold text-yellow-600">
{projects.filter((p) => p.progress >= 50 && p.progress < 75).length}
</p>
<p className="text-xs text-gray-500">50-75%</p>
</div>
<div>
<p className="text-lg font-bold text-green-600">
{projects.filter((p) => p.progress >= 75).length}
</p>
<p className="text-xs text-gray-500">75-100%</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}
// Panel de estadísticas para "Proyectos Activos"
interface ActiveProjectsStatsProps {
projects: Project[];
}
function ActiveProjectsStats({ projects }: ActiveProjectsStatsProps) {
// Filtrar solo proyectos activos (no cerrados y no completados al 100%)
const activeProjects = projects.filter((p) => p.status !== "2" && p.progress < 100);
const stats = calculateStats(activeProjects);
if (activeProjects.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<TrendingUp className="w-16 h-16 text-gray-300 mb-4" />
<h3 className="text-xl font-semibold text-gray-700">No hay proyectos activos</h3>
<p className="text-gray-500 mt-2">Todos los proyectos estan completados o cerrados</p>
</div>
);
}
// Proyectos que necesitan atencion (bajo progreso o alto consumo de presupuesto)
const needsAttention = activeProjects.filter((p) => {
const spentPercentage = p.budget > 0 ? (p.spent / p.budget) * 100 : 0;
return p.progress < 25 || spentPercentage > 80;
});
// Proyectos proximos a terminar
const nearCompletion = activeProjects.filter((p) => p.progress >= 75);
return (
<div className="space-y-6">
{/* Cards principales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="Proyectos Activos"
value={stats.total}
subtitle="En progreso actualmente"
icon={<TrendingUp className="w-4 h-4 text-blue-500" />}
highlight
/>
<StatCard
title="Progreso Promedio"
value={`${stats.avgProgress}%`}
subtitle="De proyectos activos"
icon={<Clock className="w-4 h-4 text-purple-500" />}
/>
<StatCard
title="Presupuesto Activo"
value={`${(stats.totalBudget / 1000).toFixed(0)}k`}
subtitle="En proyectos en curso"
icon={<DollarSign className="w-4 h-4 text-green-500" />}
/>
<StatCard
title="Presupuesto Consumido"
value={`${stats.budgetConsumed}%`}
subtitle={`${(stats.totalSpent / 1000).toFixed(0)}k gastado`}
icon={<PiggyBank className="w-4 h-4 text-orange-500" />}
/>
</div>
{/* Alertas */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="border-orange-200 bg-orange-50">
<CardHeader>
<CardTitle className="text-sm font-medium text-orange-700 flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
Requieren Atencion
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-orange-600">{needsAttention.length}</p>
<p className="text-xs text-orange-600 mt-1">
Proyectos con bajo progreso o alto consumo de presupuesto
</p>
</CardContent>
</Card>
<Card className="border-green-200 bg-green-50">
<CardHeader>
<CardTitle className="text-sm font-medium text-green-700 flex items-center gap-2">
<CheckCircle2 className="w-4 h-4" />
Proximos a Completar
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-green-600">{nearCompletion.length}</p>
<p className="text-xs text-green-600 mt-1">
Proyectos con 75% o mas de progreso
</p>
</CardContent>
</Card>
</div>
{/* Estadísticas adicionales */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StatCard
title="Clientes Activos"
value={stats.uniqueClients}
subtitle="Con proyectos en curso"
icon={<Users className="w-4 h-4 text-indigo-500" />}
/>
<StatCard
title="Duracion Promedio"
value={`${stats.avgDuration} dias`}
subtitle="De proyectos activos"
icon={<Calendar className="w-4 h-4 text-teal-500" />}
/>
<StatCard
title="Presupuesto Promedio"
value={`${(stats.avgBudget / 1000).toFixed(1)}k`}
subtitle="Por proyecto activo"
icon={<Target className="w-4 h-4 text-blue-500" />}
/>
</div>
{/* Distribucion y Budget */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<StatusDistribution projects={activeProjects} />
<BudgetStats projects={activeProjects} title="Presupuesto de Proyectos Activos" />
</div>
{/* Proyectos por progreso */}
<Card className="border-gray-200">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
<BarChart3 className="w-4 h-4" />
Distribucion por Progreso
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-4 gap-4 text-center">
<div className="p-4 bg-red-50 rounded-lg">
<p className="text-2xl font-bold text-red-600">
{activeProjects.filter((p) => p.progress < 25).length}
</p>
<p className="text-xs text-red-600 mt-1">Inicio (0-25%)</p>
</div>
<div className="p-4 bg-orange-50 rounded-lg">
<p className="text-2xl font-bold text-orange-600">
{activeProjects.filter((p) => p.progress >= 25 && p.progress < 50).length}
</p>
<p className="text-xs text-orange-600 mt-1">En curso (25-50%)</p>
</div>
<div className="p-4 bg-yellow-50 rounded-lg">
<p className="text-2xl font-bold text-yellow-600">
{activeProjects.filter((p) => p.progress >= 50 && p.progress < 75).length}
</p>
<p className="text-xs text-yellow-600 mt-1">Avanzado (50-75%)</p>
</div>
<div className="p-4 bg-green-50 rounded-lg">
<p className="text-2xl font-bold text-green-600">
{activeProjects.filter((p) => p.progress >= 75).length}
</p>
<p className="text-xs text-green-600 mt-1">Casi listo (75-99%)</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
// Panel de estadísticas para "Proyectos Completados"
interface CompletedProjectsStatsProps {
projects: Project[];
}
function CompletedProjectsStats({ projects }: CompletedProjectsStatsProps) {
// Filtrar proyectos completados (cerrados o 100% progreso)
const completedProjects = projects.filter((p) => p.status === "2" || p.progress >= 100);
const stats = calculateStats(completedProjects);
if (completedProjects.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<CheckCircle2 className="w-16 h-16 text-gray-300 mb-4" />
<h3 className="text-xl font-semibold text-gray-700">No hay proyectos completados</h3>
<p className="text-gray-500 mt-2">Aun no se ha completado ningun proyecto</p>
</div>
);
}
// Proyectos que terminaron dentro del presupuesto
const withinBudget = completedProjects.filter((p) => p.spent <= p.budget);
const overBudget = completedProjects.filter((p) => p.spent > p.budget);
// Eficiencia promedio (presupuesto restante / presupuesto total)
const avgEfficiency =
completedProjects.length > 0
? Math.round(
completedProjects.reduce((acc, p) => {
if (p.budget === 0) return acc;
return acc + ((p.budget - p.spent) / p.budget) * 100;
}, 0) / completedProjects.length
)
: 0;
return (
<div className="space-y-6">
{/* Cards principales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="Proyectos Completados"
value={stats.total}
subtitle="Finalizados exitosamente"
icon={<CheckCircle2 className="w-4 h-4 text-green-500" />}
highlight
/>
<StatCard
title="Presupuesto Gestionado"
value={`${(stats.totalBudget / 1000).toFixed(0)}k`}
subtitle="En proyectos completados"
icon={<DollarSign className="w-4 h-4 text-green-500" />}
/>
<StatCard
title="Total Gastado"
value={`${(stats.totalSpent / 1000).toFixed(0)}k`}
subtitle={`${stats.budgetConsumed}% del presupuesto`}
icon={<PiggyBank className="w-4 h-4 text-blue-500" />}
/>
<StatCard
title="Eficiencia Promedio"
value={`${Math.max(0, avgEfficiency)}%`}
subtitle="Presupuesto ahorrado"
icon={<Target className="w-4 h-4 text-purple-500" />}
/>
</div>
{/* Cumplimiento de presupuesto */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="border-green-200 bg-green-50">
<CardHeader>
<CardTitle className="text-sm font-medium text-green-700 flex items-center gap-2">
<CheckCircle2 className="w-4 h-4" />
Dentro del Presupuesto
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-green-600">{withinBudget.length}</p>
<p className="text-xs text-green-600 mt-1">
{stats.total > 0 ? Math.round((withinBudget.length / stats.total) * 100) : 0}% de los proyectos completados
</p>
</CardContent>
</Card>
<Card className="border-red-200 bg-red-50">
<CardHeader>
<CardTitle className="text-sm font-medium text-red-700 flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
Excedieron Presupuesto
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-red-600">{overBudget.length}</p>
<p className="text-xs text-red-600 mt-1">
{stats.total > 0 ? Math.round((overBudget.length / stats.total) * 100) : 0}% de los proyectos completados
</p>
</CardContent>
</Card>
</div>
{/* Estadísticas adicionales */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StatCard
title="Clientes Atendidos"
value={stats.uniqueClients}
subtitle="Con proyectos completados"
icon={<Users className="w-4 h-4 text-indigo-500" />}
/>
<StatCard
title="Duracion Promedio"
value={`${stats.avgDuration} dias`}
subtitle="Por proyecto completado"
icon={<Calendar className="w-4 h-4 text-teal-500" />}
/>
<StatCard
title="Presupuesto Promedio"
value={`${(stats.avgBudget / 1000).toFixed(1)}k`}
subtitle="Por proyecto completado"
icon={<Target className="w-4 h-4 text-blue-500" />}
/>
</div>
{/* Distribucion y Budget */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<BudgetStats projects={completedProjects} title="Resumen Final de Presupuesto" />
<Card className="border-gray-200">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
<DollarSign className="w-4 h-4" />
Rango de Presupuestos Completados
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-center">
<p className="text-sm text-gray-500">Minimo</p>
<p className="text-xl font-bold text-gray-900">{stats.minBudget.toLocaleString("es-ES")}</p>
</div>
<div className="flex-1 mx-4 h-px bg-gradient-to-r from-green-300 to-green-500" />
<div className="text-center">
<p className="text-sm text-gray-500">Maximo</p>
<p className="text-xl font-bold text-gray-900">{stats.maxBudget.toLocaleString("es-ES")}</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Timeline */}
<Card className="border-gray-200">
<CardHeader>
<CardTitle className="text-sm font-medium text-gray-600 flex items-center gap-2">
<Calendar className="w-4 h-4" />
Rango de Fechas
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-center">
<p className="text-sm text-gray-500">Primer Proyecto</p>
<p className="text-lg font-bold text-gray-900">{stats.oldestProject || "N/A"}</p>
</div>
<div className="flex-1 mx-4 flex items-center justify-center">
<div className="h-px w-full bg-gradient-to-r from-blue-300 to-purple-500" />
</div>
<div className="text-center">
<p className="text-sm text-gray-500">Ultimo Proyecto</p>
<p className="text-lg font-bold text-gray-900">{stats.newestProject || "N/A"}</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
// Componente de skeleton para carga
function StatisticsSkeleton() {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i} className="border-gray-200">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-16 mb-2" />
<Skeleton className="h-3 w-32" />
</CardContent>
</Card>
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i} className="border-gray-200">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-16 mb-2" />
<Skeleton className="h-3 w-32" />
</CardContent>
</Card>
))}
</div>
</div>
);
}
// Componente principal de la página
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 (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center">
<div className="text-center">
<AlertCircle className="w-16 h-16 text-red-400 mx-auto mb-4" />
<p className="text-red-600 text-lg">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Reintentar
</button>
</div>
</div>
);
}
const activeCount = projects.filter((p) => p.status !== "2" && p.progress < 100).length;
const completedCount = projects.filter((p) => p.status === "2" || p.progress >= 100).length;
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
{/* Header */}
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="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>
<h1 className="text-2xl font-bold text-gray-900">Estadisticas</h1>
<p className="text-sm text-gray-500">
Analisis detallado de {projects.length} proyectos
</p>
</div>
</div>
</div>
</div>
</header>
{/* Main content */}
<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">
<TabsList className="grid w-full grid-cols-3 lg:w-auto lg:inline-grid">
<TabsTrigger value="todos" className="gap-2">
<LayoutGrid className="w-4 h-4" />
<span className="hidden sm:inline">Todos</span>
<Badge variant="secondary" className="ml-1">
{projects.length}
</Badge>
</TabsTrigger>
<TabsTrigger value="activos" className="gap-2">
<TrendingUp className="w-4 h-4" />
<span className="hidden sm:inline">Activos</span>
<Badge variant="secondary" className="ml-1 bg-blue-100 text-blue-700">
{activeCount}
</Badge>
</TabsTrigger>
<TabsTrigger value="completados" className="gap-2">
<CheckCircle2 className="w-4 h-4" />
<span className="hidden sm:inline">Completados</span>
<Badge variant="secondary" className="ml-1 bg-green-100 text-green-700">
{completedCount}
</Badge>
</TabsTrigger>
</TabsList>
<TabsContent value="todos">
<AllProjectsStats projects={projects} />
</TabsContent>
<TabsContent value="activos">
<ActiveProjectsStats projects={projects} />
</TabsContent>
<TabsContent value="completados">
<CompletedProjectsStats projects={projects} />
</TabsContent>
</Tabs>
)}
</main>
</div>
);
}