"use client"; import { useState, useEffect, useMemo, useRef, useCallback } from "react"; import { useRouter } from "next/navigation"; import { GanttChart as GanttIcon, AlertCircle, ChevronLeft, ChevronRight, Calendar, History, Clock, Filter, } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; 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 dark:bg-blue-900/40", hex: "#3b82f6" }, { bg: "bg-violet-500", light: "bg-violet-100 dark:bg-violet-900/40", hex: "#8b5cf6" }, { bg: "bg-cyan-500", light: "bg-cyan-100 dark:bg-cyan-900/40", hex: "#06b6d4" }, { bg: "bg-emerald-500", light: "bg-emerald-100 dark:bg-emerald-900/40", hex: "#10b981" }, { bg: "bg-amber-500", light: "bg-amber-100 dark:bg-amber-900/40", hex: "#f59e0b" }, { bg: "bg-rose-500", light: "bg-rose-100 dark:bg-rose-900/40", hex: "#f43f5e" }, { bg: "bg-pink-500", light: "bg-pink-100 dark:bg-pink-900/40", hex: "#ec4899" }, { bg: "bg-indigo-500", light: "bg-indigo-100 dark:bg-indigo-900/40", hex: "#6366f1" }, { bg: "bg-teal-500", light: "bg-teal-100 dark:bg-teal-900/40", hex: "#14b8a6" }, { bg: "bg-orange-500", light: "bg-orange-100 dark:bg-orange-900/40", hex: "#f97316" }, ]; const STATUS_LABELS: Record = { "0": "Borrador", "1": "Abierto", "2": "Cerrado", }; const MONTH_WIDTH_PX = 120; // Ancho fijo por mes en modo scroll type ViewMode = "month" | "quarter" | "year"; type TimeRange = "future" | "past" | "all"; function getProjectColor(index: number) { return PROJECT_COLORS[index % PROJECT_COLORS.length]; } function formatDate(dateStr: string): string { const date = new Date(dateStr); return date.toLocaleDateString("es-ES", { day: "2-digit", month: "short", year: "numeric" }); } function getDaysBetween(start: Date, end: Date): number { return Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); } 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; } export default function GanttPage() { const router = useRouter(); 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 [timeRange, setTimeRange] = useState("all"); const [viewOffset, setViewOffset] = useState(0); // Refs para sincronizar scroll vertical entre nombres y timeline const namesRef = useRef(null); const timelineRef = useRef(null); const isSyncing = useRef(false); const syncScroll = useCallback((source: "names" | "timeline") => { if (isSyncing.current) return; isSyncing.current = true; const from = source === "names" ? namesRef.current : timelineRef.current; const to = source === "names" ? timelineRef.current : namesRef.current; if (from && to) { to.scrollTop = from.scrollTop; } requestAnimationFrame(() => { isSyncing.current = false; }); }, []); 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 por estado const statusFiltered = useMemo(() => { if (statusFilter === "all") return projects; return projects.filter(p => p.status === statusFilter); }, [projects, statusFilter]); // Filtrar por rango temporal const filteredProjects = useMemo(() => { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); if (timeRange === "future") { return statusFiltered.filter(p => new Date(p.endDate) >= today); } else if (timeRange === "past") { return statusFiltered.filter(p => new Date(p.endDate) < today); } return statusFiltered; // "all" }, [statusFiltered, timeRange]); // Calcular meses del timeline completo const allMonths = useMemo(() => { if (filteredProjects.length === 0) { const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth() - 3, 1); const end = new Date(now.getFullYear(), now.getMonth() + 3, 0); return getMonthsBetween(start, end); } const allStartDates = filteredProjects.map(p => new Date(p.startDate)); const allEndDates = filteredProjects.map(p => new Date(p.endDate)); const minStart = new Date(Math.min(...allStartDates.map(d => d.getTime()))); const maxEnd = new Date(Math.max(...allEndDates.map(d => d.getTime()))); const now = new Date(); const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1); const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0); if (timeRange === "future") { return getMonthsBetween(currentMonthStart, new Date(maxEnd.getFullYear(), maxEnd.getMonth() + 1, 0)); } else if (timeRange === "past") { return getMonthsBetween(new Date(minStart.getFullYear(), minStart.getMonth(), 1), currentMonthEnd); } // "all": desde el primer proyecto hasta el último return getMonthsBetween( new Date(minStart.getFullYear(), minStart.getMonth(), 1), new Date(maxEnd.getFullYear(), maxEnd.getMonth() + 1, 0) ); }, [filteredProjects, timeRange]); // Meses visibles: en modo "all" se muestran todos (scroll), en otros modos se pagina const visibleMonths = useMemo(() => { if (timeRange === "all") return allMonths; const monthsToShow = viewMode === "month" ? 3 : viewMode === "quarter" ? 6 : 12; if (timeRange === "past") { const maxOff = Math.max(0, allMonths.length - monthsToShow); const startIdx = Math.max(0, maxOff - viewOffset); return allMonths.slice(startIdx, startIdx + monthsToShow); } const startIdx = Math.max(0, Math.min(viewOffset, allMonths.length - monthsToShow)); return allMonths.slice(startIdx, startIdx + monthsToShow); }, [allMonths, viewMode, viewOffset, timeRange]); // Rango visible en días 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 (solo para modos paginados, no "all") const monthsToShow = viewMode === "month" ? 3 : viewMode === "quarter" ? 6 : 12; const maxOffset = Math.max(0, allMonths.length - monthsToShow); const canGoBack = timeRange !== "all" && viewOffset > 0; const canGoForward = timeRange !== "all" && viewOffset < maxOffset; 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; setViewOffset(Math.min(maxOffset, viewOffset + step)); }; const goToToday = () => setViewOffset(0); // ¿El timeline usa ancho fijo (scroll horizontal) o flexible (fill)? const useFixedWidth = timeRange === "all"; const timelineInnerWidth = useFixedWidth ? visibleMonths.length * MONTH_WIDTH_PX : undefined; if (error) { return (

