Ahora si que si, finalizado

This commit is contained in:
marcos 2026-04-30 18:58:30 +02:00
parent 2f42b7bbad
commit f4c885031b
5 changed files with 82 additions and 25 deletions

View File

@ -12,6 +12,8 @@ import {
Filter,
GanttChartSquare,
Layers,
PanelLeftClose,
PanelLeftOpen,
Search,
} from "lucide-react";
@ -292,11 +294,12 @@ export default function GanttPage() {
<div className="flex items-center gap-2 flex-wrap">
<Button
variant={showSelectionPanel ? "default" : "outline"}
variant="outline"
onClick={() => setShowSelectionPanel((current) => !current)}
className="gap-2"
aria-label={showSelectionPanel ? "Ocultar selección de proyectos" : "Mostrar selección de proyectos"}
>
<Layers className="w-4 h-4" />
{showSelectionPanel ? <PanelLeftClose className="w-4 h-4" /> : <PanelLeftOpen className="w-4 h-4" />}
{showSelectionPanel ? "Ocultar selección" : "Mostrar selección"}
</Button>
@ -401,6 +404,7 @@ export default function GanttPage() {
</Card>
)}
<Card className="border shadow-sm overflow-hidden">
<CardContent className="p-0">
{plannedRows.length === 0 ? (

View File

@ -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<Task[]>([]);
const [isLoadingTasks, setIsLoadingTasks] = useState(true);
const [tasksError, setTasksError] = useState<string | null>(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);

View File

@ -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) {
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 border rounded-md p-1">
<ZoomIn className="w-4 h-4 text-muted-foreground ml-1" />
<Button size="sm" variant={zoom === "fit" ? "default" : "ghost"} onClick={() => setZoomAndReset("fit")}>Ajustar</Button>
<Button size="sm" variant={zoom === "week" ? "default" : "ghost"} onClick={() => setZoomAndReset("week")}>Semanas</Button>
<Button size="sm" variant={zoom === "day" ? "default" : "ghost"} onClick={() => setZoomAndReset("day")}>Días</Button>
@ -244,10 +241,10 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) {
{zoom !== "fit" && (
<div className="flex items-center gap-1">
<Button size="icon" variant="outline" onClick={goBack} disabled={!canGoBack}>
<Button size="icon" variant="outline" onClick={goBack} disabled={!canGoBack} aria-label="Semana anterior">
<ChevronLeft className="w-4 h-4" />
</Button>
<Button size="icon" variant="outline" onClick={goForward} disabled={!canGoForward}>
<Button size="icon" variant="outline" onClick={goForward} disabled={!canGoForward} aria-label="Semana siguiente">
<ChevronRight className="w-4 h-4" />
</Button>
</div>
@ -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]"
>
<div
style={{
minWidth: `${LEFT_COL_WIDTH + timelineWidth}px`,
width: zoom === "fit" ? "100%" : "max-content",
}}
>
<div
style={{
minWidth: zoom === "fit" ? "100%" : `${LEFT_COL_WIDTH + timelineWidth}px`,
width: zoom === "fit" ? "100%" : "max-content",
}}
>
<div
className="border-b sticky top-0 z-20 bg-card/95 backdrop-blur"
style={{ display: "grid", gridTemplateColumns: `${LEFT_COL_WIDTH}px ${timelineWidth}px` }}
@ -277,12 +274,12 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) {
</div>
<div className="h-12 flex">
{ticks.map((tick) => (
<div
key={tick.key}
className="border-r flex items-center justify-center"
style={{ width: `${timelineWidth / ticks.length}px` }}
title={tick.label}
>
<div
key={tick.key}
className="border-r flex items-center justify-center"
style={{ width: `${tickWidth}px` }}
title={tick.label}
>
<span className="text-[11px] font-medium text-muted-foreground uppercase">{tick.label}</span>
</div>
))}
@ -322,7 +319,7 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) {
<div className="relative h-[56px]">
<div className="absolute inset-0 flex pointer-events-none">
{ticks.map((tick) => (
<div key={`grid-${item.task.id}-${tick.key}`} className="border-r" style={{ width: `${timelineWidth / ticks.length}px` }} />
<div key={`grid-${item.task.id}-${tick.key}`} className="border-r" style={{ width: `${tickWidth}px` }} />
))}
</div>

11
package-lock.json generated
View File

@ -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",

View File

@ -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"
},