diff --git a/components/gantt/gantt-page.tsx b/components/gantt/gantt-page.tsx index 4f4190c..bf3d443 100644 --- a/components/gantt/gantt-page.tsx +++ b/components/gantt/gantt-page.tsx @@ -12,6 +12,8 @@ import { Filter, GanttChartSquare, Layers, + PanelLeftClose, + PanelLeftOpen, Search, } from "lucide-react"; @@ -292,11 +294,12 @@ export default function GanttPage() {
@@ -401,6 +404,7 @@ export default function GanttPage() { )} + {plannedRows.length === 0 ? ( diff --git a/components/project-detail/project-detail-view.tsx b/components/project-detail/project-detail-view.tsx index b399bbb..5823936 100644 --- a/components/project-detail/project-detail-view.tsx +++ b/components/project-detail/project-detail-view.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { FolderKanban, RefreshCw, LayoutDashboard, ListTodo, FileText, GanttChartSquare } from "lucide-react"; +import { toast } from "sonner"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; @@ -86,6 +87,7 @@ export function ProjectDetailView({ project: initialProject }: ProjectDetailView const [tasks, setTasks] = useState([]); const [isLoadingTasks, setIsLoadingTasks] = useState(true); const [tasksError, setTasksError] = useState(null); + const hasShownReminders = useRef(false); // Cargar tareas const fetchTasks = useCallback(async () => { @@ -106,6 +108,48 @@ export function ProjectDetailView({ project: initialProject }: ProjectDetailView fetchTasks(); }, [fetchTasks]); + useEffect(() => { + if (isLoadingTasks || tasksError || hasShownReminders.current) return; + + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const upcomingThreshold = new Date(today); + upcomingThreshold.setDate(upcomingThreshold.getDate() + 7); + + const overdueTasks = tasks.filter((task) => { + const endDate = task.endDate || task.plannedEndDate; + if (!endDate) return false; + return new Date(endDate) < today && task.status !== "2"; + }); + + const upcomingTasks = tasks.filter((task) => { + const endDate = task.endDate || task.plannedEndDate; + if (!endDate) return false; + const due = new Date(endDate); + return due >= today && due <= upcomingThreshold && task.status !== "2"; + }); + + const undatedTasks = tasks.filter((task) => { + return !task.startDate && !task.endDate && !task.plannedStartDate && !task.plannedEndDate; + }); + + if (overdueTasks.length === 0 && upcomingTasks.length === 0 && undatedTasks.length === 0) { + hasShownReminders.current = true; + return; + } + + const lines: string[] = []; + if (overdueTasks.length > 0) lines.push(`• ${overdueTasks.length} tareas retrasadas`); + if (upcomingTasks.length > 0) lines.push(`• ${upcomingTasks.length} próximas a vencer (≤ 7 días)`); + if (undatedTasks.length > 0) lines.push(`• ${undatedTasks.length} sin fecha asignada`); + + toast("Recordatorios del proyecto", { + description: lines.join("\n"), + }); + + hasShownReminders.current = true; + }, [isLoadingTasks, tasksError, tasks]); + // Handler para cuando se actualiza el proyecto const handleProjectUpdated = (updatedProject: Project) => { setProject(updatedProject); diff --git a/components/project-detail/project-task-gantt.tsx b/components/project-detail/project-task-gantt.tsx index d6b535a..96b1944 100644 --- a/components/project-detail/project-task-gantt.tsx +++ b/components/project-detail/project-task-gantt.tsx @@ -9,7 +9,6 @@ import { CheckCircle2, ChevronLeft, ChevronRight, - ZoomIn, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; @@ -194,11 +193,10 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) { (viewportWidth > 0 ? viewportWidth : LEFT_COL_WIDTH + 640) - LEFT_COL_WIDTH ); - const timelineWidth = zoom === "fit" - ? fitTimelineWidth - : zoom === "week" - ? ticks.length * 96 - : ticks.length * 38; + const maxTimelineWidth = Math.max(320, fitTimelineWidth); + const baseTickWidth = zoom === "fit" ? 72 : zoom === "week" ? 84 : 36; + const tickWidth = Math.min(baseTickWidth, maxTimelineWidth / Math.max(1, ticks.length)); + const timelineWidth = Math.max(320, ticks.length * tickWidth); const today = startOfDay(new Date()); const showToday = today >= visibleStart && today <= visibleEnd; @@ -236,7 +234,6 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) {
- @@ -244,10 +241,10 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) { {zoom !== "fit" && (
- -
@@ -262,12 +259,12 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) { ref={viewportRef} className="w-full max-w-full max-h-[620px] overflow-x-auto overflow-y-auto overscroll-x-contain [scrollbar-gutter:stable_both-edges]" > -
+
{ticks.map((tick) => ( -
+
{tick.label}
))} @@ -322,7 +319,7 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) {
{ticks.map((tick) => ( -
+
))}
diff --git a/package-lock.json b/package-lock.json index 19f1e42..f2128c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "react": "19.2.0", "react-dom": "19.2.0", "recharts": "^3.7.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7" }, @@ -8799,6 +8800,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index 4d311d2..13d306a 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "react": "19.2.0", "react-dom": "19.2.0", "recharts": "^3.7.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7" },