Ir al contenido

Vista Calendario

Visualiza tareas en calendario mensual con indicadores y creación rápida.

  • Calendario mensual: Vista completa del mes con todas las semanas
  • Indicadores de tareas: Puntos de colores según prioridad más alta
  • Máximo 3 puntos: Si hay más tareas, muestra solo 3 puntos
  • Panel lateral: Detalles de tareas del día seleccionado
  • Navegación: Botones anterior/siguiente mes, botón “Hoy”
  • Crear tarea: Click en fecha para crear con fecha preseleccionada
  • Responsive: Layout adaptativo móvil/desktop

Puntos por Prioridad:

  • 🔴 Rojo: Urgente
  • 🟠 Naranja: Alta
  • 🟡 Amarillo: Media
  • 🔵 Azul: Baja

Se muestra el color de la prioridad más alta del día.

Muestra para el día seleccionado:

  • Título de la tarea
  • Badge de estado (Pendiente, En Progreso, etc.)
  • Badge de prioridad
  • Hora de vencimiento (si está configurada)
  • Click para ver detalle completo
  • Botones ◀ ▶: Cambiar mes
  • Botón “Hoy”: Volver al día actual
  • Click en día: Seleccionar fecha y ver tareas
Detalles Técnicos
  • Archivo: TaskCalendar.tsx
  • Ruta: src/features/projects/sub-modules/tasks/components/TaskCalendar.tsx
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDate, setSelectedDate] = useState<Date | undefined>(new Date());
const [sheetOpen, setSheetOpen] = useState(false);
const { data: tasks } = useQuery({
queryKey: ["tasks-calendar", format(currentDate, "yyyy-MM")],
queryFn: async () => {
const start = startOfMonth(currentDate);
const end = endOfMonth(currentDate);
const { data, error } = await supabase
.from("tasks")
.select("*")
.gte("due_date", format(start, "yyyy-MM-dd"))
.lte("due_date", format(end, "yyyy-MM-dd") + "T23:59:59")
.order("due_date", { ascending: true });
if (error) throw error;
return data as Task[];
},
});
const getTasksForDate = (date: Date) => {
return tasks.filter((task) => {
return task.due_date && isSameDay(new Date(task.due_date), date);
});
};
const tasksForSelectedDate = selectedDate ? getTasksForDate(selectedDate) : [];
const getDayContent = (date: Date) => {
const dayTasks = getTasksForDate(date);
const taskCount = dayTasks.length;
if (taskCount === 0) return null;
// Máximo 3 puntos
const dotsToShow = Math.min(taskCount, 3);
// Obtener prioridad más alta
const highestPriority = dayTasks.reduce((highest, task) => {
const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 };
return priorityOrder[task.priority] > priorityOrder[highest]
? task.priority
: highest;
}, "low" as TaskPriority);
return (
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2 flex gap-0.5">
{Array.from({ length: dotsToShow }).map((_, i) => (
<div
key={i}
className={cn(
"w-1 h-1 rounded-full",
priorityConfig[highestPriority].dotColor
)}
/>
))}
</div>
);
};
<Calendar
mode="single"
selected={selectedDate}
onSelect={setSelectedDate}
month={currentDate}
onMonthChange={handleMonthChange}
locale={es}
modifiers={{
hasTask: datesWithTasks,
}}
components={{
DayContent: ({ date }) => (
<div className="relative flex flex-col items-center justify-center w-full h-9">
<span>{date.getDate()}</span>
{getDayContent(date)}
</div>
),
}}
/>
<Card className="h-full">
<CardHeader>
<CardTitle>
{selectedDate
? format(selectedDate, "d 'de' MMMM", { locale: es })
: "Selecciona un día"}
</CardTitle>
<Button size="sm" onClick={handleNewTask}>
<Plus className="h-3.5 w-3.5" />
Nueva
</Button>
</CardHeader>
<CardContent>
{tasksForSelectedDate.length === 0 ? (
<div className="text-center py-8">
<Clock className="h-5 w-5 text-muted-foreground mx-auto mb-3" />
<p className="text-sm text-muted-foreground">
No hay tareas para este día
</p>
<Button variant="link" size="sm" onClick={handleNewTask}>
Crear una tarea
</Button>
</div>
) : (
<div className="space-y-2">
{tasksForSelectedDate.map((task) => (
<div
key={task.id}
className="p-3 border rounded-lg cursor-pointer hover:bg-accent/50"
onClick={() => navigate(`/tasks/${task.id}`)}
>
{/* Indicador prioridad */}
<div className={cn(
"w-1 h-full min-h-[40px] rounded-full",
priorityConfig[task.priority].dotColor
)} />
{/* Título */}
<p className="font-medium text-sm truncate">{task.title}</p>
{/* Badges: estado, prioridad, hora */}
<div className="flex items-center gap-2 mt-1.5">
<Badge variant="outline" className={statusConfig[task.status].color}>
{statusConfig[task.status].label}
</Badge>
<span className={priorityConfig[task.priority].color}>
{priorityConfig[task.priority].label}
</span>
{task.due_date && (
<span className="text-[10px] text-muted-foreground">
<Clock className="h-2.5 w-2.5" />
{format(new Date(task.due_date), "HH:mm")}
</span>
)}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
const priorityConfig: Record<TaskPriority, {
color: string;
dotColor: string;
label: string
}> = {
low: {
color: "bg-blue-500/10 text-blue-700",
dotColor: "bg-blue-500",
label: "Baja"
},
medium: {
color: "bg-yellow-500/10 text-yellow-700",
dotColor: "bg-yellow-500",
label: "Media"
},
high: {
color: "bg-orange-500/10 text-orange-700",
dotColor: "bg-orange-500",
label: "Alta"
},
urgent: {
color: "bg-red-500/10 text-red-700",
dotColor: "bg-red-500",
label: "Urgente"
},
};
const handlePrevMonth = () => {
const newDate = new Date(currentDate);
newDate.setMonth(newDate.getMonth() - 1);
setCurrentDate(newDate);
};
const handleNextMonth = () => {
const newDate = new Date(currentDate);
newDate.setMonth(newDate.getMonth() + 1);
setCurrentDate(newDate);
};
const handleToday = () => {
setCurrentDate(new Date());
setSelectedDate(new Date());
};
<TaskFormSheet
open={sheetOpen}
onOpenChange={setSheetOpen}
defaultDueDate={selectedDate ? format(selectedDate, "yyyy-MM-dd") : undefined}
onSuccess={handleTaskSuccess}
/>
<div className="flex flex-col lg:flex-row gap-4">
{/* Calendario principal */}
<div className="flex-1 lg:flex-[2]">
<Card>{/* Calendar */}</Card>
</div>
{/* Panel lateral */}
<div className="w-full lg:w-80">
<Card>{/* Tareas del día */}</Card>
</div>
</div>