Ir al contenido

Tareas Recurrentes

Vista centralizada para gestionar todas las tareas con configuración de recurrencia.

  • Lista de tareas activas: Tareas que se generan automáticamente
  • Lista de tareas pausadas: Configuraciones inactivas
  • Pausar/Reanudar: Control del estado de generación
  • Vista previa: Próximas 5 fechas de generación
  • Información detallada: Tipo, intervalo, límites, última generación

Tareas Recurrentes Activas:

  • Badge con contador
  • Estado verde (activo)
  • Botón “Pausar” para detener generación
  • Expandible para ver próximas fechas

Tareas Pausadas:

  • Badge con contador
  • Estado gris (pausado)
  • Botón “Reanudar” para reactivar
  • Mensaje informativo al expandir
  • Título de tarea: Nombre de la tarea original
  • Prioridad: Badge de color
  • Tipo de recurrencia: Diaria, Semanal, Mensual, Anual
  • Estado: Activa o Pausada
  • Última generación: Fecha de última instancia creada
  • Fecha final: Cuándo termina la recurrencia (si aplica)
  • Máximo ocurrencias: Límite de instancias (si aplica)

Pausar:

  • Detiene la generación automática
  • Mantiene la configuración
  • Se puede reanudar en cualquier momento

Reanudar:

  • Reactiva la generación automática
  • Continúa desde la última fecha generada

Ver Próximas Fechas:

  • Expandir tarjeta
  • Muestra las próximas 5 fechas calculadas
  • Formato legible en español
Detalles Técnicos
  • Archivo: RecurringTasksList.tsx
  • Ruta: src/features/projects/sub-modules/tasks/components/RecurringTasksList.tsx
