"use client"; import { useState, useEffect, useMemo } from "react"; import { GanttChart as GanttIcon, AlertCircle, ChevronLeft, ChevronRight, Calendar, ZoomIn, ZoomOut, Filter, } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { getProjects } from "@/lib/projectsService"; import { Project, ProjectStatus } from "@/types/project"; // Paleta de colores para proyectos const PROJECT_COLORS = [ { bg: "bg-blue-500", light: "bg-blue-100", text: "text-blue-700", hex: "#3b82f6" }, { bg: "bg-violet-500", light: "bg-violet-100", text: "text-violet-700", hex: "#8b5cf6" }, { bg: "bg-cyan-500", light: "bg-cyan-100", text: "text-cyan-700", hex: "#06b6d4" }, { bg: "bg-emerald-500", light: "bg-emerald-100", text: "text-emerald-700", hex: "#10b981" }, { bg: "bg-amber-500", light: "bg-amber-100", text: "text-amber-700", hex: "#f59e0b" }, { bg: "bg-rose-500", light: "bg-rose-100", text: "text-rose-700", hex: "#f43f5e" }, { bg: "bg-pink-500", light: "bg-pink-100", text: "text-pink-700", hex: "#ec4899" }, { bg: "bg-indigo-500", light: "bg-indigo-100", text: "text-indigo-700", hex: "#6366f1" }, { bg: "bg-teal-500", light: "bg-teal-100", text: "text-teal-700", hex: "#14b8a6" }, { bg: "bg-orange-500", light: "bg-orange-100", text: "text-orange-700", hex: "#f97316" }, ]; const STATUS_LABELS: Record = { "0": "Borrador", "1": "Abierto", "2": "Cerrado", }; type ViewMode = "month" | "quarter" | "year"; function getProjectColor(index: number) { return PROJECT_COLORS[index % PROJECT_COLORS.length]; } // Formatear fecha function formatDate(dateStr: string): string { const date = new Date(dateStr); return date.toLocaleDateString("es-ES", { day: "2-digit", month: "short", year: "numeric" }); } // Obtener días entre dos fechas function getDaysBetween(start: Date, end: Date): number { return Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); } // Generar array de meses entre dos fechas function getMonthsBetween(start: Date, end: Date): { month: number; year: number; label: string }[] { const months: { month: number; year: number; label: string }[] = []; const current = new Date(start.getFullYear(), start.getMonth(), 1); const endMonth = new Date(end.getFullYear(), end.getMonth(), 1); while (current <= endMonth) { months.push({ month: current.getMonth(), year: current.getFullYear(), label: current.toLocaleDateString("es-ES", { month: "short", year: "numeric" }), }); current.setMonth(current.getMonth() + 1); } return months; } // Obtener días en un mes function getDaysInMonth(month: number, year: number): number { return new Date(year, month + 1, 0).getDate(); } export default function GanttPage() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [viewMode, setViewMode] = useState("month"); const [statusFilter, setStatusFilter] = useState("all"); const [viewOffset, setViewOffset] = useState(0); 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(); }, []); // Filtrar proyectos const filteredProjects = useMemo(() => { if (statusFilter === "all") return projects; return projects.filter(p => p.status === statusFilter); }, [projects, statusFilter]); // Calcular rango de fechas del timeline const { timelineStart, timelineEnd, months } = useMemo(() => { if (filteredProjects.length === 0) { const now = new Date(); return { timelineStart: new Date(now.getFullYear(), now.getMonth(), 1), timelineEnd: new Date(now.getFullYear(), now.getMonth() + 6, 0), months: [], }; } const allDates = filteredProjects.flatMap(p => [new Date(p.startDate), new Date(p.endDate)]); const minDate = new Date(Math.min(...allDates.map(d => d.getTime()))); const maxDate = new Date(Math.max(...allDates.map(d => d.getTime()))); // Añadir margen de un mes antes y después const start = new Date(minDate.getFullYear(), minDate.getMonth() - 1, 1); const end = new Date(maxDate.getFullYear(), maxDate.getMonth() + 2, 0); return { timelineStart: start, timelineEnd: end, months: getMonthsBetween(start, end), }; }, [filteredProjects]); // Calcular meses visibles según el modo de vista const visibleMonths = useMemo(() => { const monthsToShow = viewMode === "month" ? 3 : viewMode === "quarter" ? 6 : 12; const startIdx = Math.max(0, Math.min(viewOffset, months.length - monthsToShow)); return months.slice(startIdx, startIdx + monthsToShow); }, [months, viewMode, viewOffset]); // Calcular el ancho total en días para los meses visibles const { totalDays, visibleStart, visibleEnd } = useMemo(() => { if (visibleMonths.length === 0) { return { totalDays: 30, visibleStart: new Date(), visibleEnd: new Date() }; } const first = visibleMonths[0]; const last = visibleMonths[visibleMonths.length - 1]; const start = new Date(first.year, first.month, 1); const end = new Date(last.year, last.month + 1, 0); return { totalDays: getDaysBetween(start, end), visibleStart: start, visibleEnd: end, }; }, [visibleMonths]); // Navegación const canGoBack = viewOffset > 0; const canGoForward = viewOffset + (viewMode === "month" ? 3 : viewMode === "quarter" ? 6 : 12) < months.length; const goBack = () => { const step = viewMode === "month" ? 1 : viewMode === "quarter" ? 3 : 6; setViewOffset(Math.max(0, viewOffset - step)); }; const goForward = () => { const step = viewMode === "month" ? 1 : viewMode === "quarter" ? 3 : 6; const maxOffset = Math.max(0, months.length - (viewMode === "month" ? 3 : viewMode === "quarter" ? 6 : 12)); setViewOffset(Math.min(maxOffset, viewOffset + step)); }; const goToToday = () => { const now = new Date(); const todayIndex = months.findIndex(m => m.month === now.getMonth() && m.year === now.getFullYear()); if (todayIndex >= 0) { setViewOffset(Math.max(0, todayIndex - 1)); } }; if (error) { return (

