import { useEffect, useMemo, useState } from 'react' import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { Plus, Edit, Trash2, Save } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import dayjs from '@/lib/dayjs' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Textarea } from '@/components/ui/textarea' import { DateTimePicker } from '@/components/datetime-picker' import { StatusBadge } from '@/components/status-badge' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' type Announcement = { id: number content: string publishDate: string type: 'default' | 'ongoing' | 'success' | 'warning' | 'error' extra?: string } type AnnouncementsSectionProps = { enabled: boolean data: string } const announcementSchema = z.object({ content: z .string() .min(1, 'Content is required') .max(500, 'Content must be less than 500 characters'), publishDate: z.string().min(1, 'Publish date is required'), type: z.enum(['default', 'ongoing', 'success', 'warning', 'error']), extra: z .string() .max(100, 'Extra must be less than 100 characters') .optional(), }) type AnnouncementFormValues = z.infer const typeOptions = [ { value: 'default', label: 'Default', color: 'bg-gray-500', badgeVariant: 'neutral' as const, }, { value: 'ongoing', label: 'Ongoing', color: 'bg-blue-500', badgeVariant: 'info' as const, }, { value: 'success', label: 'Success', color: 'bg-green-500', badgeVariant: 'success' as const, }, { value: 'warning', label: 'Warning', color: 'bg-orange-500', badgeVariant: 'warning' as const, }, { value: 'error', label: 'Error', color: 'bg-red-500', badgeVariant: 'danger' as const, }, ] export function AnnouncementsSection({ enabled, data, }: AnnouncementsSectionProps) { const { t } = useTranslation() const updateOption = useUpdateOption() const [announcements, setAnnouncements] = useState([]) const [isEnabled, setIsEnabled] = useState(enabled) const [hasChanges, setHasChanges] = useState(false) const [selectedIds, setSelectedIds] = useState([]) const [showDialog, setShowDialog] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [editingAnnouncement, setEditingAnnouncement] = useState(null) const [deleteTarget, setDeleteTarget] = useState<'single' | 'batch'>('single') const form = useForm({ resolver: zodResolver(announcementSchema), defaultValues: { content: '', publishDate: new Date().toISOString(), type: 'default', extra: '', }, }) useEffect(() => { try { const parsed = JSON.parse(data || '[]') if (Array.isArray(parsed)) { setAnnouncements( parsed.map((item, idx) => ({ ...item, id: item.id || idx + 1, })) ) } } catch { setAnnouncements([]) } }, [data]) useEffect(() => { setIsEnabled(enabled) }, [enabled]) const handleToggleEnabled = async (checked: boolean) => { try { await updateOption.mutateAsync({ key: 'console_setting.announcements_enabled', value: checked, }) setIsEnabled(checked) toast.success(t('Setting saved')) } catch { toast.error(t('Failed to update setting')) } } const handleAdd = () => { setEditingAnnouncement(null) form.reset({ content: '', publishDate: new Date().toISOString(), type: 'default', extra: '', }) setShowDialog(true) } const handleEdit = (announcement: Announcement) => { setEditingAnnouncement(announcement) form.reset({ content: announcement.content, publishDate: announcement.publishDate, type: announcement.type, extra: announcement.extra || '', }) setShowDialog(true) } const handleDelete = (announcement: Announcement) => { setEditingAnnouncement(announcement) setDeleteTarget('single') setShowDeleteDialog(true) } const handleBatchDelete = () => { if (selectedIds.length === 0) { toast.error(t('Please select items to delete')) return } setDeleteTarget('batch') setShowDeleteDialog(true) } const confirmDelete = () => { if (deleteTarget === 'single' && editingAnnouncement) { setAnnouncements((prev) => prev.filter((item) => item.id !== editingAnnouncement.id) ) setHasChanges(true) toast.success(t('Announcement deleted. Click "Save Settings" to apply.')) } else if (deleteTarget === 'batch') { setAnnouncements((prev) => prev.filter((item) => !selectedIds.includes(item.id)) ) setSelectedIds([]) setHasChanges(true) toast.success( t('{{count}} announcements deleted. Click "Save Settings" to apply.', { count: selectedIds.length, }) ) } setShowDeleteDialog(false) setEditingAnnouncement(null) } const handleSubmitForm = (values: AnnouncementFormValues) => { if (editingAnnouncement) { setAnnouncements((prev) => prev.map((item) => item.id === editingAnnouncement.id ? { ...item, ...values } : item ) ) toast.success(t('Announcement updated. Click "Save Settings" to apply.')) } else { const newId = Math.max(...announcements.map((item) => item.id), 0) + 1 setAnnouncements((prev) => [...prev, { id: newId, ...values }]) toast.success(t('Announcement added. Click "Save Settings" to apply.')) } setHasChanges(true) setShowDialog(false) } const handleSaveAll = async () => { try { await updateOption.mutateAsync({ key: 'console_setting.announcements', value: JSON.stringify(announcements), }) setHasChanges(false) toast.success(t('Announcements saved successfully')) } catch { toast.error(t('Failed to save announcements')) } } const toggleSelectAll = (checked: boolean) => { setSelectedIds(checked ? announcements.map((item) => item.id) : []) } const toggleSelectOne = (id: number, checked: boolean) => { setSelectedIds((prev) => checked ? [...prev, id] : prev.filter((item) => item !== id) ) } const sortedAnnouncements = useMemo(() => { return [...announcements].sort((a, b) => { return ( new Date(b.publishDate).getTime() - new Date(a.publishDate).getTime() ) }) }, [announcements]) const getRelativeTime = (date: string) => { const now = new Date() const past = new Date(date) const diffMs = now.getTime() - past.getTime() const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMins / 60) const diffDays = Math.floor(diffHours / 24) if (diffMins < 60) return `${diffMins}m ago` if (diffHours < 24) return `${diffHours}h ago` return `${diffDays}d ago` } return (
{t('Enabled')}
0 } onCheckedChange={toggleSelectAll} /> {t('Content')} {t('Publish Date')} {t('Type')} {t('Extra')} {t('Actions')} {sortedAnnouncements.length === 0 ? ( {t( 'No announcements yet. Click "Add Announcement" to create one.' )} ) : ( sortedAnnouncements.map((announcement) => ( toggleSelectOne(announcement.id, checked as boolean) } /> {announcement.content}
{getRelativeTime(announcement.publishDate)} {dayjs(announcement.publishDate).format( 'YYYY-MM-DD HH:mm:ss' )}
opt.value === announcement.type )?.label } variant={ typeOptions.find( (opt) => opt.value === announcement.type )?.badgeVariant ?? 'neutral' } copyable={false} /> {announcement.extra || '-'}
)) )}
{editingAnnouncement ? t('Edit Announcement') : t('Add Announcement')} {t('Create or update system announcements for the dashboard')}
( {t('Content')}