import { useEffect, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Check, Clock, Lock, Mail, Pencil, Play, Repeat, Search, Send, Trash2, X } from 'lucide-react' import { deleteAdminConference, listAdminConferences, sendConferenceInvitations, updateAdminConference, type AdminConferenceOut, type AdminConferenceUpdatePayload, } from '@/api/admin' import type { ConferenceStatus, SummaryRecipientsMode } from '@/api/conferences' import { ApiError } from '@/api/client' import { useToast } from '@/components/ui/ToastProvider' import { formatLocalDateTime, localDateTimeToIso, toLocalDateTimeParts } from '@/lib/localTime' import { formatRecurrenceSummary } from '@/lib/recurrenceFormat' const PAGE_SIZE = 10 const STATUS_FILTERS: { value: ConferenceStatus | 'all'; label: string }[] = [ { value: 'all', label: 'Все' }, { value: 'scheduled', label: 'Запланированные' }, { value: 'active', label: 'Активные' }, { value: 'ended', label: 'Завершённые' }, ] function StatusBadge({ conference }: { conference: AdminConferenceOut }) { if (conference.status === 'active') { return ( ) } if (conference.status === 'ended') { return ( ) } return ( ) } /** Форма быстрого редактирования конференции из админки (title/время/пароль/рассылка). */ function EditConferenceModal({ conference, onClose, onDelete, deleting, }: { conference: AdminConferenceOut onClose: () => void /** Удаление конференции — та же мутация, что и у корзины в строке таблицы (см. AdminConferencesTab). */ onDelete: () => void deleting: boolean }) { const queryClient = useQueryClient() const toast = useToast() const initialSlot = conference.scheduled_at ? toLocalDateTimeParts(conference.scheduled_at) : { date: '', time: '' } const [title, setTitle] = useState(conference.title ?? '') const [dateStr, setDateStr] = useState(initialSlot.date) const [timeStr, setTimeStr] = useState(initialSlot.time) const [durationMinutes, setDurationMinutes] = useState(conference.duration_minutes ?? 30) const [isClosed, setIsClosed] = useState(conference.is_closed) const [password, setPassword] = useState('') const [summaryRecipients, setSummaryRecipients] = useState<'' | SummaryRecipientsMode>( conference.summary_recipients ?? '', ) const [confirmDelete, setConfirmDelete] = useState(false) const mutation = useMutation({ mutationFn: (payload: AdminConferenceUpdatePayload) => updateAdminConference(conference.id, payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin', 'conferences'] }) toast.show('Изменения сохранены', 'success') onClose() }, onError: () => toast.show('Не удалось сохранить изменения', 'error'), }) function handleSubmit() { const payload: AdminConferenceUpdatePayload = { title: title.trim(), duration_minutes: durationMinutes, is_closed: isClosed, summary_recipients: summaryRecipients === '' ? null : summaryRecipients, } if (dateStr && timeStr) payload.scheduled_at = localDateTimeToIso(dateStr, timeStr) if (isClosed && password) payload.password = password mutation.mutate(payload) } return (
e.stopPropagation()}>

Редактирование конференции

{conference.is_pinned && conference.recurrence && (

)}
{ e.preventDefault() handleSubmit() }} >
setTitle(e.target.value)} maxLength={255} />
{!conference.is_pinned && (
setDateStr(e.target.value)} />
setTimeStr(e.target.value)} />
)}
setDurationMinutes(Number(e.target.value) || 0)} />
Закрытая по паролю Вход только по паролю, включая гостей
{isClosed && (
setPassword(e.target.value)} />
)}
{confirmDelete ? (
Удалить без возможности восстановления?
) : ( )}
) } /** Модалка ручной рассылки .ics-приглашений по конкретной конференции. */ function InviteModal({ conference, onClose }: { conference: AdminConferenceOut; onClose: () => void }) { const [emailsText, setEmailsText] = useState('') const toast = useToast() const mutation = useMutation({ mutationFn: () => { const emails = emailsText .split(/[\n,;]+/) .map((e) => e.trim()) .filter(Boolean) return sendConferenceInvitations(conference.id, emails.length > 0 ? emails : undefined) }, onSuccess: () => { toast.show('Рассылка приглашений поставлена в очередь', 'success') onClose() }, onError: () => toast.show('Не удалось поставить рассылку', 'error'), }) return (
e.stopPropagation()}>

Рассылка приглашений

«{conference.title ?? conference.number}» — оставьте поле пустым, чтобы разослать по умолчанию (владельцу и участникам прошлых сеансов).