{error}

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

Diagrama de Gantt

Timeline de {filteredProjects.length} proyecto{filteredProjects.length !== 1 ? "s" : ""}

{/* Controles */}
{/* Filtro de estado */} {/* Selector de rango temporal */}
{/* Selector de vista y navegación - solo en modos paginados */} {timeRange !== "all" && ( <>
)}
{/* Content */}
{loading ? ( ) : filteredProjects.length === 0 ? (

No hay proyectos

No se encontraron proyectos con los filtros seleccionados

) : (
{/* Columna de nombres - fija, scroll vertical */}
{/* Header nombres */}
Proyecto
{/* Lista nombres - scroll vertical */}
syncScroll("names")} > {filteredProjects.map((project, index) => { const color = getProjectColor(index); return (
router.push(`/proyectos/${project.id}`)} >

{project.name}

{STATUS_LABELS[project.status]} · {project.progress}%

); })}
{/* Timeline - scroll horizontal (en modo "all") + scroll vertical sincronizado */}
syncScroll("timeline")} >
{/* Header meses */}
{visibleMonths.map((month) => { const isCurrentMonth = new Date().getMonth() === month.month && new Date().getFullYear() === month.year; return (
{month.label}
); })}
{/* Barras del Gantt */} {filteredProjects.map((project, index) => { const color = getProjectColor(index); const projectStart = new Date(project.startDate); const projectEnd = new Date(project.endDate); 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(0.5, ((endOffset - startOffset) / totalDays) * 100); const isVisible = projectEnd >= visibleStart && projectStart <= visibleEnd; const isOverdue = projectEnd < new Date() && project.progress < 100 && project.status === "1"; return (
{/* Grid de meses */}
{visibleMonths.map((month) => { const isCurrentMonth = new Date().getMonth() === month.month && new Date().getFullYear() === month.year; return (
); })}
{/* Barra del proyecto */} {isVisible && (
router.push(`/proyectos/${project.id}`)} >
{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

)}
)}
); })}
)}
); } function GanttSkeleton() { return (
{Array.from({ length: 8 }).map((_, i) => (
))}
{Array.from({ length: 6 }).map((_, i) => (
))}
{Array.from({ length: 8 }).map((_, i) => (
))}
); }