"use client"; import { useState, useEffect, useMemo } from "react"; import { GanttChart as GanttIcon, AlertCircle, ChevronLeft, ChevronRight, Calendar, History, Clock, 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"; type TimeRange = "future" | "past" | "all"; 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 [timeRange, setTimeRange] = useState("future"); 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(() => { let filtered = projects; // Filtrar por estado if (statusFilter !== "all") { filtered = filtered.filter(p => p.status === statusFilter); } // Filtrar por rango temporal const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); if (timeRange === "future") { // Proyectos que terminan hoy o en el futuro filtered = filtered.filter(p => new Date(p.endDate) >= today); } else if (timeRange === "past") { // Proyectos que ya terminaron filtered = filtered.filter(p => new Date(p.endDate) < today); } // "all" no filtra return filtered; }, [projects, statusFilter, timeRange]); // Calcular rango de fechas del timeline basado en los proyectos // - Futuro: desde el mes actual hasta el fin del proyecto más tardío // - Pasado: desde el inicio del proyecto más antiguo hasta el mes actual // - Todo: desde el inicio del proyecto más antiguo hasta el fin del más tardío const { timelineStart, timelineEnd, months } = useMemo(() => { const now = new Date(); const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1); const currentMonthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0); // Si no hay proyectos, mostrar 12 meses desde el actual if (filteredProjects.length === 0) { const start = new Date(now.getFullYear(), now.getMonth() - 6, 1); const end = new Date(now.getFullYear(), now.getMonth() + 6, 0); return { timelineStart: start, timelineEnd: end, months: getMonthsBetween(start, end), }; } // Calcular fechas extremas de los proyectos const allStartDates = filteredProjects.map(p => new Date(p.startDate)); const allEndDates = filteredProjects.map(p => new Date(p.endDate)); const minProjectStart = new Date(Math.min(...allStartDates.map(d => d.getTime()))); const maxProjectEnd = new Date(Math.max(...allEndDates.map(d => d.getTime()))); if (timeRange === "future") { // Desde el mes actual hasta el fin del proyecto más tardío const start = currentMonthStart; const end = new Date(maxProjectEnd.getFullYear(), maxProjectEnd.getMonth() + 1, 0); return { timelineStart: start, timelineEnd: end, months: getMonthsBetween(start, end), }; } else if (timeRange === "past") { // Desde el inicio del proyecto más antiguo hasta el mes actual const start = new Date(minProjectStart.getFullYear(), minProjectStart.getMonth(), 1); const end = currentMonthEnd; return { timelineStart: start, timelineEnd: end, months: getMonthsBetween(start, end), }; } else { // "all" - Desde el inicio del proyecto más antiguo hasta el fin del más tardío const start = new Date(minProjectStart.getFullYear(), minProjectStart.getMonth(), 1); const end = new Date(maxProjectEnd.getFullYear(), maxProjectEnd.getMonth() + 1, 0); return { timelineStart: start, timelineEnd: end, months: getMonthsBetween(start, end), }; } }, [filteredProjects, timeRange]); // Calcular meses visibles según el modo de vista // En modo "todo", mostrar TODOS los meses (scroll horizontal en el timeline) // En modo "pasado", empezamos desde los meses más recientes const visibleMonths = useMemo(() => { // En modo "todo", mostrar todos los meses if (timeRange === "all") { return months; } const monthsToShow = viewMode === "month" ? 3 : viewMode === "quarter" ? 6 : 12; if (timeRange === "past") { // En pasado, viewOffset 0 = meses más recientes (final del array) const maxOffset = Math.max(0, months.length - monthsToShow); const startIdx = Math.max(0, maxOffset - viewOffset); return months.slice(startIdx, startIdx + monthsToShow); } else { // En futuro, viewOffset 0 = primeros meses const startIdx = Math.max(0, Math.min(viewOffset, months.length - monthsToShow)); return months.slice(startIdx, startIdx + monthsToShow); } }, [months, viewMode, viewOffset, timeRange]); // 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 monthsToShow = viewMode === "month" ? 3 : viewMode === "quarter" ? 6 : 12; const maxOffset = Math.max(0, months.length - monthsToShow); const canGoBack = viewOffset > 0; const canGoForward = 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); // Volver al mes actual }; if (error) { return (

{error}

); } return (
{/* Header - siempre visible */}

Diagrama de Gantt

Timeline de {filteredProjects.length} proyectos

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

No hay proyectos

No se encontraron proyectos con los filtros seleccionados

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

{project.name}

{project.progress}%

); })}
{/* Timeline - scroll horizontal en modo "todo" */}
{/* Header con 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); // Calcular posición y ancho de la barra basado en días 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) => { 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

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