{error}

); } return (
{/* Header */}

Diagrama de Gantt

Timeline de {filteredProjects.length} proyectos

{/* Controles */}
{/* Filtro de estado */} {/* Selector de vista */} {/* Navegación */}
{/* Content */}
{loading ? ( ) : filteredProjects.length === 0 ? (

No hay proyectos

No se encontraron proyectos con los filtros seleccionados

) : (
{/* Columna de nombres de proyectos */}
{/* Header */}
Proyecto
{/* Lista de proyectos */} {filteredProjects.map((project, index) => { const color = getProjectColor(index); return (

{project.name}

{project.progress}%

); })}
{/* Timeline */}
{/* Header con meses */}
{visibleMonths.map((month, idx) => { const daysInMonth = getDaysInMonth(month.month, month.year); const widthPercent = (daysInMonth / totalDays) * 100; const isCurrentMonth = new Date().getMonth() === month.month && new Date().getFullYear() === month.year; return (
{month.label} {daysInMonth} días
); })}
{/* Barras del Gantt */} {filteredProjects.map((project, index) => { const color = getProjectColor(index); const projectStart = new Date(project.startDate); const projectEnd = new Date(project.endDate); // Calcular posición y ancho de la barra const startOffset = Math.max(0, getDaysBetween(visibleStart, projectStart)); const endOffset = Math.min(totalDays, getDaysBetween(visibleStart, projectEnd)); const leftPercent = (startOffset / totalDays) * 100; const widthPercent = Math.max(1, ((endOffset - startOffset) / totalDays) * 100); // Verificar si el proyecto está visible en el rango actual const isVisible = projectEnd >= visibleStart && projectStart <= visibleEnd; const isOverdue = projectEnd < new Date() && project.progress < 100 && project.status === "1"; return (
{/* Grid de meses */}
{visibleMonths.map((month, idx) => { const daysInMonth = getDaysInMonth(month.month, month.year); const monthWidthPercent = (daysInMonth / totalDays) * 100; const isCurrentMonth = new Date().getMonth() === month.month && new Date().getFullYear() === month.year; return (
); })}
{/* Barra del proyecto */} {isVisible && (
{/* Fondo de la barra */}
{/* Progreso */}
{/* Contenido de la barra */}
{project.progress >= 30 ? `${project.progress}%` : ""}

{project.name}

Estado: {STATUS_LABELS[project.status]}

Progreso: {project.progress}%

Inicio: {formatDate(project.startDate)}

Fin: {formatDate(project.endDate)}

Cliente: {project.client}

{isOverdue && (

Proyecto retrasado

)}
)}
); })}
)} {/* Leyenda */} {!loading && filteredProjects.length > 0 && (
Progreso del proyecto
Mes actual
Proyecto retrasado
)}
); } function GanttSkeleton() { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
{Array.from({ length: 3 }).map((_, i) => (
))}
{Array.from({ length: 5 }).map((_, i) => (
))}
); }