// Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 'use client'; import * as React from 'react'; import { useTranslations } from 'next-intl'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { ChevronDown, Edit2, Layers, Loader2, Plus, Trash2, } from 'lucide-react'; 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 { Badge } from '@/components/ui/badge'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { EmptyStateWithBorder } from '@/components/layout/empty'; import services from '@/lib/services'; import type { CreatePushEventRequest, PushEvent, UpdatePushEventRequest, } from '@/lib/services/push'; import { PushService } from '@/lib/services/push'; export function EventsTab() { const t = useTranslations('admin.push.events'); const queryClient = useQueryClient(); const [deleteTarget, setDeleteTarget] = React.useState( null, ); // --- 获取所有自定义消息通道 --- const channelsQuery = useQuery({ queryKey: ['admin', 'push-channels'], queryFn: () => PushService.listChannels(), }); const availableChannels = React.useMemo(() => { const customChannels = (channelsQuery.data ?? []) .filter((c) => c.enabled) .map((c) => c.name); return ['email', ...customChannels]; }, [channelsQuery.data]); // --- 获取通知事件 --- const eventsQuery = useQuery({ queryKey: ['admin', 'push-events'], queryFn: () => PushService.listEvents(), }); const builtInEventsQuery = useQuery({ queryKey: ['admin', 'push-builtin-events'], queryFn: () => PushService.listBuiltInEvents(), }); // --- 获取所有系统可调度任务类型 --- const taskTypesQuery = useQuery({ queryKey: ['admin', 'task-types'], queryFn: () => services.adminTask.getTaskTypes(), }); // --- 修改保存事件 Mutation --- const updateEventMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: UpdatePushEventRequest }) => PushService.updateEvent(id, data), onSuccess: () => { toast.success(t('eventUpdateSuccess')); queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); setEditEventOpen(false); }, onError: (err: unknown) => { toast.error(t('eventUpdateFailed') + ': ' + (err as Error).message); }, }); const toggleEventMutation = useMutation({ mutationFn: (id: number) => PushService.toggleEvent(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); }, onError: (err: unknown) => { toast.error(t('operationFailed') + ': ' + (err as Error).message); }, }); const createEventMutation = useMutation({ mutationFn: (data: CreatePushEventRequest) => PushService.createEvent(data), onSuccess: () => { toast.success(t('eventCreateSuccess')); queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); setCreateEventOpen(false); setNewEventKey(''); setNewEventType('builtin'); setNewEventTaskType(''); setNewEventChannels([]); setNewEventEnabled(true); }, onError: (err: unknown) => { toast.error(t('eventCreateFailed') + ': ' + (err as Error).message); }, }); const deleteEventMutation = useMutation({ mutationFn: (id: number) => PushService.deleteEvent(id), onSuccess: () => { setDeleteTarget(null); toast.success(t('configDeleteSuccess')); queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] }); }, onError: (err: unknown) => { toast.error(t('configDeleteFailed') + ': ' + (err as Error).message); }, }); // --- 事件编辑对话框状态 --- const [editEventOpen, setEditEventOpen] = React.useState(false); const [selectedEvent, setSelectedEvent] = React.useState( null, ); const [eventChannels, setEventChannels] = React.useState([]); const [eventTargets, setEventTargets] = React.useState(''); const [eventTemplate, setEventTemplate] = React.useState(''); // 事件创建对话框状态 const [createEventOpen, setCreateEventOpen] = React.useState(false); const [newEventType, setNewEventType] = React.useState<'builtin' | 'task'>( 'builtin', ); const [newEventKey, setNewEventKey] = React.useState(''); const [newEventTaskType, setNewEventTaskType] = React.useState(''); const [newEventChannels, setNewEventChannels] = React.useState([]); const [newEventTargets, setNewEventTargets] = React.useState(''); const [newEventTemplate, setNewEventTemplate] = React.useState(''); const [newEventEnabled, setNewEventEnabled] = React.useState(true); const availableBuiltInEvents = React.useMemo(() => { const configuredKeys = new Set( (eventsQuery.data ?? []).map((e) => e.event_key), ); return (builtInEventsQuery.data ?? []).filter( (e) => !configuredKeys.has(e.key), ); }, [builtInEventsQuery.data, eventsQuery.data]); const handleEditEventClick = (event: PushEvent) => { setSelectedEvent(event); setEventChannels(event.channels); setEventTargets((event.targets ?? []).join(', ')); setEventTemplate(event.template); setEditEventOpen(true); }; const handleSaveEvent = () => { if (!selectedEvent) return; try { JSON.parse(eventTemplate); } catch { toast.error(t('templateInvalidJson')); return; } const targets = eventTargets .split(',') .map((t) => t.trim()) .filter((t) => t !== ''); updateEventMutation.mutate({ id: selectedEvent.id, data: { channels: eventChannels, targets, template: eventTemplate, enabled: selectedEvent.enabled, }, }); }; const handleCreateEventClick = () => { setNewEventKey(''); setNewEventType('builtin'); setNewEventTaskType(''); setNewEventChannels([]); setNewEventTargets(''); setNewEventTemplate(''); setNewEventEnabled(true); setCreateEventOpen(true); }; const handleNewEventKeyChange = (key: string) => { setNewEventKey(key); const ev = availableBuiltInEvents.find((e) => e.key === key); if (ev) { setNewEventTemplate(JSON.stringify(ev.default_template, null, 2)); } else { setNewEventTemplate(''); } }; const handleNewEventTaskTypeChange = (taskType: string) => { setNewEventTaskType(taskType); const taskMeta = (taskTypesQuery.data ?? []).find( (t) => t.asynq_task === taskType, ); if (taskMeta) { const defaultTemplate = { title: t('taskCompleteTitle', { name: taskMeta.name }), content: t('taskCompleteContent'), level: 'INFO', }; setNewEventTemplate(JSON.stringify(defaultTemplate, null, 2)); } else { setNewEventTemplate(''); } }; const handleCreateEvent = () => { if (newEventType === 'builtin' && !newEventKey) { toast.error(t('selectSystemEvent')); return; } if (newEventType === 'task' && !newEventTaskType) { toast.error(t('selectAsyncTask')); return; } if (newEventTemplate) { try { JSON.parse(newEventTemplate); } catch { toast.error(t('templateInvalidJson')); return; } } const targets = newEventTargets .split(',') .map((t) => t.trim()) .filter((t) => t !== ''); createEventMutation.mutate({ event_key: newEventType === 'builtin' ? newEventKey : undefined, task_type: newEventType === 'task' ? newEventTaskType : undefined, channels: newEventChannels, targets: targets.length > 0 ? targets : undefined, template: newEventTemplate || undefined, enabled: newEventEnabled, }); }; return (
{eventsQuery.isLoading ? ( ) : eventsQuery.isError ? (
eventsQuery.refetch()} className='justify-center' />
) : (eventsQuery.data ?? []).length === 0 ? ( ) : (
ID {t('colEvent')} {t('colChannels')} {t('colTargets')} {t('colStatus')} {t('colActions')} {(eventsQuery.data ?? []).map((event) => ( handleEditEventClick(event)} > {event.id}
{event.name} {event.task_type && ( {t('taskBadge')} )}
{event.event_key}
{(event.channels ?? []).length === 0 ? ( {t('noChannelSpecified')} ) : ( event.channels.map((ch) => ( {ch === 'email' ? t('emailChannel') : ch} )) )}
{!event.targets || event.targets.length === 0 ? ( - ) : ( event.targets.map((t) => ( {t} )) )}
e.stopPropagation()} > toggleEventMutation.mutate(event.id) } className='scale-75' /> e.stopPropagation()} >
{t('configure')} {t('delete')}
))}
)} {/* ==================== 对话框:新增事件 ==================== */} {t('addEventDialogTitle')} {t('addEventDialogDesc')}
{newEventType === 'builtin' ? (
{builtInEventsQuery.isLoading ? (
{t('loadingSystemEvents')}
) : availableBuiltInEvents.length === 0 ? (
{t('allBuiltinEventsConfigured')}
) : ( )}
) : (
{taskTypesQuery.isLoading ? (
{t('loadingAsyncTasks')}
) : (taskTypesQuery.data ?? []).length === 0 ? (
{t('noAvailableTasks')}
) : ( )}
)} {newEventType === 'builtin' && newEventKey && (
{t('eventDescription')} {availableBuiltInEvents.find((e) => e.key === newEventKey) ?.description || t('noDescription')}
)} {newEventType === 'task' && newEventTaskType && (
{t('taskDescription')} {(taskTypesQuery.data ?? []).find( (t) => t.asynq_task === newEventTaskType, )?.description || t('noDescription')}
)}
{availableChannels.map((ch) => ( ))} {availableChannels.length === 0 && (
{t('noAvailableChannels')}
)}
setNewEventTargets(e.target.value)} className='text-xs h-9' />
{newEventType === 'task' ? t('taskTemplateVars') : t('eventTemplateVars')}