Ahora si que si, finalizado
This commit is contained in:
parent
2f42b7bbad
commit
f4c885031b
|
|
@ -12,6 +12,8 @@ import {
|
||||||
Filter,
|
Filter,
|
||||||
GanttChartSquare,
|
GanttChartSquare,
|
||||||
Layers,
|
Layers,
|
||||||
|
PanelLeftClose,
|
||||||
|
PanelLeftOpen,
|
||||||
Search,
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
|
@ -292,11 +294,12 @@ export default function GanttPage() {
|
||||||
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<Button
|
<Button
|
||||||
variant={showSelectionPanel ? "default" : "outline"}
|
variant="outline"
|
||||||
onClick={() => setShowSelectionPanel((current) => !current)}
|
onClick={() => setShowSelectionPanel((current) => !current)}
|
||||||
className="gap-2"
|
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"}
|
{showSelectionPanel ? "Ocultar selección" : "Mostrar selección"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
@ -401,6 +404,7 @@ export default function GanttPage() {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<Card className="border shadow-sm overflow-hidden">
|
<Card className="border shadow-sm overflow-hidden">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{plannedRows.length === 0 ? (
|
{plannedRows.length === 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
"use client";
|
"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 { FolderKanban, RefreshCw, LayoutDashboard, ListTodo, FileText, GanttChartSquare } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -86,6 +87,7 @@ export function ProjectDetailView({ project: initialProject }: ProjectDetailView
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
const [isLoadingTasks, setIsLoadingTasks] = useState(true);
|
const [isLoadingTasks, setIsLoadingTasks] = useState(true);
|
||||||
const [tasksError, setTasksError] = useState<string | null>(null);
|
const [tasksError, setTasksError] = useState<string | null>(null);
|
||||||
|
const hasShownReminders = useRef(false);
|
||||||
|
|
||||||
// Cargar tareas
|
// Cargar tareas
|
||||||
const fetchTasks = useCallback(async () => {
|
const fetchTasks = useCallback(async () => {
|
||||||
|
|
@ -106,6 +108,48 @@ export function ProjectDetailView({ project: initialProject }: ProjectDetailView
|
||||||
fetchTasks();
|
fetchTasks();
|
||||||
}, [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
|
// Handler para cuando se actualiza el proyecto
|
||||||
const handleProjectUpdated = (updatedProject: Project) => {
|
const handleProjectUpdated = (updatedProject: Project) => {
|
||||||
setProject(updatedProject);
|
setProject(updatedProject);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ZoomIn,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
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
|
(viewportWidth > 0 ? viewportWidth : LEFT_COL_WIDTH + 640) - LEFT_COL_WIDTH
|
||||||
);
|
);
|
||||||
|
|
||||||
const timelineWidth = zoom === "fit"
|
const maxTimelineWidth = Math.max(320, fitTimelineWidth);
|
||||||
? fitTimelineWidth
|
const baseTickWidth = zoom === "fit" ? 72 : zoom === "week" ? 84 : 36;
|
||||||
: zoom === "week"
|
const tickWidth = Math.min(baseTickWidth, maxTimelineWidth / Math.max(1, ticks.length));
|
||||||
? ticks.length * 96
|
const timelineWidth = Math.max(320, ticks.length * tickWidth);
|
||||||
: ticks.length * 38;
|
|
||||||
|
|
||||||
const today = startOfDay(new Date());
|
const today = startOfDay(new Date());
|
||||||
const showToday = today >= visibleStart && today <= visibleEnd;
|
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-2">
|
||||||
<div className="flex items-center gap-1 border rounded-md p-1">
|
<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 === "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 === "week" ? "default" : "ghost"} onClick={() => setZoomAndReset("week")}>Semanas</Button>
|
||||||
<Button size="sm" variant={zoom === "day" ? "default" : "ghost"} onClick={() => setZoomAndReset("day")}>Días</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" && (
|
{zoom !== "fit" && (
|
||||||
<div className="flex items-center gap-1">
|
<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" />
|
<ChevronLeft className="w-4 h-4" />
|
||||||
</Button>
|
</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" />
|
<ChevronRight className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -262,12 +259,12 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) {
|
||||||
ref={viewportRef}
|
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]"
|
className="w-full max-w-full max-h-[620px] overflow-x-auto overflow-y-auto overscroll-x-contain [scrollbar-gutter:stable_both-edges]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
minWidth: `${LEFT_COL_WIDTH + timelineWidth}px`,
|
minWidth: zoom === "fit" ? "100%" : `${LEFT_COL_WIDTH + timelineWidth}px`,
|
||||||
width: zoom === "fit" ? "100%" : "max-content",
|
width: zoom === "fit" ? "100%" : "max-content",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="border-b sticky top-0 z-20 bg-card/95 backdrop-blur"
|
className="border-b sticky top-0 z-20 bg-card/95 backdrop-blur"
|
||||||
style={{ display: "grid", gridTemplateColumns: `${LEFT_COL_WIDTH}px ${timelineWidth}px` }}
|
style={{ display: "grid", gridTemplateColumns: `${LEFT_COL_WIDTH}px ${timelineWidth}px` }}
|
||||||
|
|
@ -277,12 +274,12 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) {
|
||||||
</div>
|
</div>
|
||||||
<div className="h-12 flex">
|
<div className="h-12 flex">
|
||||||
{ticks.map((tick) => (
|
{ticks.map((tick) => (
|
||||||
<div
|
<div
|
||||||
key={tick.key}
|
key={tick.key}
|
||||||
className="border-r flex items-center justify-center"
|
className="border-r flex items-center justify-center"
|
||||||
style={{ width: `${timelineWidth / ticks.length}px` }}
|
style={{ width: `${tickWidth}px` }}
|
||||||
title={tick.label}
|
title={tick.label}
|
||||||
>
|
>
|
||||||
<span className="text-[11px] font-medium text-muted-foreground uppercase">{tick.label}</span>
|
<span className="text-[11px] font-medium text-muted-foreground uppercase">{tick.label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -322,7 +319,7 @@ export function ProjectTaskGantt({ tasks }: ProjectTaskGanttProps) {
|
||||||
<div className="relative h-[56px]">
|
<div className="relative h-[56px]">
|
||||||
<div className="absolute inset-0 flex pointer-events-none">
|
<div className="absolute inset-0 flex pointer-events-none">
|
||||||
{ticks.map((tick) => (
|
{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>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
|
|
@ -8799,6 +8800,16 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue