'use client'; import { useCallback, useEffect, useState } from 'react'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { Spinner } from '@/components/ui/spinner'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Clock, Edit2, Info, Plus, RefreshCw, Trash2 } from 'lucide-react'; import type { CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest, } from '@/lib/services/admin'; import services from '@/lib/services'; import { buildTaskPayload } from '@/lib/task-param-utils'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { EmptyStateWithBorder } from '@/components/layout/empty'; export function TaskSchedulesManager() { const t = useTranslations('admin.tasks'); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [schedules, setSchedules] = useState([]); const [taskTypes, setTaskTypes] = useState([]); // Modal States const [dialogOpen, setDialogOpen] = useState(false); const [submitLoading, setSubmitLoading] = useState(false); const [editingSchedule, setEditingSchedule] = useState(null); // Delete States const [deleteOpen, setDeleteOpen] = useState(false); const [deletingId, setDeletingId] = useState(null); const [deleteLoading, setDeleteLoading] = useState(false); // Form Fields const [name, setName] = useState(''); const [selectedTaskType, setSelectedTaskType] = useState(''); const [cron, setCron] = useState(''); const [isActive, setIsActive] = useState(true); const [paramValues, setParamValues] = useState>({}); // Fetch Schedules & Task Types const fetchData = useCallback(async () => { try { setLoading(true); setError(null); const [schedulesData, taskTypesData] = await Promise.all([ services.adminTask.listSchedules(), services.adminTask.getTaskTypes(), ]); setSchedules(schedulesData || []); setTaskTypes(taskTypesData || []); } catch (err) { setError(err instanceof Error ? err : new Error(t('loadSchedulesFailed'))); } finally { setLoading(false); } }, [t]); useEffect(() => { fetchData(); }, [fetchData]); // Track task type selection to initialize parameters useEffect(() => { if (selectedTaskType) { const targetTask = taskTypes.find((t) => t.type === selectedTaskType); if (editingSchedule && editingSchedule.task_type === selectedTaskType) { // Parse existing payload when editing the same task type try { const parsed = JSON.parse(editingSchedule.payload || '{}'); const values: Record = {}; targetTask?.params?.forEach((p) => { values[p.name] = String(parsed[p.name] ?? ''); }); setParamValues(values); } catch { const initialValues: Record = {}; targetTask?.params?.forEach((p) => { initialValues[p.name] = ''; }); setParamValues(initialValues); } } else { const initialValues: Record = {}; targetTask?.params?.forEach((p) => { initialValues[p.name] = ''; }); setParamValues(initialValues); } } else { setParamValues({}); } }, [selectedTaskType, taskTypes, editingSchedule]); const handleToggleActive = async (schedule: Schedule) => { try { const updated = await services.adminTask.updateSchedule(schedule.id, { name: schedule.name, task_type: schedule.task_type, cron: schedule.cron, payload: schedule.payload, is_active: !schedule.is_active, }); setSchedules((prev) => prev.map((s) => (s.id === schedule.id ? updated : s)), ); toast.success( updated.is_active ? t('scheduleEnabled', { name: updated.name }) : t('scheduleDisabled', { name: updated.name }), ); } catch (err) { toast.error(t('changeScheduleStatusFailed'), { description: err instanceof Error ? err.message : t('unknownError'), }); } }; const openCreateDialog = () => { setEditingSchedule(null); setName(''); setSelectedTaskType(taskTypes[0]?.type || ''); setCron('0 */2 * * *'); setIsActive(true); setParamValues({}); setDialogOpen(true); }; const openEditDialog = (schedule: Schedule) => { setEditingSchedule(schedule); setName(schedule.name); setSelectedTaskType(schedule.task_type); setCron(schedule.cron); setIsActive(schedule.is_active); setDialogOpen(true); }; const handleSubmit = async () => { if (!name.trim()) { toast.error(t('scheduleNameRequired')); return; } if (!cron.trim()) { toast.error(t('cronRequired')); return; } try { setSubmitLoading(true); const targetTask = taskTypes.find((t) => t.type === selectedTaskType); let payloadStr = '{}'; if (targetTask?.params && targetTask.params.length > 0) { const { payload, error } = buildTaskPayload( targetTask.params, paramValues, ); if (error) { toast.error(error); setSubmitLoading(false); return; } payloadStr = payload ?? '{}'; } if (editingSchedule) { const req: UpdateScheduleRequest = { name, task_type: selectedTaskType, cron, payload: payloadStr, is_active: isActive, }; const updated = await services.adminTask.updateSchedule( editingSchedule.id, req, ); setSchedules((prev) => prev.map((s) => (s.id === editingSchedule.id ? updated : s)), ); toast.success(t('scheduleUpdateSuccess')); } else { const req: CreateScheduleRequest = { name, task_type: selectedTaskType, cron, payload: payloadStr, is_active: isActive, }; const created = await services.adminTask.createSchedule(req); setSchedules((prev) => [created, ...prev]); toast.success(t('scheduleCreateSuccess')); } setDialogOpen(false); } catch (err) { toast.error(t('submitFailed'), { description: err instanceof Error ? err.message : t('unknownError'), }); } finally { setSubmitLoading(false); } }; const openDeleteDialog = (id: string) => { setDeletingId(id); setDeleteOpen(true); }; const handleDelete = async () => { if (!deletingId) return; try { setDeleteLoading(true); await services.adminTask.deleteSchedule(deletingId); setSchedules((prev) => prev.filter((s) => s.id !== deletingId)); toast.success(t('scheduleDeleteSuccess')); setDeleteOpen(false); } catch (err) { toast.error(t('deleteFailed'), { description: err instanceof Error ? err.message : t('unknownError'), }); } finally { setDeleteLoading(false); } }; const getSelectedTaskMeta = () => { return taskTypes.find((t) => t.type === selectedTaskType); }; const getTaskName = (type: string) => { const meta = taskTypes.find((t) => t.type === type); return meta ? meta.name : type; }; return (

{error ? (
) : loading && schedules.length === 0 ? ( ) : schedules.length === 0 ? ( ) : (
{t('colScheduleName')} {t('colAssociatedTask')} {t('colCron')} {t('colPayload')} {t('colEnabled')} {t('colActions')} {schedules.map((schedule) => ( {schedule.name}
{getTaskName(schedule.task_type)} {schedule.task_type}
{schedule.cron} {schedule.payload || '{}'}
handleToggleActive(schedule)} />
))}
)} {/* Add / Edit Dialog */} {editingSchedule ? t('editSchedule') : t('addSchedule')} {t('scheduleDialogDesc')}
setName(e.target.value)} className='text-xs' />
setCron(e.target.value)} className='text-xs font-mono' />

{t('cronHint')}

{t('scheduleActiveDesc')}

{(() => { const targetTask = getSelectedTaskMeta(); if ( !targetTask || !targetTask.params || targetTask.params.length === 0 ) { return (
{t('noParamsRequired')}
); } return (
{targetTask.params.map((param) => (
{param.type === 'text' ? (