import {useCallback, useEffect, useState} from 'react'; import {useTranslation} from 'react-i18next'; import {Loader2, Search, Send, X} from 'lucide-react'; import {toast} from 'sonner'; import {Button} from '@/components/ui/button'; import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import {Input} from '@/components/ui/input'; import {Label} from '@/components/ui/label'; import {Textarea} from '@/components/ui/textarea'; import PageContainer from '../components/PageContainer.jsx'; import FitText from '../components/FitText.jsx'; import {fetchUsers} from '../utils/admin.js'; import {fetchBroadcasts, fetchRecipientCount, sendBroadcast, sendTestBroadcast} from '../utils/broadcasts.js'; import {formatDateTime} from '../utils/format.js'; import {MAX_BROADCAST_BODY_LENGTH, MAX_BROADCAST_SUBJECT_LENGTH} from '../utils/limits.js'; const emptyForm = () => ({subjectEn: '', subjectDe: '', bodyEn: '', bodyDe: ''}); const SEARCH_SIZE = 20; /** * PLATFORM_ADMIN only: write a mail in both languages, send it to all active users or to picked ones, and see the * history with the counts. Auth checks the role. */ export default function AdminBroadcastPage() { const {t, i18n} = useTranslation(); const [entries, setEntries] = useState([]); const [status, setStatus] = useState('loading'); const [form, setForm] = useState(emptyForm); const [mode, setMode] = useState('all'); const [picked, setPicked] = useState([]); const [query, setQuery] = useState(''); const [found, setFound] = useState([]); const [confirm, setConfirm] = useState(null); const [busy, setBusy] = useState(false); const load = useCallback(async () => { try { const response = await fetchBroadcasts(); if (response.status === 401) return; if (response.status === 403) { setStatus('forbidden'); return; } if (!response.ok) { setStatus('error'); return; } setEntries((await response.json()).results); setStatus('ready'); } catch (err) { console.error('Admin broadcast error:', err); setStatus('error'); } }, []); useEffect(() => { load(); }, [load]); const trimmed = () => ({ subjectEn: form.subjectEn.trim(), subjectDe: form.subjectDe.trim(), bodyEn: form.bodyEn.trim(), bodyDe: form.bodyDe.trim(), }); const incomplete = Object.values(trimmed()).some((value) => !value); const userIds = () => (mode === 'all' ? null : picked.map((user) => user.id)); const change = (field) => (event) => setForm({...form, [field]: event.target.value}); const sendTest = async () => { setBusy(true); try { const response = await sendTestBroadcast(trimmed()); if (response.status === 401) return; if (response.ok) toast.success(t('ADMIN_BROADCAST.TEST_SENT')); else toast.error(t('ADMIN_BROADCAST.ERROR')); } catch (err) { console.error('Admin broadcast test error:', err); toast.error(t('INFO.ERROR.NETWORK_FAILURE')); } finally { setBusy(false); } }; const submit = async (event) => { event.preventDefault(); setConfirm({count: null}); try { const response = await fetchRecipientCount(userIds()); if (response.status === 401) return; if (!response.ok) throw new Error(`count ${response.status}`); const {count} = await response.json(); setConfirm((current) => (current ? {count} : current)); } catch (err) { console.error('Admin broadcast count error:', err); toast.error(t('ADMIN_BROADCAST.ERROR')); setConfirm(null); } }; const send = async () => { setBusy(true); try { const response = await sendBroadcast({...trimmed(), userIds: userIds()}); if (response.status === 401) return; if (response.ok) { const data = await response.json(); toast.success(t('ADMIN_BROADCAST.SENT', {count: data.recipientCount})); setConfirm(null); setForm(emptyForm()); setPicked([]); setFound([]); await load(); } else { toast.error(t(response.status === 409 ? 'ADMIN_BROADCAST.NO_RECIPIENTS' : 'ADMIN_BROADCAST.ERROR')); } } catch (err) { console.error('Admin broadcast send error:', err); toast.error(t('INFO.ERROR.NETWORK_FAILURE')); } finally { setBusy(false); } }; const search = async () => { try { const response = await fetchUsers(query.trim(), 'ACTIVE', 0, SEARCH_SIZE); if (response.status === 401) return; if (!response.ok) throw new Error(`search ${response.status}`); const {content} = await response.json(); setFound(content.filter((user) => !user.blocked && !user.deletedAt)); } catch (err) { console.error('Admin broadcast search error:', err); toast.error(t('ADMIN_BROADCAST.ERROR')); } }; const toggle = (user) => setPicked((current) => (current.some((p) => p.id === user.id) ? current.filter((p) => p.id !== user.id) : [...current, {id: user.id, email: user.email, displayName: user.displayName}])); if (status === 'forbidden') { return (

{t('ADMIN_BROADCAST.NO_ACCESS')}

); } return ( {t('ADMIN_BROADCAST.NEW')}