Ahora si que si, finalizado
This commit is contained in:
parent
2f42b7bbad
commit
f4c885031b
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue