import React, { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import axios from '../api/client'; import { Plus, Play, Pause, Trash2, Edit, Clock, Calendar, CalendarDays, RotateCw, CheckCircle2, XCircle, AlertCircle, Zap, Power, } from 'lucide-react'; import toast from 'react-hot-toast'; import CreateTaskModal from '../components/CreateTaskModal'; import EditTaskModal from '../components/EditTaskModal'; import { usePlanFeatures } from '../hooks/usePlanFeatures'; import { LockedSection } from '../components/UpgradePrompt'; // Types interface ScheduledTask { id: string; name: string; description: string; plugin_name: string; plugin_display_name: string; schedule_type: 'ONE_TIME' | 'INTERVAL' | 'CRON'; cron_expression?: string; interval_minutes?: number; run_at?: string; next_run_at?: string; last_run_at?: string; status: 'ACTIVE' | 'PAUSED' | 'DISABLED'; last_run_status?: string; plugin_config: Record; created_at: string; updated_at: string; } interface PluginInstallation { id: string; template: number; template_name: string; template_slug: string; template_description: string; category: string; version: string; author_name: string; logo_url?: string; template_variables: Record; scheduled_task?: number; scheduled_task_name?: string; installed_at: string; config_values: Record; has_update: boolean; } interface GlobalEventPlugin { id: string; plugin_installation: number; plugin_name: string; plugin_description: string; plugin_category: string; plugin_logo_url?: string; trigger: string; trigger_display: string; offset_minutes: number; timing_description: string; is_active: boolean; apply_to_existing: boolean; execution_order: number; events_count: number; created_at: string; updated_at: string; } // Unified task type for display type UnifiedTask = { type: 'scheduled'; data: ScheduledTask; } | { type: 'event'; data: GlobalEventPlugin; }; const Tasks: React.FC = () => { const { t } = useTranslation(); const queryClient = useQueryClient(); const [showCreateModal, setShowCreateModal] = useState(false); const [editingTask, setEditingTask] = useState(null); const [editingEventAutomation, setEditingEventAutomation] = useState(null); // Check plan permissions - tasks requires both plugins AND tasks features const { canUse, isLoading: permissionsLoading } = usePlanFeatures(); const hasPluginsFeature = canUse('plugins'); const hasTasksFeature = canUse('tasks'); const isLocked = !hasPluginsFeature || !hasTasksFeature; // Fetch scheduled tasks const { data: scheduledTasks = [], isLoading: tasksLoading } = useQuery({ queryKey: ['scheduled-tasks'], queryFn: async () => { const { data } = await axios.get('/scheduled-tasks/'); return data; }, }); // Fetch global event plugins (event automations) const { data: eventAutomations = [], isLoading: automationsLoading } = useQuery({ queryKey: ['global-event-plugins'], queryFn: async () => { const { data } = await axios.get('/global-event-plugins/'); return data; }, }); // Combine into unified list const allTasks: UnifiedTask[] = useMemo(() => { const scheduled: UnifiedTask[] = scheduledTasks.map(t => ({ type: 'scheduled' as const, data: t })); const events: UnifiedTask[] = eventAutomations.map(e => ({ type: 'event' as const, data: e })); return [...scheduled, ...events].sort((a, b) => { const dateA = new Date(a.data.created_at).getTime(); const dateB = new Date(b.data.created_at).getTime(); return dateB - dateA; // Most recent first }); }, [scheduledTasks, eventAutomations]); const isLoading = tasksLoading || automationsLoading; // Delete task const deleteMutation = useMutation({ mutationFn: async (taskId: string) => { await axios.delete(`/scheduled-tasks/${taskId}/`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] }); toast.success('Task deleted successfully'); }, onError: (error: any) => { toast.error(error.response?.data?.detail || 'Failed to delete task'); }, }); // Toggle task active status const toggleActiveMutation = useMutation({ mutationFn: async ({ taskId, status }: { taskId: string; status: 'ACTIVE' | 'PAUSED' }) => { await axios.patch(`/scheduled-tasks/${taskId}/`, { status }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] }); toast.success('Task updated successfully'); }, onError: (error: any) => { toast.error(error.response?.data?.detail || 'Failed to update task'); }, }); // Trigger task manually const triggerMutation = useMutation({ mutationFn: async (taskId: string) => { await axios.post(`/scheduled-tasks/${taskId}/trigger/`); }, onSuccess: () => { toast.success('Task triggered successfully'); }, onError: (error: any) => { toast.error(error.response?.data?.detail || 'Failed to trigger task'); }, }); // Delete event automation const deleteEventAutomationMutation = useMutation({ mutationFn: async (automationId: string) => { await axios.delete(`/global-event-plugins/${automationId}/`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['global-event-plugins'] }); toast.success('Event automation deleted'); }, onError: (error: any) => { toast.error(error.response?.data?.detail || 'Failed to delete automation'); }, }); // Toggle event automation active status const toggleEventAutomationMutation = useMutation({ mutationFn: async (automationId: string) => { await axios.post(`/global-event-plugins/${automationId}/toggle/`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['global-event-plugins'] }); toast.success('Automation updated'); }, onError: (error: any) => { toast.error(error.response?.data?.detail || 'Failed to update automation'); }, }); // Update event automation const updateEventAutomationMutation = useMutation({ mutationFn: async ({ id, data }: { id: string; data: Partial }) => { await axios.patch(`/global-event-plugins/${id}/`, data); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['global-event-plugins'] }); toast.success('Automation updated'); setEditingEventAutomation(null); }, onError: (error: any) => { toast.error(error.response?.data?.detail || 'Failed to update automation'); }, }); const getScheduleDisplay = (task: ScheduledTask): string => { if (task.schedule_type === 'ONE_TIME') { return `Once on ${new Date(task.run_at!).toLocaleString()}`; } else if (task.schedule_type === 'INTERVAL') { const hours = Math.floor(task.interval_minutes! / 60); const mins = task.interval_minutes! % 60; if (hours > 0 && mins > 0) { return `Every ${hours}h ${mins}m`; } else if (hours > 0) { return `Every ${hours} hour${hours > 1 ? 's' : ''}`; } else { return `Every ${mins} minute${mins > 1 ? 's' : ''}`; } } else { return task.cron_expression || 'Custom schedule'; } }; const getScheduleIcon = (task: ScheduledTask) => { if (task.schedule_type === 'ONE_TIME') return Calendar; if (task.schedule_type === 'INTERVAL') return RotateCw; return Clock; }; const getStatusColor = (task: ScheduledTask): string => { if (task.status === 'PAUSED' || task.status === 'DISABLED') return 'text-gray-400 dark:text-gray-500'; if (task.last_run_at) return 'text-green-600 dark:text-green-400'; return 'text-blue-600 dark:text-blue-400'; }; const getStatusIcon = (task: ScheduledTask) => { if (task.status === 'PAUSED' || task.status === 'DISABLED') return Pause; if (task.last_run_at) return CheckCircle2; return AlertCircle; }; if (isLoading || permissionsLoading) { return (
); } return (
{/* Header */}

{t('Tasks')}

Schedule and manage automated plugin executions

{/* Stats */}

Total Tasks

{allTasks.length}

Active

{scheduledTasks.filter(t => t.status === 'ACTIVE').length + eventAutomations.filter(e => e.is_active).length}

Event Automations

{eventAutomations.length}

Scheduled

{scheduledTasks.length}

{/* Tasks List */} {allTasks.length === 0 ? (

No tasks yet

Create your first automated task to get started

) : (
{allTasks.map((unifiedTask) => { if (unifiedTask.type === 'scheduled') { const task = unifiedTask.data; const ScheduleIcon = getScheduleIcon(task); const StatusIcon = getStatusIcon(task); return (
Scheduled

{task.name}

{task.description && (

{task.description}

)}
{task.plugin_display_name}
{getScheduleDisplay(task)}
{task.next_run_at && task.status === 'ACTIVE' && (
Next: {new Date(task.next_run_at).toLocaleString()}
)} {task.last_run_at && (
Last: {new Date(task.last_run_at).toLocaleString()}
)}
{/* Actions */}
); } else { // Event automation const automation = unifiedTask.data; return (
Event Automation

{automation.plugin_name}

{automation.is_active ? ( ) : ( )}
{automation.plugin_description && (

{automation.plugin_description}

)}
{automation.timing_description}
{automation.events_count} events
{automation.apply_to_existing ? 'All events' : 'Future only'}
{/* Actions */}
); } })}
)} {/* Create Task Modal */} setShowCreateModal(false)} onSuccess={() => { queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] }); queryClient.invalidateQueries({ queryKey: ['global-event-plugins'] }); }} /> {/* Edit Task Modal */} setEditingTask(null)} onSuccess={() => { queryClient.invalidateQueries({ queryKey: ['scheduled-tasks'] }); toast.success('Task updated successfully'); }} /> {/* Edit Event Automation Modal */} {editingEventAutomation && ( setEditingEventAutomation(null)} onSave={(data) => updateEventAutomationMutation.mutate({ id: editingEventAutomation.id, data })} isLoading={updateEventAutomationMutation.isPending} /> )}
); }; // Inline Edit Event Automation Modal component interface EditEventAutomationModalProps { automation: GlobalEventPlugin; isOpen: boolean; onClose: () => void; onSave: (data: Partial) => void; isLoading: boolean; } const TRIGGER_OPTIONS = [ { value: 'before_start', label: 'Before Start' }, { value: 'at_start', label: 'At Start' }, { value: 'after_start', label: 'After Start' }, { value: 'after_end', label: 'After End' }, { value: 'on_complete', label: 'When Completed' }, { value: 'on_cancel', label: 'When Canceled' }, ]; const OFFSET_PRESETS = [ { value: 0, label: 'Immediately' }, { value: 5, label: '5 min' }, { value: 10, label: '10 min' }, { value: 15, label: '15 min' }, { value: 30, label: '30 min' }, { value: 60, label: '1 hour' }, ]; const EditEventAutomationModal: React.FC = ({ automation, isOpen, onClose, onSave, isLoading, }) => { const [trigger, setTrigger] = useState(automation.trigger); const [offsetMinutes, setOffsetMinutes] = useState(automation.offset_minutes); const showOffset = !['on_complete', 'on_cancel'].includes(trigger); const getTimingDescription = () => { if (trigger === 'on_complete') return 'When event is completed'; if (trigger === 'on_cancel') return 'When event is canceled'; if (offsetMinutes === 0) { if (trigger === 'before_start' || trigger === 'at_start' || trigger === 'after_start') return 'At event start'; if (trigger === 'after_end') return 'At event end'; } const offsetLabel = OFFSET_PRESETS.find(o => o.value === offsetMinutes)?.label || `${offsetMinutes} min`; if (trigger === 'before_start') return `${offsetLabel} before event starts`; if (trigger === 'at_start' || trigger === 'after_start') return `${offsetLabel} after event starts`; if (trigger === 'after_end') return `${offsetLabel} after event ends`; return 'Unknown timing'; }; const handleSave = () => { onSave({ trigger, offset_minutes: offsetMinutes, }); }; if (!isOpen) return null; return (
{/* Header */}

Edit Event Automation

{/* Content */}
{/* Plugin info */}

Plugin

{automation.plugin_name}

{/* When to run */}
{TRIGGER_OPTIONS.map((opt) => ( ))}
{/* Offset selection */} {showOffset && (
{OFFSET_PRESETS.map((preset) => ( ))}
)} {/* Preview */}
Runs: {getTimingDescription()}
{/* Footer */}
); }; export default Tasks;