interface RecurringTaskConfig {
id: string;
task_id: string;
recurrence_type: string;
recurrence_interval: number;
recurrence_days: number[] | null;
recurrence_end_date: string | null;
max_occurrences: number | null;
last_generated_date: string | null;
is_active: boolean;
created_at: string;
task: {
id: string;
title: string;
priority: string;
status: string;
} | null;
}
const { data: recurringTasks } = useQuery({
queryKey: ['recurring-tasks-list'],
queryFn: async () => {
const { data, error } = await supabase
.from('task_recurrence_config')
.select(`
*,
task:tasks(
id,
title,
priority,
status
)
`)
.order('is_active', { ascending: false })
.order('created_at', { ascending: false });
if (error) throw error;
return data as RecurringTaskConfig[];
},
});
const activeTasks = recurringTasks.filter(t => t.is_active);
const pausedTasks = recurringTasks.filter(t => !t.is_active);
const toggleActiveMutation = useMutation({
mutationFn: async ({ configId, isActive }) => {
const { error } = await supabase
.from('task_recurrence_config')
.update({ is_active: !isActive })
.eq('id', configId);
if (error) throw error;
},
onSuccess: (_, { isActive }) => {
queryClient.invalidateQueries({ queryKey: ['recurring-tasks-list'] });
toast({
title: isActive ? "Recurrencia pausada" : "Recurrencia reanudada",
description: isActive
? "No se generarán nuevas instancias hasta que la reactives"
: "Se generarán nuevas instancias según la configuración",
});
}
});
function RecurringTaskCard({
config,
isExpanded,
onToggleExpand,
onToggleActive,
isToggling
}) {
const priority = config.task?.priority || 'medium';
const priorityInfo = priorityConfig[priority];
return (
<Collapsible open={isExpanded} onOpenChange={onToggleExpand}>
<div className={cn(
"border rounded-lg",
config.is_active
? "bg-card hover:border-primary/50"
: "bg-muted/30 opacity-75"
)}>
{/* Contenido principal */}
<div className="p-4">
{/* Header: título, prioridad */}
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<div className={cn(
"w-2 h-2 rounded-full",
config.is_active ? "bg-green-500" : "bg-gray-400"
)} />
<h3 className="font-medium">{config.task?.title}</h3>
</div>
<Badge className={priorityInfo.bgColor}>
{priorityInfo.label}
</Badge>
</div>
{/* Badges: tipo recurrencia, estado */}
<div className="flex items-center gap-2 mt-3">
<Badge variant="outline">
<Repeat className="h-3 w-3 mr-1" />
{getRecurrenceLabel(config)}
</Badge>
<Badge variant={config.is_active ? "default" : "secondary"}>
{config.is_active ? (
<><Play className="h-3 w-3 mr-1" />Activa</>
) : (
<><Pause className="h-3 w-3 mr-1" />Pausada</>
)}
</Badge>
</div>
{/* Info: última, hasta, máx */}
<div className="flex items-center gap-4 text-xs text-muted-foreground mt-3">
{config.last_generated_date && (
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
<span>Última: {formatDate(config.last_generated_date)}</span>
</div>
)}
{config.recurrence_end_date && (
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
<span>Hasta: {formatDate(config.recurrence_end_date)}</span>
</div>
)}
{config.max_occurrences && (
<div className="flex items-center gap-1">
<Hash className="h-3 w-3" />
<span>Máx. {config.max_occurrences} veces</span>
</div>
)}
</div>
{/* Acciones */}
<div className="flex items-center justify-between gap-2 mt-3">
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm">
{isExpanded ? (
<ChevronDown className="h-4 w-4 mr-1" />
) : (
<ChevronRight className="h-4 w-4 mr-1" />
)}
Ver próximas fechas
</Button>
</CollapsibleTrigger>
<Button
variant={config.is_active ? "outline" : "default"}
size="sm"
onClick={() => onToggleActive(config.id, config.is_active)}
disabled={isToggling}
>
{config.is_active ? (
<><Pause className="h-4 w-4 mr-1" />Pausar</>
) : (
<><Play className="h-4 w-4 mr-1" />Reanudar</>
)}
</Button>
</div>
</div>
{/* Contenido expandido */}
<CollapsibleContent>
<div className="px-4 pb-4 pt-2 border-t bg-muted/30">
{config.is_active ? (
<RecurrencePreview
config={config}
count={5}
showIcon={false}
/>
) : (
<p className="text-sm text-muted-foreground">
La recurrencia está pausada. Reactívala para ver las próximas fechas.
</p>
)}
</div>
</CollapsibleContent>
</div>
</Collapsible>
);
}
// Obtener etiqueta legible
export const getRecurrenceLabel = (config: RecurrenceConfig) => {
const { recurrence_type, recurrence_interval } = config;
const labels = {
daily: 'día',
weekly: 'semana',
monthly: 'mes',
yearly: 'año'
};
const label = labels[recurrence_type];
return recurrence_interval === 1
? `Cada ${label}`
: `Cada ${recurrence_interval} ${label}s`;
};
// Formatear fecha
export const formatRecurrenceDate = (date: string) => {
return format(new Date(date), "d MMM yyyy", { locale: es });
};
// Fecha relativa
export const formatRelativeDate = (date: string) => {
const now = new Date();
const target = new Date(date);
const diffDays = differenceInDays(target, now);
if (diffDays === 0) return 'Hoy';
if (diffDays === 1) return 'Mañana';
if (diffDays === -1) return 'Ayer';
if (diffDays > 1 && diffDays <= 7) return `En ${diffDays} días`;
if (diffDays < -1 && diffDays >= -7) return `Hace ${Math.abs(diffDays)} días`;
return formatRecurrenceDate(date);
};
const priorityConfig: Record<string, {
label: string;
color: string;
bgColor: string
}> = {
low: {
label: "Baja",
color: "text-green-600",
bgColor: "bg-green-100 dark:bg-green-900/30"
},
medium: {
label: "Media",
color: "text-amber-600",
bgColor: "bg-amber-100 dark:bg-amber-900/30"
},
high: {
label: "Alta",
color: "text-orange-600",
bgColor: "bg-orange-100 dark:bg-orange-900/30"
},
urgent: {
label: "Urgente",
color: "text-red-600",
bgColor: "bg-red-100 dark:bg-red-900/30"
},
};
<div className="space-y-6">
{/* Tareas Activas */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Repeat className="h-5 w-5 text-primary" />
Tareas Recurrentes Activas
<Badge variant="secondary">{activeTasks.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
{activeTasks.length === 0 ? (
<p className="text-sm text-muted-foreground py-4 text-center">
No hay tareas recurrentes activas
</p>
) : (
<div className="space-y-3">
{activeTasks.map((config) => (
<RecurringTaskCard key={config.id} config={config} {...props} />
))}
</div>
)}
</CardContent>
</Card>
{/* Tareas Pausadas */}
{pausedTasks.length > 0 && (
<Card className="border-dashed">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-muted-foreground">
<Pause className="h-5 w-5" />
Tareas Pausadas
<Badge variant="outline">{pausedTasks.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{pausedTasks.map((config) => (
<RecurringTaskCard key={config.id} config={config} {...props} />
))}
</div>
</CardContent>
</Card>
)}
</div>