Tareas Recurrentes
Exportar contenido
Vista centralizada para gestionar todas las tareas con configuración de recurrencia.
Características
Sección titulada «Características»- 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
Secciones
Sección titulada «Secciones»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
Información Mostrada
Sección titulada «Información Mostrada»- 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)
Acciones
Sección titulada «Acciones»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
Componente
Sección titulada «Componente»- Archivo:
RecurringTasksList.tsx - Ruta:
src/features/projects/sub-modules/tasks/components/RecurringTasksList.tsx
Interface RecurringTaskConfig
Sección titulada «Interface RecurringTaskConfig»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;}Query Tareas Recurrentes
Sección titulada «Query Tareas Recurrentes»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[]; },});Separar Activas y Pausadas
Sección titulada «Separar Activas y Pausadas»const activeTasks = recurringTasks.filter(t => t.is_active);const pausedTasks = recurringTasks.filter(t => !t.is_active);Mutación Toggle Estado
Sección titulada «Mutación Toggle Estado»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", }); }});Componente RecurringTaskCard
Sección titulada «Componente RecurringTaskCard»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> );}Utilidades de Recurrencia
Sección titulada «Utilidades de Recurrencia»// Obtener etiqueta legibleexport 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 fechaexport const formatRecurrenceDate = (date: string) => { return format(new Date(date), "d MMM yyyy", { locale: es });};
// Fecha relativaexport 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);};Configuración de Prioridad
Sección titulada «Configuración de Prioridad»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" },};Layout con Secciones
Sección titulada «Layout con Secciones»<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>